Initial commit
This commit is contained in:
575
skill/references/layouts.md
Normal file
575
skill/references/layouts.md
Normal file
@@ -0,0 +1,575 @@
|
||||
# Textual Layout Patterns
|
||||
|
||||
Common layout recipes for Textual applications.
|
||||
|
||||
## Layout Types
|
||||
|
||||
### Vertical (Default)
|
||||
|
||||
Stack widgets vertically:
|
||||
```python
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Label
|
||||
|
||||
class VerticalApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label("Top")
|
||||
yield Label("Middle")
|
||||
yield Label("Bottom")
|
||||
|
||||
# Or explicit CSS
|
||||
CSS = """
|
||||
Screen {
|
||||
layout: vertical;
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
### Horizontal
|
||||
|
||||
Arrange widgets side-by-side:
|
||||
```python
|
||||
from textual.containers import Horizontal
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Horizontal():
|
||||
yield Label("Left")
|
||||
yield Label("Center")
|
||||
yield Label("Right")
|
||||
|
||||
# Or via CSS
|
||||
CSS = """
|
||||
Horizontal {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
Horizontal > Label {
|
||||
width: 1fr; /* Equal distribution */
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
### Grid
|
||||
|
||||
Create grid layouts:
|
||||
```python
|
||||
class GridApp(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
layout: grid;
|
||||
grid-size: 3 2; /* 3 columns, 2 rows */
|
||||
grid-gutter: 1;
|
||||
}
|
||||
|
||||
.cell {
|
||||
border: solid $accent;
|
||||
height: 100%;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
for i in range(6):
|
||||
yield Label(f"Cell {i+1}", classes="cell")
|
||||
```
|
||||
|
||||
Advanced grid with spanning:
|
||||
```python
|
||||
CSS = """
|
||||
Screen {
|
||||
layout: grid;
|
||||
grid-size: 4; /* 4 columns, auto rows */
|
||||
}
|
||||
|
||||
#header {
|
||||
column-span: 4; /* Spans all columns */
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
row-span: 2; /* Spans 2 rows */
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
### Dock Layout
|
||||
|
||||
Dock widgets to edges:
|
||||
```python
|
||||
from textual.widgets import Header, Footer
|
||||
|
||||
class DockedApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header() # Docked to top
|
||||
yield Label("Content") # Takes remaining space
|
||||
yield Footer() # Docked to bottom
|
||||
|
||||
# Custom docking
|
||||
CSS = """
|
||||
#sidebar {
|
||||
dock: left;
|
||||
width: 30;
|
||||
}
|
||||
|
||||
#toolbar {
|
||||
dock: top;
|
||||
height: 3;
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Split Screen (Vertical)
|
||||
|
||||
Two panels side-by-side:
|
||||
```python
|
||||
class SplitScreen(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
layout: horizontal;
|
||||
}
|
||||
|
||||
#left-panel {
|
||||
width: 30%;
|
||||
border-right: solid $accent;
|
||||
}
|
||||
|
||||
#right-panel {
|
||||
width: 70%;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="left-panel"):
|
||||
yield Label("Sidebar")
|
||||
with Container(id="right-panel"):
|
||||
yield Label("Main content")
|
||||
```
|
||||
|
||||
### Split Screen (Horizontal)
|
||||
|
||||
Two panels stacked:
|
||||
```python
|
||||
class SplitScreenHorizontal(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
layout: vertical;
|
||||
}
|
||||
|
||||
#top-panel {
|
||||
height: 50%;
|
||||
border-bottom: solid $accent;
|
||||
}
|
||||
|
||||
#bottom-panel {
|
||||
height: 50%;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="top-panel"):
|
||||
yield Label("Top content")
|
||||
with Container(id="bottom-panel"):
|
||||
yield Label("Bottom content")
|
||||
```
|
||||
|
||||
### Three-Column Layout
|
||||
|
||||
Classic sidebar-content-sidebar:
|
||||
```python
|
||||
class ThreeColumn(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
layout: horizontal;
|
||||
}
|
||||
|
||||
#left-sidebar {
|
||||
width: 20;
|
||||
}
|
||||
|
||||
#content {
|
||||
width: 1fr; /* Take remaining space */
|
||||
}
|
||||
|
||||
#right-sidebar {
|
||||
width: 25;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="left-sidebar"):
|
||||
yield Label("Menu")
|
||||
with Container(id="content"):
|
||||
yield Label("Main")
|
||||
with Container(id="right-sidebar"):
|
||||
yield Label("Info")
|
||||
```
|
||||
|
||||
### Dashboard Grid
|
||||
|
||||
Grid-based dashboard:
|
||||
```python
|
||||
class Dashboard(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
layout: grid;
|
||||
grid-size: 2 3; /* 2 columns, 3 rows */
|
||||
grid-gutter: 1 2; /* vertical horizontal */
|
||||
}
|
||||
|
||||
#header {
|
||||
column-span: 2;
|
||||
height: 3;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
border: solid $primary;
|
||||
padding: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(id="header")
|
||||
yield Static("Users: 1,234", classes="metric-card")
|
||||
yield Static("Revenue: $12K", classes="metric-card")
|
||||
yield Static("Growth: +15%", classes="metric-card")
|
||||
yield Static("Active: 567", classes="metric-card")
|
||||
```
|
||||
|
||||
### Centered Content
|
||||
|
||||
Center content horizontally and vertically:
|
||||
```python
|
||||
class CenteredApp(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
#dialog {
|
||||
width: 60;
|
||||
height: 20;
|
||||
border: thick $accent;
|
||||
padding: 2;
|
||||
background: $surface;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="dialog"):
|
||||
yield Label("Centered Dialog")
|
||||
yield Button("OK")
|
||||
```
|
||||
|
||||
### Scrollable Content
|
||||
|
||||
Handle overflow with scrolling:
|
||||
```python
|
||||
from textual.containers import ScrollableContainer
|
||||
|
||||
class ScrollableApp(App):
|
||||
CSS = """
|
||||
#content {
|
||||
height: 100%;
|
||||
border: solid $primary;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with ScrollableContainer(id="content"):
|
||||
for i in range(100):
|
||||
yield Label(f"Line {i+1}")
|
||||
```
|
||||
|
||||
### Tabbed Interface
|
||||
|
||||
Tab-based navigation:
|
||||
```python
|
||||
from textual.widgets import TabbedContent, TabPane
|
||||
|
||||
class TabbedApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
with TabbedContent():
|
||||
with TabPane("Dashboard"):
|
||||
yield Label("Dashboard content")
|
||||
with TabPane("Users"):
|
||||
yield Label("Users content")
|
||||
with TabPane("Settings"):
|
||||
yield Label("Settings content")
|
||||
```
|
||||
|
||||
## Sizing Strategies
|
||||
|
||||
### Fixed Sizes
|
||||
|
||||
Absolute dimensions:
|
||||
```css
|
||||
#widget {
|
||||
width: 40; /* 40 columns */
|
||||
height: 20; /* 20 rows */
|
||||
}
|
||||
```
|
||||
|
||||
### Fractional Units
|
||||
|
||||
Proportional sizing:
|
||||
```css
|
||||
#sidebar {
|
||||
width: 1fr; /* 1 part */
|
||||
}
|
||||
|
||||
#content {
|
||||
width: 3fr; /* 3 parts (3x sidebar) */
|
||||
}
|
||||
```
|
||||
|
||||
### Percentage
|
||||
|
||||
Relative to parent:
|
||||
```css
|
||||
#widget {
|
||||
width: 50%; /* Half of parent width */
|
||||
height: 100%; /* Full parent height */
|
||||
}
|
||||
```
|
||||
|
||||
### Auto Sizing
|
||||
|
||||
Size to content:
|
||||
```css
|
||||
#widget {
|
||||
width: auto; /* Width matches content */
|
||||
height: auto; /* Height matches content */
|
||||
}
|
||||
```
|
||||
|
||||
### Min/Max Constraints
|
||||
|
||||
Bounded sizing:
|
||||
```css
|
||||
#widget {
|
||||
width: 1fr;
|
||||
min-width: 30;
|
||||
max-width: 80;
|
||||
}
|
||||
```
|
||||
|
||||
## Spacing and Alignment
|
||||
|
||||
### Padding
|
||||
|
||||
Space inside widget:
|
||||
```css
|
||||
#widget {
|
||||
padding: 1; /* All sides */
|
||||
padding: 1 2; /* Vertical Horizontal */
|
||||
padding: 1 2 1 2; /* Top Right Bottom Left */
|
||||
padding-top: 1; /* Individual sides */
|
||||
}
|
||||
```
|
||||
|
||||
### Margin
|
||||
|
||||
Space outside widget:
|
||||
```css
|
||||
#widget {
|
||||
margin: 1;
|
||||
margin: 0 2; /* No vertical, 2 horizontal */
|
||||
margin-left: 1;
|
||||
}
|
||||
```
|
||||
|
||||
### Alignment
|
||||
|
||||
Position within container:
|
||||
```css
|
||||
Container {
|
||||
align: center middle; /* Horizontal Vertical */
|
||||
align: left top;
|
||||
align: right bottom;
|
||||
}
|
||||
|
||||
/* Content alignment (for containers) */
|
||||
Container {
|
||||
content-align: center middle;
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Layouts
|
||||
|
||||
### Container Queries
|
||||
|
||||
Adjust based on container size:
|
||||
```python
|
||||
class ResponsiveApp(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
layout: horizontal;
|
||||
}
|
||||
|
||||
/* Default mobile layout */
|
||||
#content {
|
||||
layout: vertical;
|
||||
}
|
||||
|
||||
/* Desktop layout when width > 80 */
|
||||
Screen:width-gt-80 #content {
|
||||
layout: horizontal;
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
### Conditional Layouts
|
||||
|
||||
Switch layouts based on screen size:
|
||||
```python
|
||||
def compose(self) -> ComposeResult:
|
||||
if self.size.width > 100:
|
||||
# Wide layout
|
||||
with Horizontal():
|
||||
yield self.make_sidebar()
|
||||
yield self.make_content()
|
||||
else:
|
||||
# Narrow layout
|
||||
with Vertical():
|
||||
yield self.make_content()
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Modal Overlay
|
||||
|
||||
Centered modal dialog:
|
||||
```python
|
||||
from textual.screen import ModalScreen
|
||||
from textual.containers import Container
|
||||
|
||||
class Modal(ModalScreen[bool]):
|
||||
CSS = """
|
||||
Modal {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
#dialog {
|
||||
width: 50;
|
||||
height: 15;
|
||||
border: thick $accent;
|
||||
background: $surface;
|
||||
padding: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="dialog"):
|
||||
yield Label("Are you sure?")
|
||||
with Horizontal():
|
||||
yield Button("Yes", variant="primary")
|
||||
yield Button("No", variant="error")
|
||||
```
|
||||
|
||||
### Sidebar Toggle
|
||||
|
||||
Collapsible sidebar:
|
||||
```python
|
||||
class SidebarApp(App):
|
||||
show_sidebar = reactive(True)
|
||||
|
||||
CSS = """
|
||||
#sidebar {
|
||||
width: 30;
|
||||
transition: width 200ms;
|
||||
}
|
||||
|
||||
#sidebar.hidden {
|
||||
width: 0;
|
||||
display: none;
|
||||
}
|
||||
"""
|
||||
|
||||
def watch_show_sidebar(self, show: bool) -> None:
|
||||
sidebar = self.query_one("#sidebar")
|
||||
sidebar.set_class(not show, "hidden")
|
||||
```
|
||||
|
||||
### Masonry Layout
|
||||
|
||||
Staggered grid:
|
||||
```python
|
||||
class MasonryLayout(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
layout: grid;
|
||||
grid-size: 3;
|
||||
grid-gutter: 1;
|
||||
}
|
||||
|
||||
.card {
|
||||
height: auto;
|
||||
border: solid $primary;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
.card.tall {
|
||||
row-span: 2;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static("Short card", classes="card")
|
||||
yield Static("Tall card\n\n\n", classes="card tall")
|
||||
yield Static("Short", classes="card")
|
||||
```
|
||||
|
||||
### Split Resizable
|
||||
|
||||
Adjustable split panels:
|
||||
```python
|
||||
class ResizableSplit(App):
|
||||
left_width = reactive(30)
|
||||
|
||||
CSS = """
|
||||
#left {
|
||||
width: var(--left-width);
|
||||
}
|
||||
|
||||
#right {
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
#divider {
|
||||
width: 1;
|
||||
background: $accent;
|
||||
}
|
||||
"""
|
||||
|
||||
def watch_left_width(self, width: int) -> None:
|
||||
self.set_var("left-width", width)
|
||||
```
|
||||
|
||||
## Layout Debugging
|
||||
|
||||
Use borders to visualize layout:
|
||||
```css
|
||||
* {
|
||||
border: solid red; /* Temporary debugging */
|
||||
}
|
||||
|
||||
Container {
|
||||
border: solid blue;
|
||||
}
|
||||
|
||||
Widget {
|
||||
border: solid green;
|
||||
}
|
||||
```
|
||||
|
||||
Use Textual devtools:
|
||||
```bash
|
||||
textual run --dev app.py
|
||||
```
|
||||
|
||||
Add debug info to widgets:
|
||||
```python
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label(f"Size: {self.size}")
|
||||
yield Label(f"Region: {self.region}")
|
||||
```
|
||||
284
skill/references/official-guides-index.md
Normal file
284
skill/references/official-guides-index.md
Normal file
@@ -0,0 +1,284 @@
|
||||
# Textual Official Guide Index
|
||||
|
||||
High-level index of every guide from the official Textual documentation. Use `web_fetch` to retrieve full content on-demand when needed.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
**URL:** https://textual.textualize.io/getting_started/
|
||||
**Topics:** Installing Textual, requirements, first app
|
||||
**When to fetch:** User asks about installation, setup, or getting started
|
||||
|
||||
### Tutorial
|
||||
**URL:** https://textual.textualize.io/tutorial/
|
||||
**Topics:** Building a stopwatch app, complete walkthrough, reactive attributes, widgets
|
||||
**When to fetch:** User wants step-by-step tutorial or building their first app
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### App Basics
|
||||
**URL:** https://textual.textualize.io/guide/app/
|
||||
**Topics:** Creating apps, running apps, compose method, mounting widgets, app lifecycle, application mode, inline mode, suspending apps
|
||||
**When to fetch:** Questions about app structure, lifecycle, or basic app operations
|
||||
|
||||
### Widgets
|
||||
**URL:** https://textual.textualize.io/guide/widgets/
|
||||
**Topics:** Creating custom widgets, widget communication, compound widgets, render method, line API, renderables, uni-directional data flow, widget design patterns
|
||||
**When to fetch:** Creating custom widgets, widget architecture, or advanced widget patterns
|
||||
|
||||
### Layout
|
||||
**URL:** https://textual.textualize.io/guide/layout/
|
||||
**Topics:** Vertical layout, horizontal layout, grid layout, dock layout, layers, FR units, container layouts, grid-size, grid-columns, grid-rows, row-span, column-span
|
||||
**When to fetch:** Layout questions, arranging widgets, grid systems, positioning
|
||||
|
||||
### CSS & Styling
|
||||
**URL:** https://textual.textualize.io/guide/CSS/
|
||||
**Topics:** Textual CSS basics, selectors (type, ID, class, universal), pseudo-classes, CSS_PATH, live editing, combinator selectors
|
||||
**When to fetch:** CSS syntax, selectors, styling fundamentals
|
||||
|
||||
### Styles
|
||||
**URL:** https://textual.textualize.io/guide/styles/
|
||||
**Topics:** Style properties, colors (hex, RGB, HSL), units (%, fr, vw, vh, w, h), box model, box-sizing, dimensions, spacing
|
||||
**When to fetch:** Specific style properties, units, colors, dimensions
|
||||
|
||||
### Design System / Themes
|
||||
**URL:** https://textual.textualize.io/guide/design/
|
||||
**Topics:** Theme system, built-in themes, creating custom themes, color variables, semantic colors, $text variables, theme switching, design tokens
|
||||
**When to fetch:** Theming, color systems, design tokens, theme customization
|
||||
|
||||
## Interaction & Events
|
||||
|
||||
### Input
|
||||
**URL:** https://textual.textualize.io/guide/input/
|
||||
**Topics:** Keyboard input, mouse input, focus, key events, mouse events, input handling
|
||||
**When to fetch:** Handling keyboard/mouse input, focus management, input events
|
||||
|
||||
### Events
|
||||
**URL:** https://textual.textualize.io/guide/events/
|
||||
**Topics:** Event system, message handlers, event bubbling, preventing events, custom events, event lifecycle
|
||||
**When to fetch:** Event handling, custom events, event propagation
|
||||
|
||||
### Actions
|
||||
**URL:** https://textual.textualize.io/guide/actions/
|
||||
**Topics:** Action system, key bindings, BINDINGS, action_* methods, built-in actions, custom actions
|
||||
**When to fetch:** Keyboard shortcuts, actions, key bindings
|
||||
|
||||
### Reactivity
|
||||
**URL:** https://textual.textualize.io/guide/reactivity/
|
||||
**Topics:** Reactive attributes, watch methods, compute methods, reactive decorators, smart refresh, recompose, data binding
|
||||
**When to fetch:** Reactive programming, automatic updates, computed values, watchers
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Screens
|
||||
**URL:** https://textual.textualize.io/guide/screens/
|
||||
**Topics:** Screen stack, push_screen, pop_screen, ModalScreen, screen navigation, installed screens, SCREENS, screen opacity
|
||||
**When to fetch:** Multi-screen apps, navigation, modal dialogs, screen management
|
||||
|
||||
### Query
|
||||
**URL:** https://textual.textualize.io/guide/queries/
|
||||
**Topics:** Querying widgets, query_one, query, set methods, DOM traversal, selectors in queries
|
||||
**When to fetch:** Finding widgets, DOM navigation, bulk operations
|
||||
|
||||
### Workers
|
||||
**URL:** https://textual.textualize.io/guide/workers/
|
||||
**Topics:** Background tasks, @work decorator, run_worker, thread workers, async workers, worker lifecycle, cancellation
|
||||
**When to fetch:** Background processing, async operations, threading, long-running tasks
|
||||
|
||||
### Animation
|
||||
**URL:** https://textual.textualize.io/guide/animation/
|
||||
**Topics:** Animating styles, animate method, easing functions, duration, transitions, animation callbacks
|
||||
**When to fetch:** Animations, transitions, easing, style animations
|
||||
|
||||
### Command Palette
|
||||
**URL:** https://textual.textualize.io/guide/command_palette/
|
||||
**Topics:** Built-in command palette, Provider class, fuzzy matching, custom commands, command discovery
|
||||
**When to fetch:** Command palette, custom commands, keyboard-driven interfaces
|
||||
|
||||
## Content & Display
|
||||
|
||||
### Content / Markup
|
||||
**URL:** https://textual.textualize.io/guide/content/
|
||||
**Topics:** Content markup, Rich renderables, styling text, markup tags, links, clickable actions, content objects
|
||||
**When to fetch:** Text formatting, markup syntax, Rich renderables, styled text
|
||||
|
||||
### Rich Content
|
||||
**URL:** https://textual.textualize.io/guide/rich/
|
||||
**Topics:** Using Rich library, Rich renderables, tables, syntax highlighting, panels, progress bars
|
||||
**When to fetch:** Rich library integration, advanced text formatting, Rich features
|
||||
|
||||
## Development & Debugging
|
||||
|
||||
### Devtools
|
||||
**URL:** https://textual.textualize.io/guide/devtools/
|
||||
**Topics:** textual command, console, run command, live CSS editing, --dev mode, debugging, logging
|
||||
**When to fetch:** Development workflow, debugging, console logging, dev tools
|
||||
|
||||
### Testing
|
||||
**URL:** https://textual.textualize.io/guide/testing/
|
||||
**Topics:** Testing apps, Pilot API, run_test, simulating input, snapshots, unit testing
|
||||
**When to fetch:** Testing, unit tests, test automation, Pilot
|
||||
|
||||
### Performance
|
||||
**URL:** https://textual.textualize.io/guide/performance/
|
||||
**Topics:** Performance optimization, profiling, rendering performance, widget efficiency, best practices
|
||||
**When to fetch:** Performance issues, optimization, profiling
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
### Widgets Reference
|
||||
**URL:** https://textual.textualize.io/widgets/
|
||||
**Topics:** Complete widget reference, all built-in widgets with examples
|
||||
**When to fetch:** Looking for specific widget documentation, widget API details
|
||||
|
||||
### Styles Reference
|
||||
**URL:** https://textual.textualize.io/styles/
|
||||
**Topics:** Complete CSS properties reference, all style properties
|
||||
**When to fetch:** Specific style property details, CSS reference
|
||||
|
||||
### API Reference
|
||||
**URL:** https://textual.textualize.io/api/
|
||||
**Topics:** Complete Python API reference
|
||||
**When to fetch:** API details, method signatures, class documentation
|
||||
|
||||
## Specialized Topics
|
||||
|
||||
### Scrolling
|
||||
**URL:** https://textual.textualize.io/guide/scrolling/
|
||||
**Topics:** Scroll views, ScrollableContainer, scrolling behavior, scroll_visible, programmatic scrolling
|
||||
**When to fetch:** Scrolling issues, scroll containers, programmatic scrolling
|
||||
|
||||
### Tooltips
|
||||
**URL:** https://textual.textualize.io/guide/tooltips/
|
||||
**Topics:** Adding tooltips, tooltip property, tooltip customization
|
||||
**When to fetch:** Tooltips, hover help text
|
||||
|
||||
### Notifications
|
||||
**URL:** https://textual.textualize.io/guide/notifications/
|
||||
**Topics:** Toast notifications, notify method, notification severity, notification styling
|
||||
**When to fetch:** Notifications, alerts, toasts
|
||||
|
||||
### Input Validation
|
||||
**URL:** https://textual.textualize.io/guide/input_validation/
|
||||
**Topics:** Validating input, Validator class, built-in validators, custom validators
|
||||
**When to fetch:** Form validation, input validation, validators
|
||||
|
||||
### Timers
|
||||
**URL:** https://textual.textualize.io/guide/timers/
|
||||
**Topics:** Scheduling tasks, set_timer, set_interval, timer callbacks, timer management
|
||||
**When to fetch:** Scheduled tasks, periodic updates, timers
|
||||
|
||||
### Paths
|
||||
**URL:** https://textual.textualize.io/guide/paths/
|
||||
**Topics:** File paths, resource paths, CSS_PATH, path resolution
|
||||
**When to fetch:** File loading, resource paths, path management
|
||||
|
||||
## Additional Topics
|
||||
|
||||
### FAQ
|
||||
**URL:** https://textual.textualize.io/FAQ/
|
||||
**Topics:** Common questions, troubleshooting, best practices
|
||||
**When to fetch:** Common issues, general questions, troubleshooting
|
||||
|
||||
### Why Textual?
|
||||
**URL:** https://textual.textualize.io/guide/why/
|
||||
**Topics:** Benefits of Textual, use cases, comparison with alternatives
|
||||
**When to fetch:** Understanding Textual benefits, when to use Textual
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
### When to Fetch Guides
|
||||
|
||||
1. **Don't fetch unless needed**: The skill already covers fundamentals. Only fetch when:
|
||||
- User asks about a specific topic not covered in skill
|
||||
- Need detailed API information
|
||||
- Complex examples required
|
||||
- Latest updates needed (docs may be newer than skill)
|
||||
|
||||
2. **Fetch specific sections**: Use targeted URLs for relevant topics
|
||||
|
||||
3. **Combine with skill knowledge**: Use fetched content to supplement, not replace, skill knowledge
|
||||
|
||||
### Example Fetch Patterns
|
||||
|
||||
```python
|
||||
# User asks about command palette
|
||||
web_fetch("https://textual.textualize.io/guide/command_palette/")
|
||||
|
||||
# User needs animation details
|
||||
web_fetch("https://textual.textualize.io/guide/animation/")
|
||||
|
||||
# User wants testing info
|
||||
web_fetch("https://textual.textualize.io/guide/testing/")
|
||||
|
||||
# Need widget reference
|
||||
web_fetch("https://textual.textualize.io/widgets/data_table/")
|
||||
```
|
||||
|
||||
## Quick Reference by Topic
|
||||
|
||||
### Need information about...
|
||||
|
||||
**App Structure** → App Basics guide
|
||||
**Layout & Positioning** → Layout guide
|
||||
**Styling & CSS** → CSS guide, Styles guide, Design guide
|
||||
**User Input** → Input guide, Events guide, Actions guide
|
||||
**Custom Widgets** → Widgets guide
|
||||
**Navigation** → Screens guide
|
||||
**Async Tasks** → Workers guide
|
||||
**Animations** → Animation guide
|
||||
**Commands** → Command Palette guide
|
||||
**Testing** → Testing guide
|
||||
**Development** → Devtools guide
|
||||
**Rich Integration** → Rich Content guide
|
||||
**Form Validation** → Input Validation guide
|
||||
**Auto-Updates** → Reactivity guide
|
||||
|
||||
## Widget-Specific Documentation
|
||||
|
||||
All built-in widgets have dedicated documentation at:
|
||||
`https://textual.textualize.io/widgets/{widget_name}/`
|
||||
|
||||
Common widget docs:
|
||||
- **Button**: https://textual.textualize.io/widgets/button/
|
||||
- **Input**: https://textual.textualize.io/widgets/input/
|
||||
- **DataTable**: https://textual.textualize.io/widgets/data_table/
|
||||
- **Tree**: https://textual.textualize.io/widgets/tree/
|
||||
- **Select**: https://textual.textualize.io/widgets/select/
|
||||
- **TextArea**: https://textual.textualize.io/widgets/text_area/
|
||||
- **ListView**: https://textual.textualize.io/widgets/list_view/
|
||||
- **ProgressBar**: https://textual.textualize.io/widgets/progress_bar/
|
||||
- **Markdown**: https://textual.textualize.io/widgets/markdown/
|
||||
- **MarkdownViewer**: https://textual.textualize.io/widgets/markdown_viewer/
|
||||
- **DirectoryTree**: https://textual.textualize.io/widgets/directory_tree/
|
||||
- **Header**: https://textual.textualize.io/widgets/header/
|
||||
- **Footer**: https://textual.textualize.io/widgets/footer/
|
||||
- **Label**: https://textual.textualize.io/widgets/label/
|
||||
- **Static**: https://textual.textualize.io/widgets/static/
|
||||
- **Log**: https://textual.textualize.io/widgets/log/
|
||||
- **RichLog**: https://textual.textualize.io/widgets/rich_log/
|
||||
- **Sparkline**: https://textual.textualize.io/widgets/sparkline/
|
||||
- **Switch**: https://textual.textualize.io/widgets/switch/
|
||||
- **Checkbox**: https://textual.textualize.io/widgets/checkbox/
|
||||
- **RadioButton**: https://textual.textualize.io/widgets/radio_button/
|
||||
- **RadioSet**: https://textual.textualize.io/widgets/radio_set/
|
||||
- **TabbedContent**: https://textual.textualize.io/widgets/tabbed_content/
|
||||
- **ContentSwitcher**: https://textual.textualize.io/widgets/content_switcher/
|
||||
- **LoadingIndicator**: https://textual.textualize.io/widgets/loading_indicator/
|
||||
|
||||
## Container Widget Documentation
|
||||
|
||||
- **Container**: https://textual.textualize.io/widgets/container/
|
||||
- **Horizontal**: https://textual.textualize.io/widgets/horizontal/
|
||||
- **Vertical**: https://textual.textualize.io/widgets/vertical/
|
||||
- **Grid**: https://textual.textualize.io/widgets/grid/
|
||||
- **ScrollableContainer**: https://textual.textualize.io/widgets/scrollable_container/
|
||||
- **VerticalScroll**: https://textual.textualize.io/widgets/vertical_scroll/
|
||||
- **HorizontalScroll**: https://textual.textualize.io/widgets/horizontal_scroll/
|
||||
|
||||
## Notes
|
||||
|
||||
- All URLs follow pattern: `https://textual.textualize.io/{section}/{topic}/`
|
||||
- Official docs are actively maintained and may have updates not in this skill
|
||||
- Use web_fetch with specific URLs when detailed or latest information needed
|
||||
- Combine official docs with skill knowledge for best results
|
||||
700
skill/references/styling.md
Normal file
700
skill/references/styling.md
Normal file
@@ -0,0 +1,700 @@
|
||||
# Textual CSS Styling Guide
|
||||
|
||||
Complete guide to styling Textual applications with TCSS (Textual CSS).
|
||||
|
||||
## CSS Basics
|
||||
|
||||
### Inline Styles
|
||||
|
||||
Define styles directly in widget class:
|
||||
```python
|
||||
class MyWidget(Widget):
|
||||
DEFAULT_CSS = """
|
||||
MyWidget {
|
||||
background: $primary;
|
||||
color: $text;
|
||||
border: solid $accent;
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
### External Stylesheets
|
||||
|
||||
Load from file:
|
||||
```python
|
||||
class MyApp(App):
|
||||
CSS_PATH = "app.tcss" # Load from app.tcss file
|
||||
```
|
||||
|
||||
### Multiple Stylesheets
|
||||
|
||||
Load multiple files:
|
||||
```python
|
||||
class MyApp(App):
|
||||
CSS_PATH = ["base.tcss", "theme.tcss", "overrides.tcss"]
|
||||
```
|
||||
|
||||
## Selectors
|
||||
|
||||
### Type Selectors
|
||||
|
||||
Target widget types:
|
||||
```css
|
||||
Button {
|
||||
background: blue;
|
||||
}
|
||||
|
||||
Label {
|
||||
color: white;
|
||||
}
|
||||
```
|
||||
|
||||
### ID Selectors
|
||||
|
||||
Target specific widgets:
|
||||
```css
|
||||
#submit-button {
|
||||
background: green;
|
||||
}
|
||||
|
||||
#error-message {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
### Class Selectors
|
||||
|
||||
Target classes:
|
||||
```css
|
||||
.highlight {
|
||||
background: yellow;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: solid white;
|
||||
padding: 1;
|
||||
}
|
||||
```
|
||||
|
||||
### Pseudo-classes
|
||||
|
||||
Target widget states:
|
||||
```css
|
||||
Button:hover {
|
||||
background: lighten($primary, 20%);
|
||||
}
|
||||
|
||||
Button:focus {
|
||||
border: thick $accent;
|
||||
}
|
||||
|
||||
Input:focus {
|
||||
border: solid $success;
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
Button:disabled {
|
||||
opacity: 50%;
|
||||
}
|
||||
```
|
||||
|
||||
### Descendant Selectors
|
||||
|
||||
Target nested widgets:
|
||||
```css
|
||||
/* Any Label inside a Container */
|
||||
Container Label {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
/* Direct children only */
|
||||
Container > Label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Specific nesting */
|
||||
#sidebar .menu-item {
|
||||
padding: 1;
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Selectors
|
||||
|
||||
Apply same style to multiple targets:
|
||||
```css
|
||||
Button, Input, Select {
|
||||
border: solid $accent;
|
||||
}
|
||||
|
||||
.error, .warning {
|
||||
font-weight: bold;
|
||||
}
|
||||
```
|
||||
|
||||
## Colors
|
||||
|
||||
### Semantic Colors
|
||||
|
||||
Use theme colors:
|
||||
```css
|
||||
Widget {
|
||||
background: $background;
|
||||
color: $text;
|
||||
border: solid $primary;
|
||||
}
|
||||
|
||||
/* Available semantic colors */
|
||||
$primary /* Primary theme color */
|
||||
$secondary /* Secondary theme color */
|
||||
$accent /* Accent color */
|
||||
$background /* Background color */
|
||||
$surface /* Surface color */
|
||||
$panel /* Panel color */
|
||||
$text /* Primary text color */
|
||||
$text-muted /* Muted text */
|
||||
$text-disabled /* Disabled text */
|
||||
$success /* Success state */
|
||||
$warning /* Warning state */
|
||||
$error /* Error state */
|
||||
$boost /* Highlight color */
|
||||
```
|
||||
|
||||
### Color Formats
|
||||
|
||||
Define custom colors:
|
||||
```css
|
||||
Widget {
|
||||
background: #1e3a8a; /* Hex */
|
||||
color: rgb(255, 255, 255); /* RGB */
|
||||
border-color: rgba(255, 0, 0, 0.5); /* RGBA with alpha */
|
||||
}
|
||||
|
||||
/* Named colors */
|
||||
Widget {
|
||||
background: transparent;
|
||||
color: black;
|
||||
border-color: white;
|
||||
}
|
||||
```
|
||||
|
||||
### Color Functions
|
||||
|
||||
Manipulate colors:
|
||||
```css
|
||||
Widget {
|
||||
background: darken($primary, 20%);
|
||||
color: lighten($text, 10%);
|
||||
border-color: fade($accent, 50%);
|
||||
}
|
||||
```
|
||||
|
||||
## Typography
|
||||
|
||||
### Text Style
|
||||
|
||||
```css
|
||||
Label {
|
||||
text-style: bold; /* bold, italic, underline */
|
||||
text-style: bold italic; /* Multiple styles */
|
||||
text-style: reverse; /* Reverse colors */
|
||||
text-style: strike; /* Strikethrough */
|
||||
}
|
||||
```
|
||||
|
||||
### Text Alignment
|
||||
|
||||
```css
|
||||
Label {
|
||||
text-align: left; /* left, center, right, justify */
|
||||
}
|
||||
```
|
||||
|
||||
### Text Opacity
|
||||
|
||||
```css
|
||||
Label {
|
||||
text-opacity: 70%; /* Semi-transparent text */
|
||||
}
|
||||
```
|
||||
|
||||
## Borders
|
||||
|
||||
### Border Styles
|
||||
|
||||
```css
|
||||
Widget {
|
||||
border: solid $accent; /* Solid border */
|
||||
border: dashed blue; /* Dashed */
|
||||
border: heavy green; /* Heavy */
|
||||
border: double white; /* Double */
|
||||
border: thick $primary; /* Thick */
|
||||
border: none; /* No border */
|
||||
}
|
||||
```
|
||||
|
||||
### Border Sides
|
||||
|
||||
```css
|
||||
Widget {
|
||||
border-top: solid red;
|
||||
border-right: dashed blue;
|
||||
border-bottom: thick green;
|
||||
border-left: double white;
|
||||
}
|
||||
```
|
||||
|
||||
### Border Title
|
||||
|
||||
```css
|
||||
Widget {
|
||||
border: solid $accent;
|
||||
border-title-align: center; /* left, center, right */
|
||||
}
|
||||
```
|
||||
|
||||
## Dimensions
|
||||
|
||||
### Width
|
||||
|
||||
```css
|
||||
Widget {
|
||||
width: 40; /* Fixed columns */
|
||||
width: 50%; /* Percentage of parent */
|
||||
width: 1fr; /* Fractional unit */
|
||||
width: auto; /* Size to content */
|
||||
}
|
||||
```
|
||||
|
||||
### Height
|
||||
|
||||
```css
|
||||
Widget {
|
||||
height: 20; /* Fixed rows */
|
||||
height: 100%; /* Full parent height */
|
||||
height: auto; /* Size to content */
|
||||
}
|
||||
```
|
||||
|
||||
### Min/Max Constraints
|
||||
|
||||
```css
|
||||
Widget {
|
||||
min-width: 20;
|
||||
max-width: 80;
|
||||
min-height: 10;
|
||||
max-height: 50;
|
||||
}
|
||||
```
|
||||
|
||||
## Spacing
|
||||
|
||||
### Padding
|
||||
|
||||
Space inside widget:
|
||||
```css
|
||||
Widget {
|
||||
padding: 1; /* All sides */
|
||||
padding: 1 2; /* Vertical Horizontal */
|
||||
padding: 1 2 3 4; /* Top Right Bottom Left */
|
||||
}
|
||||
|
||||
/* Individual sides */
|
||||
Widget {
|
||||
padding-top: 1;
|
||||
padding-right: 2;
|
||||
padding-bottom: 1;
|
||||
padding-left: 2;
|
||||
}
|
||||
```
|
||||
|
||||
### Margin
|
||||
|
||||
Space outside widget:
|
||||
```css
|
||||
Widget {
|
||||
margin: 1;
|
||||
margin: 0 2;
|
||||
margin: 1 2 1 2;
|
||||
}
|
||||
|
||||
/* Individual sides */
|
||||
Widget {
|
||||
margin-top: 1;
|
||||
margin-right: 2;
|
||||
}
|
||||
```
|
||||
|
||||
## Layout Properties
|
||||
|
||||
### Display
|
||||
|
||||
Control visibility:
|
||||
```css
|
||||
Widget {
|
||||
display: block; /* Visible */
|
||||
display: none; /* Hidden */
|
||||
}
|
||||
```
|
||||
|
||||
### Visibility
|
||||
|
||||
Alternative to display:
|
||||
```css
|
||||
Widget {
|
||||
visibility: visible;
|
||||
visibility: hidden; /* Hidden but takes space */
|
||||
}
|
||||
```
|
||||
|
||||
### Opacity
|
||||
|
||||
Transparency:
|
||||
```css
|
||||
Widget {
|
||||
opacity: 100%; /* Fully opaque */
|
||||
opacity: 50%; /* Semi-transparent */
|
||||
opacity: 0%; /* Fully transparent */
|
||||
}
|
||||
```
|
||||
|
||||
### Layout Type
|
||||
|
||||
```css
|
||||
Container {
|
||||
layout: vertical; /* Stack vertically */
|
||||
layout: horizontal; /* Stack horizontally */
|
||||
layout: grid; /* Grid layout */
|
||||
}
|
||||
```
|
||||
|
||||
### Grid Properties
|
||||
|
||||
```css
|
||||
Container {
|
||||
layout: grid;
|
||||
grid-size: 3 2; /* 3 columns, 2 rows */
|
||||
grid-gutter: 1 2; /* Vertical Horizontal gaps */
|
||||
grid-rows: 10 auto 1fr; /* Row sizes */
|
||||
grid-columns: 1fr 2fr; /* Column sizes */
|
||||
}
|
||||
|
||||
/* Grid item spanning */
|
||||
Widget {
|
||||
column-span: 2; /* Span 2 columns */
|
||||
row-span: 3; /* Span 3 rows */
|
||||
}
|
||||
```
|
||||
|
||||
### Alignment
|
||||
|
||||
```css
|
||||
Container {
|
||||
align: center middle; /* Horizontal Vertical */
|
||||
align-horizontal: left; /* left, center, right */
|
||||
align-vertical: top; /* top, middle, bottom */
|
||||
}
|
||||
|
||||
/* Content alignment */
|
||||
Container {
|
||||
content-align: center middle;
|
||||
content-align-horizontal: right;
|
||||
content-align-vertical: bottom;
|
||||
}
|
||||
```
|
||||
|
||||
### Scrollbars
|
||||
|
||||
```css
|
||||
Widget {
|
||||
overflow: auto; /* Show scrollbars when needed */
|
||||
overflow: scroll; /* Always show scrollbars */
|
||||
overflow: hidden; /* No scrollbars */
|
||||
}
|
||||
|
||||
/* Individual axes */
|
||||
Widget {
|
||||
overflow-x: auto;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
Widget {
|
||||
scrollbar-background: $panel;
|
||||
scrollbar-color: $primary;
|
||||
scrollbar-color-hover: $accent;
|
||||
scrollbar-color-active: $boost;
|
||||
}
|
||||
```
|
||||
|
||||
## Effects
|
||||
|
||||
### Transitions
|
||||
|
||||
Animate property changes:
|
||||
```css
|
||||
Button {
|
||||
background: blue;
|
||||
transition: background 300ms;
|
||||
}
|
||||
|
||||
Button:hover {
|
||||
background: lightblue; /* Animates over 300ms */
|
||||
}
|
||||
|
||||
/* Multiple properties */
|
||||
Widget {
|
||||
transition: background 200ms, border 150ms;
|
||||
}
|
||||
```
|
||||
|
||||
### Offset
|
||||
|
||||
Position adjustment:
|
||||
```css
|
||||
Widget {
|
||||
offset: 1 2; /* X Y offset */
|
||||
offset-x: 1;
|
||||
offset-y: 2;
|
||||
}
|
||||
```
|
||||
|
||||
### Layer
|
||||
|
||||
Z-index equivalent:
|
||||
```css
|
||||
Widget {
|
||||
layer: above; /* Higher layer */
|
||||
layer: below; /* Lower layer */
|
||||
}
|
||||
```
|
||||
|
||||
## Docking
|
||||
|
||||
Pin widgets to edges:
|
||||
```css
|
||||
#header {
|
||||
dock: top;
|
||||
height: 3;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
dock: left;
|
||||
width: 30;
|
||||
}
|
||||
|
||||
#footer {
|
||||
dock: bottom;
|
||||
height: 3;
|
||||
}
|
||||
```
|
||||
|
||||
## Theme Variables
|
||||
|
||||
Define reusable values:
|
||||
```css
|
||||
/* Define variables */
|
||||
Screen {
|
||||
--card-bg: #1e3a8a;
|
||||
--card-border: white;
|
||||
--card-padding: 1 2;
|
||||
}
|
||||
|
||||
/* Use variables */
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border: solid var(--card-border);
|
||||
padding: var(--card-padding);
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Theme Example
|
||||
|
||||
```css
|
||||
/* app.tcss */
|
||||
|
||||
/* Theme colors */
|
||||
Screen {
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Headers */
|
||||
Header {
|
||||
background: #1e293b;
|
||||
color: #60a5fa;
|
||||
dock: top;
|
||||
height: 3;
|
||||
}
|
||||
|
||||
Footer {
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
dock: bottom;
|
||||
height: 1;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
Button {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
margin: 0 1;
|
||||
padding: 0 2;
|
||||
min-width: 16;
|
||||
transition: background 200ms;
|
||||
}
|
||||
|
||||
Button:hover {
|
||||
background: #60a5fa;
|
||||
}
|
||||
|
||||
Button:focus {
|
||||
border: solid #93c5fd;
|
||||
}
|
||||
|
||||
Button.-primary {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
Button.-primary:hover {
|
||||
background: #34d399;
|
||||
}
|
||||
|
||||
Button.-error {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
Button.-error:hover {
|
||||
background: #f87171;
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
Input {
|
||||
border: solid #475569;
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
padding: 0 1;
|
||||
}
|
||||
|
||||
Input:focus {
|
||||
border: solid #3b82f6;
|
||||
}
|
||||
|
||||
/* Containers */
|
||||
.card {
|
||||
background: #1e293b;
|
||||
border: solid #334155;
|
||||
padding: 1 2;
|
||||
margin: 1;
|
||||
}
|
||||
|
||||
.card > .title {
|
||||
text-style: bold;
|
||||
color: #60a5fa;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
/* Data tables */
|
||||
DataTable {
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
DataTable > .datatable--header {
|
||||
background: #334155;
|
||||
color: #60a5fa;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
DataTable > .datatable--cursor {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
/* Scrollbars */
|
||||
*::-webkit-scrollbar {
|
||||
scrollbar-background: #1e293b;
|
||||
scrollbar-color: #475569;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar:hover {
|
||||
scrollbar-color: #64748b;
|
||||
}
|
||||
```
|
||||
|
||||
## Dark/Light Themes
|
||||
|
||||
Support theme switching:
|
||||
```python
|
||||
class MyApp(App):
|
||||
ENABLE_DARK_MODE = True
|
||||
|
||||
CSS = """
|
||||
/* Dark theme (default) */
|
||||
Screen {
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Light theme */
|
||||
Screen.light {
|
||||
background: #f8fafc;
|
||||
color: #1e293b;
|
||||
}
|
||||
"""
|
||||
|
||||
def action_toggle_theme(self) -> None:
|
||||
self.dark = not self.dark
|
||||
```
|
||||
|
||||
## Responsive Styles
|
||||
|
||||
Conditional styles based on size:
|
||||
```css
|
||||
/* Default (small screens) */
|
||||
#sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Medium screens */
|
||||
Screen:width-gt-80 #sidebar {
|
||||
width: 30;
|
||||
}
|
||||
|
||||
/* Large screens */
|
||||
Screen:width-gt-120 #sidebar {
|
||||
width: 40;
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use semantic colors** - Prefer `$primary` over hardcoded values
|
||||
2. **Organize CSS** - Group related styles together
|
||||
3. **Use classes** - Reusable styles via classes, not IDs
|
||||
4. **Minimize specificity** - Avoid overly specific selectors
|
||||
5. **Use transitions** - Smooth state changes
|
||||
6. **Test both themes** - Ensure dark/light compatibility
|
||||
7. **Keep CSS DRY** - Use variables for repeated values
|
||||
8. **Document custom variables** - Comment non-obvious choices
|
||||
|
||||
## Debugging Styles
|
||||
|
||||
View computed styles:
|
||||
```python
|
||||
def on_mount(self) -> None:
|
||||
widget = self.query_one("#my-widget")
|
||||
self.log(widget.styles) # Log all computed styles
|
||||
```
|
||||
|
||||
Use Textual devtools:
|
||||
```bash
|
||||
textual run --dev app.py
|
||||
# Press F1 to view CSS inspector
|
||||
```
|
||||
|
||||
Temporary debugging borders:
|
||||
```css
|
||||
* {
|
||||
border: solid red; /* See all widget boundaries */
|
||||
}
|
||||
```
|
||||
533
skill/references/widgets.md
Normal file
533
skill/references/widgets.md
Normal file
@@ -0,0 +1,533 @@
|
||||
# Textual Widget Gallery
|
||||
|
||||
Comprehensive examples of all built-in Textual widgets.
|
||||
|
||||
## Basic Widgets
|
||||
|
||||
### Label
|
||||
|
||||
Display static or dynamic text:
|
||||
```python
|
||||
from textual.widgets import Label
|
||||
|
||||
# Simple label
|
||||
yield Label("Hello World")
|
||||
|
||||
# With styling
|
||||
yield Label("Important!", classes="highlight")
|
||||
|
||||
# With markup
|
||||
yield Label("[bold]Bold[/] and [italic]italic[/]")
|
||||
|
||||
# Dynamic label with reactive
|
||||
class DynamicLabel(Widget):
|
||||
message = reactive("Initial")
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label(self.message)
|
||||
```
|
||||
|
||||
### Static
|
||||
|
||||
Display Rich renderables:
|
||||
```python
|
||||
from textual.widgets import Static
|
||||
from rich.table import Table
|
||||
|
||||
table = Table()
|
||||
table.add_column("Name")
|
||||
table.add_column("Value")
|
||||
table.add_row("Alpha", "100")
|
||||
|
||||
yield Static(table)
|
||||
```
|
||||
|
||||
### Button
|
||||
|
||||
Interactive buttons with variants:
|
||||
```python
|
||||
from textual.widgets import Button
|
||||
|
||||
# Standard button
|
||||
yield Button("Click me", id="action")
|
||||
|
||||
# Button variants
|
||||
yield Button("Primary", variant="primary")
|
||||
yield Button("Success", variant="success")
|
||||
yield Button("Warning", variant="warning")
|
||||
yield Button("Error", variant="error")
|
||||
|
||||
# Disabled button
|
||||
yield Button("Disabled", disabled=True)
|
||||
|
||||
# Handle click
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "action":
|
||||
self.action_perform()
|
||||
```
|
||||
|
||||
## Input Widgets
|
||||
|
||||
### Input
|
||||
|
||||
Single-line text input:
|
||||
```python
|
||||
from textual.widgets import Input
|
||||
|
||||
# Basic input
|
||||
yield Input(placeholder="Enter text...", id="name")
|
||||
|
||||
# Password input
|
||||
yield Input(placeholder="Password", password=True, id="pass")
|
||||
|
||||
# Validated input
|
||||
yield Input(
|
||||
placeholder="Email",
|
||||
validators=[Email()], # Built-in validators
|
||||
id="email"
|
||||
)
|
||||
|
||||
# Handle submission
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
value = event.value
|
||||
self.log(f"Submitted: {value}")
|
||||
|
||||
# Handle changes
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
self.validate_live(event.value)
|
||||
```
|
||||
|
||||
### TextArea
|
||||
|
||||
Multi-line text editor:
|
||||
```python
|
||||
from textual.widgets import TextArea
|
||||
|
||||
# Basic text area
|
||||
yield TextArea(id="editor")
|
||||
|
||||
# With initial content
|
||||
yield TextArea(
|
||||
text="Initial content\nLine 2",
|
||||
language="python", # Syntax highlighting
|
||||
theme="monokai",
|
||||
id="code"
|
||||
)
|
||||
|
||||
# Handle changes
|
||||
def on_text_area_changed(self, event: TextArea.Changed) -> None:
|
||||
content = event.text_area.text
|
||||
```
|
||||
|
||||
### Select
|
||||
|
||||
Dropdown selection:
|
||||
```python
|
||||
from textual.widgets import Select
|
||||
|
||||
# Basic select
|
||||
options = [
|
||||
("Option A", "a"),
|
||||
("Option B", "b"),
|
||||
("Option C", "c"),
|
||||
]
|
||||
yield Select(options=options, prompt="Choose...", id="choice")
|
||||
|
||||
# Handle selection
|
||||
def on_select_changed(self, event: Select.Changed) -> None:
|
||||
value = event.value # "a", "b", or "c"
|
||||
self.log(f"Selected: {value}")
|
||||
```
|
||||
|
||||
### Checkbox
|
||||
|
||||
Boolean input:
|
||||
```python
|
||||
from textual.widgets import Checkbox
|
||||
|
||||
yield Checkbox("Enable feature", id="feature")
|
||||
|
||||
# Handle changes
|
||||
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
|
||||
is_checked = event.value
|
||||
self.toggle_feature(is_checked)
|
||||
```
|
||||
|
||||
### RadioButton and RadioSet
|
||||
|
||||
Mutually exclusive options:
|
||||
```python
|
||||
from textual.widgets import RadioButton, RadioSet
|
||||
|
||||
with RadioSet(id="size"):
|
||||
yield RadioButton("Small")
|
||||
yield RadioButton("Medium", value=True) # Default
|
||||
yield RadioButton("Large")
|
||||
|
||||
# Handle selection
|
||||
def on_radio_set_changed(self, event: RadioSet.Changed) -> None:
|
||||
selected = event.pressed.label
|
||||
self.log(f"Size: {selected}")
|
||||
```
|
||||
|
||||
### Switch
|
||||
|
||||
Toggle switch:
|
||||
```python
|
||||
from textual.widgets import Switch
|
||||
|
||||
yield Switch(value=True, id="notifications")
|
||||
|
||||
# Handle toggle
|
||||
def on_switch_changed(self, event: Switch.Changed) -> None:
|
||||
is_on = event.value
|
||||
self.toggle_notifications(is_on)
|
||||
```
|
||||
|
||||
## Data Display Widgets
|
||||
|
||||
### DataTable
|
||||
|
||||
Tabular data with selection and sorting:
|
||||
```python
|
||||
from textual.widgets import DataTable
|
||||
|
||||
table = DataTable(id="users")
|
||||
|
||||
# Add columns
|
||||
table.add_columns("Name", "Age", "City")
|
||||
|
||||
# Add rows (returns row key)
|
||||
row_key = table.add_row("Alice", 30, "NYC")
|
||||
table.add_row("Bob", 25, "LA")
|
||||
|
||||
# Cursor control
|
||||
table.cursor_type = "row" # or "cell", "column", "none"
|
||||
|
||||
# Handle selection
|
||||
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||
row_key = event.row_key
|
||||
row_data = event.row
|
||||
self.log(f"Selected: {row_data}")
|
||||
|
||||
# Update cells
|
||||
table.update_cell(row_key, "Age", 31)
|
||||
|
||||
# Remove rows
|
||||
table.remove_row(row_key)
|
||||
|
||||
# Sort
|
||||
table.sort("Age", reverse=True)
|
||||
```
|
||||
|
||||
### Tree
|
||||
|
||||
Hierarchical data:
|
||||
```python
|
||||
from textual.widgets import Tree
|
||||
|
||||
tree = Tree("Root", id="file-tree")
|
||||
|
||||
# Add nodes
|
||||
root = tree.root
|
||||
folder = root.add("Folder", expand=True)
|
||||
folder.add_leaf("file1.txt")
|
||||
folder.add_leaf("file2.txt")
|
||||
|
||||
subfolder = folder.add("Subfolder")
|
||||
subfolder.add_leaf("nested.txt")
|
||||
|
||||
# Handle selection
|
||||
def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
|
||||
node = event.node
|
||||
self.log(f"Selected: {node.label}")
|
||||
|
||||
# Expand/collapse
|
||||
def on_tree_node_expanded(self, event: Tree.NodeExpanded) -> None:
|
||||
# Load children dynamically
|
||||
node = event.node
|
||||
self.load_children(node)
|
||||
```
|
||||
|
||||
### ListView
|
||||
|
||||
List with selection:
|
||||
```python
|
||||
from textual.widgets import ListView, ListItem, Label
|
||||
|
||||
list_view = ListView(id="menu")
|
||||
|
||||
# Add items
|
||||
list_view.append(ListItem(Label("Item 1")))
|
||||
list_view.append(ListItem(Label("Item 2")))
|
||||
list_view.append(ListItem(Label("Item 3")))
|
||||
|
||||
# Handle selection
|
||||
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
||||
item = event.item
|
||||
self.log(f"Selected: {item}")
|
||||
```
|
||||
|
||||
### Log / RichLog
|
||||
|
||||
Scrollable log output:
|
||||
```python
|
||||
from textual.widgets import Log, RichLog
|
||||
|
||||
# Simple log
|
||||
log = Log(id="output", auto_scroll=True)
|
||||
log.write_line("Log entry")
|
||||
log.write_lines(["Line 1", "Line 2"])
|
||||
|
||||
# Rich log with markup
|
||||
rich_log = RichLog(id="rich-output", highlight=True)
|
||||
rich_log.write("[bold green]Success![/]")
|
||||
rich_log.write("[red]Error occurred[/]")
|
||||
|
||||
# Clear log
|
||||
log.clear()
|
||||
```
|
||||
|
||||
### ProgressBar
|
||||
|
||||
Progress indicator:
|
||||
```python
|
||||
from textual.widgets import ProgressBar
|
||||
|
||||
# Determinate progress
|
||||
progress = ProgressBar(total=100, id="progress")
|
||||
progress.advance(25) # 25%
|
||||
progress.update(progress=50) # 50%
|
||||
|
||||
# Indeterminate progress
|
||||
progress = ProgressBar(total=None) # Animated spinner
|
||||
```
|
||||
|
||||
### Sparkline
|
||||
|
||||
Inline data visualization:
|
||||
```python
|
||||
from textual.widgets import Sparkline
|
||||
|
||||
data = [1, 2, 3, 5, 8, 13, 21]
|
||||
yield Sparkline(data, id="chart")
|
||||
|
||||
# Update data
|
||||
sparkline = self.query_one(Sparkline)
|
||||
sparkline.data = new_data
|
||||
```
|
||||
|
||||
## Navigation Widgets
|
||||
|
||||
### Header / Footer
|
||||
|
||||
Standard app chrome:
|
||||
```python
|
||||
from textual.widgets import Header, Footer
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
# ... content ...
|
||||
yield Footer()
|
||||
```
|
||||
|
||||
### TabbedContent / TabPane
|
||||
|
||||
Tabbed interface:
|
||||
```python
|
||||
from textual.widgets import TabbedContent, TabPane
|
||||
|
||||
with TabbedContent(id="tabs"):
|
||||
with TabPane("Tab 1", id="tab1"):
|
||||
yield Label("Content 1")
|
||||
with TabPane("Tab 2", id="tab2"):
|
||||
yield Label("Content 2")
|
||||
with TabPane("Tab 3", id="tab3"):
|
||||
yield Label("Content 3")
|
||||
|
||||
# Handle tab changes
|
||||
def on_tabbed_content_tab_activated(self, event: TabbedContent.TabActivated) -> None:
|
||||
tab_id = event.pane.id
|
||||
self.log(f"Switched to {tab_id}")
|
||||
```
|
||||
|
||||
### ContentSwitcher
|
||||
|
||||
Programmatically switch content:
|
||||
```python
|
||||
from textual.widgets import ContentSwitcher
|
||||
|
||||
with ContentSwitcher(initial="view1", id="switcher"):
|
||||
yield Label("View 1", id="view1")
|
||||
yield Label("View 2", id="view2")
|
||||
yield Label("View 3", id="view3")
|
||||
|
||||
# Switch views
|
||||
def switch_view(self, view_id: str) -> None:
|
||||
switcher = self.query_one(ContentSwitcher)
|
||||
switcher.current = view_id
|
||||
```
|
||||
|
||||
### OptionList
|
||||
|
||||
Selectable list of options:
|
||||
```python
|
||||
from textual.widgets import OptionList
|
||||
from textual.widgets.option_list import Option
|
||||
|
||||
option_list = OptionList(
|
||||
Option("Option 1", id="opt1"),
|
||||
Option("Option 2", id="opt2"),
|
||||
Option("Option 3", id="opt3"),
|
||||
)
|
||||
|
||||
# Handle selection
|
||||
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
||||
option_id = event.option.id
|
||||
self.log(f"Selected: {option_id}")
|
||||
```
|
||||
|
||||
## Loading Widgets
|
||||
|
||||
### LoadingIndicator
|
||||
|
||||
Spinning loader:
|
||||
```python
|
||||
from textual.widgets import LoadingIndicator
|
||||
|
||||
# Show while loading
|
||||
with LoadingIndicator():
|
||||
# Content loading...
|
||||
pass
|
||||
|
||||
# Or standalone
|
||||
yield LoadingIndicator(id="loader")
|
||||
```
|
||||
|
||||
### Placeholder
|
||||
|
||||
Development placeholder:
|
||||
```python
|
||||
from textual.widgets import Placeholder
|
||||
|
||||
# Quick placeholder during development
|
||||
yield Placeholder("Chart goes here")
|
||||
yield Placeholder(label="[b]User Profile[/]", variant="text")
|
||||
```
|
||||
|
||||
## Special Widgets
|
||||
|
||||
### Markdown
|
||||
|
||||
Render markdown:
|
||||
```python
|
||||
from textual.widgets import Markdown
|
||||
|
||||
markdown_content = """
|
||||
# Title
|
||||
This is **bold** and *italic*.
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
|
||||
```python
|
||||
code block
|
||||
```
|
||||
"""
|
||||
|
||||
yield Markdown(markdown_content, id="docs")
|
||||
```
|
||||
|
||||
### DirectoryTree
|
||||
|
||||
File system browser:
|
||||
```python
|
||||
from textual.widgets import DirectoryTree
|
||||
|
||||
tree = DirectoryTree("/home/user", id="files")
|
||||
|
||||
# Handle file selection
|
||||
def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected) -> None:
|
||||
file_path = event.path
|
||||
self.open_file(file_path)
|
||||
```
|
||||
|
||||
## Custom Widget Example
|
||||
|
||||
Create composite widgets:
|
||||
```python
|
||||
from textual.widget import Widget
|
||||
from textual.containers import Horizontal, Vertical
|
||||
|
||||
class UserCard(Widget):
|
||||
"""Display user information."""
|
||||
|
||||
def __init__(self, name: str, email: str, role: str) -> None:
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.email = email
|
||||
self.role = role
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(classes="card"):
|
||||
yield Label(self.name, classes="name")
|
||||
yield Label(self.email, classes="email")
|
||||
with Horizontal():
|
||||
yield Label(f"Role: {self.role}", classes="role")
|
||||
yield Button("Edit", variant="primary")
|
||||
|
||||
DEFAULT_CSS = """
|
||||
UserCard {
|
||||
border: solid $primary;
|
||||
padding: 1;
|
||||
margin: 1;
|
||||
}
|
||||
|
||||
UserCard .name {
|
||||
text-style: bold;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
UserCard .email {
|
||||
color: $text-muted;
|
||||
}
|
||||
"""
|
||||
|
||||
# Use the widget
|
||||
yield UserCard("Alice Smith", "alice@example.com", "Admin")
|
||||
```
|
||||
|
||||
## Widget Composition Patterns
|
||||
|
||||
### Form Layout
|
||||
```python
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="form"):
|
||||
yield Label("Registration Form")
|
||||
yield Input(placeholder="Name", id="name")
|
||||
yield Input(placeholder="Email", id="email")
|
||||
yield Input(placeholder="Password", password=True, id="pass")
|
||||
with Horizontal():
|
||||
yield Button("Submit", variant="primary")
|
||||
yield Button("Cancel")
|
||||
```
|
||||
|
||||
### Dashboard Layout
|
||||
```python
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
with Container(id="dashboard"):
|
||||
with Horizontal(classes="stats"):
|
||||
yield Static("[b]Users:[/] 1,234", classes="stat")
|
||||
yield Static("[b]Active:[/] 567", classes="stat")
|
||||
yield Static("[b]Revenue:[/] $12K", classes="stat")
|
||||
with Horizontal(classes="content"):
|
||||
with Vertical(id="sidebar"):
|
||||
yield Label("Menu")
|
||||
yield Button("Dashboard")
|
||||
yield Button("Users")
|
||||
yield Button("Settings")
|
||||
with Container(id="main"):
|
||||
yield DataTable()
|
||||
yield Footer()
|
||||
```
|
||||
Reference in New Issue
Block a user