Initial commit
This commit is contained in:
182
skills/textual-builder/references/basics.md
Normal file
182
skills/textual-builder/references/basics.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Textual Basics
|
||||
|
||||
## App Structure
|
||||
|
||||
Every Textual app follows this pattern:
|
||||
|
||||
```python
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Widget
|
||||
|
||||
class MyApp(App):
|
||||
"""Docstring describing the app."""
|
||||
|
||||
# Optional: Link to external CSS file
|
||||
CSS_PATH = "app.tcss"
|
||||
|
||||
# Optional: Inline CSS
|
||||
CSS = """
|
||||
Screen {
|
||||
align: center middle;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Create child widgets."""
|
||||
yield Widget()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Called when app is mounted and ready."""
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = MyApp()
|
||||
app.run()
|
||||
```
|
||||
|
||||
## Compose Method
|
||||
|
||||
The `compose()` method yields widgets to add to the app. It's called once during initialization:
|
||||
|
||||
```python
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield ContentWidget()
|
||||
yield Footer()
|
||||
```
|
||||
|
||||
## Mounting
|
||||
|
||||
- `on_mount()`: Called when the app/widget is fully mounted and ready
|
||||
- `mount()`: Dynamically add widgets after app starts (returns a coroutine)
|
||||
|
||||
```python
|
||||
async def on_key(self) -> None:
|
||||
# Must await when modifying mounted widgets
|
||||
await self.mount(NewWidget())
|
||||
self.query_one(Button).label = "Modified!"
|
||||
```
|
||||
|
||||
## Reactive Attributes
|
||||
|
||||
Reactive attributes automatically update the UI when changed:
|
||||
|
||||
```python
|
||||
from textual.reactive import reactive
|
||||
|
||||
class Counter(Widget):
|
||||
count = reactive(0) # Initial value
|
||||
|
||||
def watch_count(self, new_value: int) -> None:
|
||||
"""Called automatically when count changes."""
|
||||
self.query_one(Label).update(f"Count: {new_value}")
|
||||
|
||||
def increment(self) -> None:
|
||||
self.count += 1 # Triggers watch_count
|
||||
```
|
||||
|
||||
### Reactive with Bindings
|
||||
|
||||
Set `bindings=True` to auto-refresh footer bindings when reactive changes:
|
||||
|
||||
```python
|
||||
class MyApp(App):
|
||||
page = reactive(0, bindings=True)
|
||||
|
||||
def check_action(self, action: str, parameters) -> bool | None:
|
||||
"""Return None to disable action."""
|
||||
if action == "next" and self.page == MAX_PAGES:
|
||||
return None # Dims the key in footer
|
||||
return True
|
||||
```
|
||||
|
||||
## Querying Widgets
|
||||
|
||||
Find widgets in the DOM:
|
||||
|
||||
```python
|
||||
# Get one widget (raises if not found)
|
||||
button = self.query_one(Button)
|
||||
button = self.query_one("#my-id")
|
||||
|
||||
# Get multiple widgets
|
||||
all_buttons = self.query(Button)
|
||||
for button in all_buttons:
|
||||
pass
|
||||
|
||||
# Get with CSS selector
|
||||
widget = self.query_one("#container .special-class")
|
||||
```
|
||||
|
||||
## Messages and Events
|
||||
|
||||
### Built-in Events
|
||||
|
||||
Handle with `on_<event>` methods:
|
||||
|
||||
```python
|
||||
def on_mount(self) -> None:
|
||||
"""When mounted."""
|
||||
pass
|
||||
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
"""Key pressed."""
|
||||
if event.key == "escape":
|
||||
self.exit()
|
||||
```
|
||||
|
||||
### Widget Messages
|
||||
|
||||
Handle messages from child widgets:
|
||||
|
||||
```python
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Button was clicked."""
|
||||
self.notify(f"Button {event.button.id} clicked!")
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
"""Input text changed."""
|
||||
self.value = event.value
|
||||
```
|
||||
|
||||
### Custom Messages
|
||||
|
||||
Define custom messages in your widgets:
|
||||
|
||||
```python
|
||||
from textual.message import Message
|
||||
|
||||
class MyWidget(Widget):
|
||||
class ValueChanged(Message):
|
||||
"""Posted when value changes."""
|
||||
def __init__(self, value: int) -> None:
|
||||
super().__init__()
|
||||
self.value = value
|
||||
|
||||
def update_value(self, new_value: int) -> None:
|
||||
self.value = new_value
|
||||
self.post_message(self.ValueChanged(new_value))
|
||||
|
||||
# Handle in parent
|
||||
def on_my_widget_value_changed(self, event: MyWidget.ValueChanged) -> None:
|
||||
self.notify(f"New value: {event.value}")
|
||||
```
|
||||
|
||||
## Preventing Message Propagation
|
||||
|
||||
Stop messages from bubbling to parent:
|
||||
|
||||
```python
|
||||
def on_switch_changed(self, event: Switch.Changed) -> None:
|
||||
event.stop() # Don't propagate to parent
|
||||
# Handle here
|
||||
```
|
||||
|
||||
## Preventing Reactive Watchers
|
||||
|
||||
Temporarily prevent reactive watchers from firing:
|
||||
|
||||
```python
|
||||
with self.prevent(MyWidget.ValueChanged):
|
||||
self.value = new_value # Won't trigger watch_value or post message
|
||||
```
|
||||
346
skills/textual-builder/references/interactivity.md
Normal file
346
skills/textual-builder/references/interactivity.md
Normal file
@@ -0,0 +1,346 @@
|
||||
# Interactivity: Events, Bindings, and Actions
|
||||
|
||||
## Key Bindings
|
||||
|
||||
Define keyboard shortcuts:
|
||||
|
||||
```python
|
||||
from textual.app import App
|
||||
from textual.binding import Binding
|
||||
|
||||
class MyApp(App):
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quit"), # key, action, description
|
||||
("s", "save", "Save"),
|
||||
("ctrl+c", "copy", "Copy"),
|
||||
Binding("f1", "help", "Help", show=True, priority=True),
|
||||
]
|
||||
|
||||
def action_save(self) -> None:
|
||||
"""Actions are methods prefixed with 'action_'."""
|
||||
self.notify("Saved!")
|
||||
|
||||
def action_copy(self) -> None:
|
||||
self.notify("Copied!")
|
||||
|
||||
def action_help(self) -> None:
|
||||
self.notify("Help content...")
|
||||
```
|
||||
|
||||
### Binding Options
|
||||
|
||||
```python
|
||||
Binding(
|
||||
key="f1",
|
||||
action="help",
|
||||
description="Help",
|
||||
show=True, # Show in footer (default: True)
|
||||
priority=True, # Prioritize over widget bindings
|
||||
)
|
||||
```
|
||||
|
||||
### Dynamic Bindings
|
||||
|
||||
Refresh bindings when state changes:
|
||||
|
||||
```python
|
||||
class MyApp(App):
|
||||
page = reactive(0, bindings=True) # Auto-refresh bindings
|
||||
|
||||
def check_action(self, action: str, parameters) -> bool | None:
|
||||
"""Control action availability."""
|
||||
if action == "next" and self.page >= MAX_PAGES:
|
||||
return None # Disables and dims the key
|
||||
if action == "previous" and self.page == 0:
|
||||
return None
|
||||
return True # Enabled
|
||||
```
|
||||
|
||||
Or manually refresh:
|
||||
|
||||
```python
|
||||
def update_state(self):
|
||||
self.state = "new_state"
|
||||
self.refresh_bindings() # Update footer
|
||||
```
|
||||
|
||||
## Mouse Events
|
||||
|
||||
Handle mouse interactions:
|
||||
|
||||
```python
|
||||
from textual import events
|
||||
|
||||
class MyWidget(Widget):
|
||||
def on_click(self, event: events.Click) -> None:
|
||||
"""Widget was clicked."""
|
||||
self.notify(f"Clicked at {event.x}, {event.y}")
|
||||
|
||||
def on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
"""Mouse moved over widget."""
|
||||
pass
|
||||
|
||||
def on_enter(self, event: events.Enter) -> None:
|
||||
"""Mouse entered widget."""
|
||||
self.add_class("hover")
|
||||
|
||||
def on_leave(self, event: events.Leave) -> None:
|
||||
"""Mouse left widget."""
|
||||
self.remove_class("hover")
|
||||
```
|
||||
|
||||
## Keyboard Events
|
||||
|
||||
Handle key presses:
|
||||
|
||||
```python
|
||||
from textual import events
|
||||
|
||||
class MyApp(App):
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
"""Any key pressed."""
|
||||
if event.key == "escape":
|
||||
self.exit()
|
||||
elif event.key == "space":
|
||||
self.toggle_pause()
|
||||
|
||||
def key_r(self, event: events.Key) -> None:
|
||||
"""Specific key handler (press 'r')."""
|
||||
self.reset()
|
||||
```
|
||||
|
||||
## Focus Events
|
||||
|
||||
Track focus changes:
|
||||
|
||||
```python
|
||||
def on_focus(self, event: events.Focus) -> None:
|
||||
"""Widget gained focus."""
|
||||
self.border_title = "Focused"
|
||||
|
||||
def on_blur(self, event: events.Blur) -> None:
|
||||
"""Widget lost focus."""
|
||||
self.border_title = ""
|
||||
```
|
||||
|
||||
Programmatic focus:
|
||||
|
||||
```python
|
||||
widget.focus() # Give focus to widget
|
||||
widget.can_focus = True # Enable focusing (default for inputs)
|
||||
```
|
||||
|
||||
## Widget Messages
|
||||
|
||||
Handle messages from specific widgets:
|
||||
|
||||
```python
|
||||
from textual.widgets import Button, Input, Switch
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Any button pressed."""
|
||||
button_id = event.button.id
|
||||
self.notify(f"Button {button_id} pressed")
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
"""Input text changed."""
|
||||
self.update_preview(event.value)
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
"""User pressed Enter in input."""
|
||||
self.process(event.value)
|
||||
|
||||
def on_switch_changed(self, event: Switch.Changed) -> None:
|
||||
"""Switch toggled."""
|
||||
self.feature_enabled = event.value
|
||||
|
||||
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||
"""Row in table selected."""
|
||||
row_key = event.row_key
|
||||
```
|
||||
|
||||
### Message Naming Convention
|
||||
|
||||
Handler method: `on_{widget_type}_{message_name}`
|
||||
- Converts to snake_case
|
||||
- Example: `Button.Pressed` → `on_button_pressed`
|
||||
- Custom widget: `MyWidget.ValueChanged` → `on_my_widget_value_changed`
|
||||
|
||||
## Custom Messages
|
||||
|
||||
Define custom messages for your widgets:
|
||||
|
||||
```python
|
||||
from textual.message import Message
|
||||
from textual.widget import Widget
|
||||
|
||||
class Card(Widget):
|
||||
class Selected(Message):
|
||||
"""Posted when card is selected."""
|
||||
def __init__(self, card_id: str, value: int) -> None:
|
||||
super().__init__()
|
||||
self.card_id = card_id
|
||||
self.value = value
|
||||
|
||||
def on_click(self) -> None:
|
||||
self.post_message(self.Selected(self.id, self.value))
|
||||
|
||||
# Handle in parent
|
||||
def on_card_selected(self, event: Card.Selected) -> None:
|
||||
self.notify(f"Card {event.card_id} (value: {event.value}) selected")
|
||||
```
|
||||
|
||||
## Message Control
|
||||
|
||||
### Stop Propagation
|
||||
|
||||
Prevent message from bubbling to parent:
|
||||
|
||||
```python
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
event.stop() # Don't propagate to parent
|
||||
# Handle locally
|
||||
```
|
||||
|
||||
### Prevent Messages
|
||||
|
||||
Temporarily suppress messages:
|
||||
|
||||
```python
|
||||
with widget.prevent(Switch.Changed):
|
||||
widget.value = True # Won't emit Changed message
|
||||
```
|
||||
|
||||
Useful when programmatically updating to avoid infinite loops.
|
||||
|
||||
## Actions
|
||||
|
||||
Actions are methods that can be triggered by bindings or programmatically:
|
||||
|
||||
```python
|
||||
class MyApp(App):
|
||||
BINDINGS = [
|
||||
("n", "next_page", "Next"),
|
||||
("p", "prev_page", "Previous"),
|
||||
]
|
||||
|
||||
def action_next_page(self) -> None:
|
||||
self.page += 1
|
||||
self.refresh_view()
|
||||
|
||||
def action_prev_page(self) -> None:
|
||||
self.page -= 1
|
||||
self.refresh_view()
|
||||
```
|
||||
|
||||
### Parameterized Actions
|
||||
|
||||
Pass parameters to actions:
|
||||
|
||||
```python
|
||||
BINDINGS = [
|
||||
("r", "add_color('red')", "Red"),
|
||||
("g", "add_color('green')", "Green"),
|
||||
("b", "add_color('blue')", "Blue"),
|
||||
]
|
||||
|
||||
def action_add_color(self, color: str) -> None:
|
||||
self.add_widget(ColorBar(color))
|
||||
```
|
||||
|
||||
### Programmatic Action Calls
|
||||
|
||||
```python
|
||||
self.run_action("save") # Trigger action by name
|
||||
```
|
||||
|
||||
## Notifications
|
||||
|
||||
Show temporary messages to user:
|
||||
|
||||
```python
|
||||
self.notify("File saved successfully!")
|
||||
self.notify("Error occurred", severity="error")
|
||||
self.notify("Warning!", severity="warning")
|
||||
self.notify("Info message", severity="information", timeout=5)
|
||||
```
|
||||
|
||||
## Timers
|
||||
|
||||
Schedule repeated actions:
|
||||
|
||||
```python
|
||||
def on_mount(self) -> None:
|
||||
self.set_interval(1.0, self.update_timer) # Every 1 second
|
||||
|
||||
def update_timer(self) -> None:
|
||||
self.elapsed += 1
|
||||
self.query_one("#timer").update(str(self.elapsed))
|
||||
```
|
||||
|
||||
One-time delayed action:
|
||||
|
||||
```python
|
||||
self.set_timer(2.0, self.delayed_action) # After 2 seconds
|
||||
|
||||
def delayed_action(self) -> None:
|
||||
self.notify("Timer complete!")
|
||||
```
|
||||
|
||||
## Example: Interactive Card Selection
|
||||
|
||||
```python
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Horizontal
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Label, Static
|
||||
from textual.message import Message
|
||||
|
||||
class Card(Widget):
|
||||
DEFAULT_CSS = """
|
||||
Card {
|
||||
width: 12;
|
||||
height: 10;
|
||||
border: round white;
|
||||
background: $panel;
|
||||
}
|
||||
Card:hover {
|
||||
background: $boost;
|
||||
}
|
||||
Card.selected {
|
||||
border: double cyan;
|
||||
background: $accent;
|
||||
}
|
||||
"""
|
||||
|
||||
class Selected(Message):
|
||||
def __init__(self, card: "Card") -> None:
|
||||
super().__init__()
|
||||
self.card = card
|
||||
|
||||
def __init__(self, suit: str, value: str) -> None:
|
||||
super().__init__()
|
||||
self.suit = suit
|
||||
self.value = value
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label(f"{self.value}\n{self.suit}")
|
||||
|
||||
def on_click(self) -> None:
|
||||
self.post_message(self.Selected(self))
|
||||
|
||||
class CardGame(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
with Horizontal(id="hand"):
|
||||
yield Card("♠", "A")
|
||||
yield Card("♥", "K")
|
||||
yield Card("♣", "Q")
|
||||
|
||||
def on_card_selected(self, event: Card.Selected) -> None:
|
||||
# Deselect all
|
||||
for card in self.query(Card):
|
||||
card.remove_class("selected")
|
||||
# Select clicked
|
||||
event.card.add_class("selected")
|
||||
self.notify(f"Selected {event.card.value} of {event.card.suit}")
|
||||
```
|
||||
298
skills/textual-builder/references/layout.md
Normal file
298
skills/textual-builder/references/layout.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# Layout and Positioning
|
||||
|
||||
## Layout Types
|
||||
|
||||
### Vertical (Default)
|
||||
|
||||
Stacks widgets vertically:
|
||||
|
||||
```css
|
||||
Container {
|
||||
layout: vertical;
|
||||
}
|
||||
```
|
||||
|
||||
### Horizontal
|
||||
|
||||
Arranges widgets side-by-side:
|
||||
|
||||
```css
|
||||
Container {
|
||||
layout: horizontal;
|
||||
}
|
||||
```
|
||||
|
||||
### Grid
|
||||
|
||||
Grid layout with rows and columns:
|
||||
|
||||
```css
|
||||
Grid {
|
||||
layout: grid;
|
||||
grid-size: 3 2; /* 3 columns, 2 rows */
|
||||
grid-gutter: 1 2; /* vertical horizontal spacing */
|
||||
}
|
||||
```
|
||||
|
||||
#### Grid Cell Spanning
|
||||
|
||||
Make widgets span multiple cells:
|
||||
|
||||
```css
|
||||
#header {
|
||||
column-span: 3; /* Span 3 columns */
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
row-span: 2; /* Span 2 rows */
|
||||
}
|
||||
```
|
||||
|
||||
#### Grid Rows and Columns
|
||||
|
||||
Define row heights and column widths:
|
||||
|
||||
```css
|
||||
Grid {
|
||||
grid-size: 2 3;
|
||||
grid-rows: 1fr 6 25%; /* Flexible, fixed 6, 25% */
|
||||
grid-columns: 1fr 2fr; /* 1:2 ratio */
|
||||
}
|
||||
```
|
||||
|
||||
## Alignment
|
||||
|
||||
### Screen/Container Alignment
|
||||
|
||||
Center content within screen:
|
||||
|
||||
```css
|
||||
Screen {
|
||||
align: center middle; /* horizontal vertical */
|
||||
}
|
||||
```
|
||||
|
||||
Options: `left`, `center`, `right` × `top`, `middle`, `bottom`
|
||||
|
||||
### Content Alignment
|
||||
|
||||
Align content within a widget:
|
||||
|
||||
```css
|
||||
MyWidget {
|
||||
content-align: center middle;
|
||||
text-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
## Docking
|
||||
|
||||
Pin widgets to screen edges:
|
||||
|
||||
```css
|
||||
#header {
|
||||
dock: top;
|
||||
height: 3;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
dock: left;
|
||||
width: 20;
|
||||
}
|
||||
|
||||
#footer {
|
||||
dock: bottom;
|
||||
}
|
||||
```
|
||||
|
||||
Docking order matters - earlier docked widgets take priority.
|
||||
|
||||
## Sizing
|
||||
|
||||
### Fixed Sizes
|
||||
|
||||
```css
|
||||
Widget {
|
||||
width: 50; /* 50 cells */
|
||||
height: 10; /* 10 rows */
|
||||
}
|
||||
```
|
||||
|
||||
### Relative Sizes
|
||||
|
||||
```css
|
||||
Widget {
|
||||
width: 50%; /* 50% of parent */
|
||||
height: 100%;
|
||||
}
|
||||
```
|
||||
|
||||
### Fractional Units
|
||||
|
||||
Share available space proportionally:
|
||||
|
||||
```css
|
||||
#left {
|
||||
width: 1fr; /* Gets 1 part */
|
||||
}
|
||||
|
||||
#right {
|
||||
width: 2fr; /* Gets 2 parts (twice as wide) */
|
||||
}
|
||||
```
|
||||
|
||||
### Auto Sizing
|
||||
|
||||
Fit content:
|
||||
|
||||
```css
|
||||
Widget {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
```
|
||||
|
||||
### Min/Max Constraints
|
||||
|
||||
```css
|
||||
Widget {
|
||||
min-width: 20;
|
||||
max-width: 80;
|
||||
min-height: 5;
|
||||
max-height: 30;
|
||||
}
|
||||
```
|
||||
|
||||
## Spacing
|
||||
|
||||
### Margin
|
||||
|
||||
Space outside widget border:
|
||||
|
||||
```css
|
||||
Widget {
|
||||
margin: 1; /* All sides */
|
||||
margin: 1 2; /* vertical horizontal */
|
||||
margin: 1 2 3 4; /* top right bottom left */
|
||||
}
|
||||
```
|
||||
|
||||
### Padding
|
||||
|
||||
Space inside widget border:
|
||||
|
||||
```css
|
||||
Widget {
|
||||
padding: 1; /* All sides */
|
||||
padding: 1 2; /* vertical horizontal */
|
||||
}
|
||||
```
|
||||
|
||||
## Visibility
|
||||
|
||||
### Display
|
||||
|
||||
Show or hide widgets:
|
||||
|
||||
```css
|
||||
#hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#visible {
|
||||
display: block;
|
||||
}
|
||||
```
|
||||
|
||||
Toggle in Python:
|
||||
|
||||
```python
|
||||
widget.display = False # Hide
|
||||
widget.display = True # Show
|
||||
```
|
||||
|
||||
### Visibility
|
||||
|
||||
Similar to display but reserves space:
|
||||
|
||||
```css
|
||||
Widget {
|
||||
visibility: hidden; /* Hidden but takes space */
|
||||
visibility: visible;
|
||||
}
|
||||
```
|
||||
|
||||
## Layers
|
||||
|
||||
Control stacking order:
|
||||
|
||||
```css
|
||||
#background {
|
||||
layer: below;
|
||||
}
|
||||
|
||||
#popup {
|
||||
layer: above;
|
||||
}
|
||||
```
|
||||
|
||||
## Scrolling
|
||||
|
||||
### Enable Scrolling
|
||||
|
||||
```css
|
||||
Container {
|
||||
overflow-x: auto; /* Horizontal scrolling */
|
||||
overflow-y: auto; /* Vertical scrolling */
|
||||
overflow: auto auto; /* Both */
|
||||
}
|
||||
```
|
||||
|
||||
### Programmatic Scrolling
|
||||
|
||||
```python
|
||||
# Scroll to specific position
|
||||
container.scroll_to(x=0, y=100)
|
||||
|
||||
# Scroll widget into view
|
||||
widget.scroll_visible()
|
||||
|
||||
# Scroll to end
|
||||
self.screen.scroll_end(animate=True)
|
||||
```
|
||||
|
||||
## Example: Card Game Layout
|
||||
|
||||
```css
|
||||
Screen {
|
||||
layout: vertical;
|
||||
}
|
||||
|
||||
#opponent-hand {
|
||||
dock: top;
|
||||
height: 12;
|
||||
layout: horizontal;
|
||||
align: center top;
|
||||
}
|
||||
|
||||
#play-area {
|
||||
height: 1fr;
|
||||
layout: grid;
|
||||
grid-size: 5 3;
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
#player-hand {
|
||||
dock: bottom;
|
||||
height: 15;
|
||||
layout: horizontal;
|
||||
align: center bottom;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 12;
|
||||
height: 10;
|
||||
margin: 0 1;
|
||||
}
|
||||
```
|
||||
323
skills/textual-builder/references/styling.md
Normal file
323
skills/textual-builder/references/styling.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# Styling with CSS
|
||||
|
||||
## CSS Files
|
||||
|
||||
Link external CSS file:
|
||||
|
||||
```python
|
||||
class MyApp(App):
|
||||
CSS_PATH = "app.tcss" # Textual CSS file
|
||||
```
|
||||
|
||||
Or inline CSS:
|
||||
|
||||
```python
|
||||
class MyApp(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
background: $background;
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
## Selectors
|
||||
|
||||
### Type Selectors
|
||||
|
||||
Target all widgets of a type:
|
||||
|
||||
```css
|
||||
Button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
Label {
|
||||
color: cyan;
|
||||
}
|
||||
```
|
||||
|
||||
### ID Selectors
|
||||
|
||||
Target specific widget:
|
||||
|
||||
```css
|
||||
#my-button {
|
||||
background: red;
|
||||
}
|
||||
|
||||
#header {
|
||||
dock: top;
|
||||
}
|
||||
```
|
||||
|
||||
### Class Selectors
|
||||
|
||||
Target widgets with specific class:
|
||||
|
||||
```css
|
||||
.card {
|
||||
border: round white;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background: yellow;
|
||||
}
|
||||
```
|
||||
|
||||
Add classes in Python:
|
||||
|
||||
```python
|
||||
widget = Label("Text", classes="card selected")
|
||||
# or
|
||||
widget.add_class("highlighted")
|
||||
widget.remove_class("selected")
|
||||
widget.toggle_class("active")
|
||||
```
|
||||
|
||||
### Pseudo-classes
|
||||
|
||||
Style based on state:
|
||||
|
||||
```css
|
||||
Button:hover {
|
||||
background: $accent;
|
||||
}
|
||||
|
||||
Button:focus {
|
||||
border: double green;
|
||||
}
|
||||
|
||||
Input:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
Common pseudo-classes: `:hover`, `:focus`, `:focus-within`, `:disabled`, `:enabled`
|
||||
|
||||
### Combinators
|
||||
|
||||
```css
|
||||
/* Direct children */
|
||||
Container > Label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Descendants */
|
||||
Container Label {
|
||||
margin: 1;
|
||||
}
|
||||
|
||||
/* Class and type */
|
||||
Label.card {
|
||||
border: round;
|
||||
}
|
||||
```
|
||||
|
||||
## Colors
|
||||
|
||||
### Named Colors
|
||||
|
||||
```css
|
||||
Widget {
|
||||
color: red;
|
||||
background: blue;
|
||||
border: green;
|
||||
}
|
||||
```
|
||||
|
||||
### Hex Colors
|
||||
|
||||
```css
|
||||
Widget {
|
||||
color: #ff0000;
|
||||
background: #00ff0088; /* With alpha */
|
||||
}
|
||||
```
|
||||
|
||||
### RGB/RGBA
|
||||
|
||||
```css
|
||||
Widget {
|
||||
color: rgb(255, 0, 0);
|
||||
background: rgba(0, 255, 0, 0.5);
|
||||
}
|
||||
```
|
||||
|
||||
### Theme Variables
|
||||
|
||||
Use built-in theme colors:
|
||||
|
||||
```css
|
||||
Widget {
|
||||
background: $background;
|
||||
color: $text;
|
||||
border: $primary;
|
||||
}
|
||||
```
|
||||
|
||||
Common theme variables:
|
||||
- `$background` - Main background
|
||||
- `$surface` - Surface color
|
||||
- `$panel` - Panel background
|
||||
- `$boost` - Highlighted background
|
||||
- `$primary` - Primary accent
|
||||
- `$secondary` - Secondary accent
|
||||
- `$accent` - Accent color
|
||||
- `$text` - Main text color
|
||||
- `$text-muted` - Muted text
|
||||
- `$foreground-muted` - Dimmed foreground
|
||||
|
||||
## Borders
|
||||
|
||||
### Border Styles
|
||||
|
||||
```css
|
||||
Widget {
|
||||
border: solid red; /* Style and color */
|
||||
border: round cyan; /* Rounded border */
|
||||
border: double white; /* Double line */
|
||||
border: dashed yellow; /* Dashed */
|
||||
border: heavy green; /* Heavy/thick */
|
||||
border: tall blue; /* Tall characters */
|
||||
}
|
||||
```
|
||||
|
||||
### Border Sides
|
||||
|
||||
```css
|
||||
Widget {
|
||||
border-top: solid red;
|
||||
border-bottom: round blue;
|
||||
border-left: double green;
|
||||
border-right: dashed yellow;
|
||||
}
|
||||
```
|
||||
|
||||
### Border Title
|
||||
|
||||
```css
|
||||
Widget {
|
||||
border: round white;
|
||||
border-title-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
Set title in Python:
|
||||
|
||||
```python
|
||||
widget.border_title = "My Widget"
|
||||
```
|
||||
|
||||
## Text Styling
|
||||
|
||||
### Text Properties
|
||||
|
||||
```css
|
||||
Label {
|
||||
text-style: bold;
|
||||
text-style: italic;
|
||||
text-style: bold italic;
|
||||
text-style: underline;
|
||||
text-style: strike;
|
||||
}
|
||||
```
|
||||
|
||||
### Text Alignment
|
||||
|
||||
```css
|
||||
Static {
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
text-align: right;
|
||||
}
|
||||
```
|
||||
|
||||
## Keylines
|
||||
|
||||
Add separators between grid cells or flex items:
|
||||
|
||||
```css
|
||||
Grid {
|
||||
keyline: thin green;
|
||||
keyline: thick $primary;
|
||||
}
|
||||
```
|
||||
|
||||
Note: Must be on a container with a layout.
|
||||
|
||||
## Opacity
|
||||
|
||||
```css
|
||||
Widget {
|
||||
opacity: 0.5; /* 50% transparent */
|
||||
opacity: 0; /* Fully transparent */
|
||||
opacity: 1; /* Fully opaque */
|
||||
}
|
||||
```
|
||||
|
||||
## Tint
|
||||
|
||||
Apply color overlay:
|
||||
|
||||
```css
|
||||
Widget {
|
||||
tint: rgba(255, 0, 0, 0.3); /* Red tint */
|
||||
}
|
||||
```
|
||||
|
||||
## Rich Markup
|
||||
|
||||
Use Rich markup in text:
|
||||
|
||||
```python
|
||||
label = Label("[bold cyan]Hello[/bold cyan] [red]World[/red]")
|
||||
label.update("[underline]Updated[/underline]")
|
||||
```
|
||||
|
||||
Common markup:
|
||||
- `[bold]...[/bold]` - Bold
|
||||
- `[italic]...[/italic]` - Italic
|
||||
- `[color]...[/color]` - Colored (e.g., `[red]`, `[#ff0000]`)
|
||||
- `[underline]...[/underline]` - Underline
|
||||
- `[strike]...[/strike]` - Strikethrough
|
||||
- `[link=...]...[/link]` - Link
|
||||
|
||||
## Example: Card Styling
|
||||
|
||||
```css
|
||||
.card {
|
||||
width: 12;
|
||||
height: 10;
|
||||
border: round $secondary;
|
||||
background: $panel;
|
||||
padding: 1;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
background: $boost;
|
||||
border: heavy $primary;
|
||||
}
|
||||
|
||||
.card.selected {
|
||||
background: $accent;
|
||||
border: double $primary;
|
||||
}
|
||||
|
||||
.card.disabled {
|
||||
opacity: 0.5;
|
||||
tint: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
text-style: bold;
|
||||
text-align: center;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
text-align: center;
|
||||
color: $text-muted;
|
||||
}
|
||||
```
|
||||
241
skills/textual-builder/references/widgets.md
Normal file
241
skills/textual-builder/references/widgets.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Common Widgets
|
||||
|
||||
## Display Widgets
|
||||
|
||||
### Label / Static
|
||||
|
||||
Display static or updatable text:
|
||||
|
||||
```python
|
||||
from textual.widgets import Label, Static
|
||||
|
||||
# Label is just an alias for Static
|
||||
label = Label("Hello World")
|
||||
static = Static("Initial text")
|
||||
|
||||
# Update later
|
||||
static.update("New text")
|
||||
static.update("[bold]Rich markup[/bold]")
|
||||
```
|
||||
|
||||
### Placeholder
|
||||
|
||||
Useful for prototyping layouts:
|
||||
|
||||
```python
|
||||
from textual.widgets import Placeholder
|
||||
|
||||
# Shows widget ID and size info
|
||||
yield Placeholder("Custom label", id="p1")
|
||||
yield Placeholder(variant="size") # Shows dimensions
|
||||
yield Placeholder(variant="text") # Shows placeholder text
|
||||
```
|
||||
|
||||
## Input Widgets
|
||||
|
||||
### Button
|
||||
|
||||
```python
|
||||
from textual.widgets import Button
|
||||
|
||||
yield Button("Click Me", id="my-button")
|
||||
yield Button("Disabled", disabled=True)
|
||||
|
||||
# Handle click
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
button_id = event.button.id
|
||||
self.notify(f"{button_id} clicked!")
|
||||
```
|
||||
|
||||
### Input
|
||||
|
||||
Single-line text input:
|
||||
|
||||
```python
|
||||
from textual.widgets import Input
|
||||
|
||||
yield Input(placeholder="Enter text...", id="name-input")
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
self.text_value = event.value
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
# User pressed Enter
|
||||
self.process_input(event.value)
|
||||
```
|
||||
|
||||
### TextArea
|
||||
|
||||
Multi-line text editor:
|
||||
|
||||
```python
|
||||
from textual.widgets import TextArea
|
||||
|
||||
text_area = TextArea()
|
||||
text_area.load_text("Initial content")
|
||||
|
||||
# Get content
|
||||
content = text_area.text
|
||||
```
|
||||
|
||||
### Switch
|
||||
|
||||
Toggle switch (like checkbox):
|
||||
|
||||
```python
|
||||
from textual.widgets import Switch
|
||||
|
||||
yield Switch(value=True) # Initially on
|
||||
|
||||
def on_switch_changed(self, event: Switch.Changed) -> None:
|
||||
is_on = event.value
|
||||
self.toggle_feature(is_on)
|
||||
```
|
||||
|
||||
## Data Display
|
||||
|
||||
### DataTable
|
||||
|
||||
Display tabular data:
|
||||
|
||||
```python
|
||||
from textual.widgets import DataTable
|
||||
|
||||
table = DataTable()
|
||||
|
||||
# Add columns
|
||||
table.add_columns("Name", "Age", "Country")
|
||||
|
||||
# Add rows
|
||||
table.add_row("Alice", 30, "USA")
|
||||
table.add_row("Bob", 25, "UK")
|
||||
|
||||
# Add row with custom label
|
||||
from rich.text import Text
|
||||
label = Text("1", style="bold cyan")
|
||||
table.add_row("Charlie", 35, "Canada", label=label)
|
||||
|
||||
# Configuration
|
||||
table.zebra_stripes = True # Alternating row colors
|
||||
table.cursor_type = "row" # "cell", "row", "column", or "none"
|
||||
table.show_header = True
|
||||
table.show_row_labels = True
|
||||
|
||||
# Handle selection
|
||||
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||
row_key = event.row_key
|
||||
row_data = table.get_row(row_key)
|
||||
|
||||
def on_data_table_cell_selected(self, event: DataTable.CellSelected) -> None:
|
||||
value = event.value
|
||||
coordinate = event.coordinate
|
||||
```
|
||||
|
||||
## Layout Containers
|
||||
|
||||
### Container
|
||||
|
||||
Generic container for grouping widgets:
|
||||
|
||||
```python
|
||||
from textual.containers import Container
|
||||
|
||||
with Container(id="sidebar"):
|
||||
yield Label("Title")
|
||||
yield Button("Action")
|
||||
```
|
||||
|
||||
### Vertical / Horizontal / VerticalScroll / HorizontalScroll
|
||||
|
||||
Directional containers:
|
||||
|
||||
```python
|
||||
from textual.containers import Vertical, Horizontal, VerticalScroll
|
||||
|
||||
with Horizontal():
|
||||
yield Button("Left")
|
||||
yield Button("Right")
|
||||
|
||||
with VerticalScroll():
|
||||
for i in range(100):
|
||||
yield Label(f"Item {i}")
|
||||
```
|
||||
|
||||
### Grid
|
||||
|
||||
Grid layout container:
|
||||
|
||||
```python
|
||||
from textual.containers import Grid
|
||||
|
||||
with Grid(id="my-grid"):
|
||||
yield Label("A")
|
||||
yield Label("B")
|
||||
yield Label("C")
|
||||
yield Label("D")
|
||||
|
||||
# Style in CSS:
|
||||
# Grid {
|
||||
# grid-size: 2 2; /* 2 columns, 2 rows */
|
||||
# }
|
||||
```
|
||||
|
||||
## App Widgets
|
||||
|
||||
### Header / Footer
|
||||
|
||||
Standard app chrome:
|
||||
|
||||
```python
|
||||
from textual.widgets import Header, Footer
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
# ... content ...
|
||||
yield Footer()
|
||||
```
|
||||
|
||||
Footer automatically shows key bindings defined in BINDINGS.
|
||||
|
||||
## Custom Widgets
|
||||
|
||||
Create reusable components:
|
||||
|
||||
```python
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Label, Button
|
||||
|
||||
class Card(Widget):
|
||||
"""A card widget with title and content."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
Card {
|
||||
width: 30;
|
||||
height: 15;
|
||||
border: round white;
|
||||
padding: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, title: str, content: str) -> None:
|
||||
super().__init__()
|
||||
self.title = title
|
||||
self.content = content
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label(self.title, classes="card-title")
|
||||
yield Label(self.content, classes="card-content")
|
||||
yield Button("Select", id=f"select-{self.title}")
|
||||
```
|
||||
|
||||
### Render Method
|
||||
|
||||
For simple custom widgets that just render text:
|
||||
|
||||
```python
|
||||
from textual.widget import Widget
|
||||
|
||||
class FizzBuzz(Widget):
|
||||
def render(self) -> str:
|
||||
return "FizzBuzz!"
|
||||
```
|
||||
Reference in New Issue
Block a user