Initial commit
This commit is contained in:
593
commands/gpui-component.md
Normal file
593
commands/gpui-component.md
Normal file
@@ -0,0 +1,593 @@
|
||||
---
|
||||
name: gpui-component
|
||||
description: Generate reusable GPUI components with proper typing, state management, styling, and documentation
|
||||
---
|
||||
|
||||
# GPUI Component Generator
|
||||
|
||||
Generate reusable, well-structured GPUI components with proper typing, state management, styling, and comprehensive documentation.
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$1`: Component name (required) - Name of the component in PascalCase (e.g., "Button", "DataTable", "SearchInput")
|
||||
- `$2`: Component type (optional) - Either "stateless" (default) or "stateful"
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Gather Component Requirements
|
||||
|
||||
Ask user for:
|
||||
- Component purpose and description
|
||||
- Props/configuration needed
|
||||
- Whether component needs internal state
|
||||
- Event handlers required
|
||||
- Styling requirements
|
||||
- Accessibility needs
|
||||
|
||||
### 2. Generate Component Struct
|
||||
|
||||
Create component struct based on type:
|
||||
|
||||
#### Stateless Component
|
||||
|
||||
```rust
|
||||
use gpui::*;
|
||||
|
||||
/// A reusable button component
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// Button::new("Click me")
|
||||
/// .on_click(|cx| {
|
||||
/// println!("Clicked!");
|
||||
/// })
|
||||
/// ```
|
||||
pub struct Button {
|
||||
label: String,
|
||||
variant: ButtonVariant,
|
||||
disabled: bool,
|
||||
on_click: Option<Box<dyn Fn(&mut WindowContext)>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum ButtonVariant {
|
||||
Primary,
|
||||
Secondary,
|
||||
Destructive,
|
||||
Ghost,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(label: impl Into<String>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
variant: ButtonVariant::Primary,
|
||||
disabled: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn variant(mut self, variant: ButtonVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(mut self, handler: impl Fn(&mut WindowContext) + 'static) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Stateful Component
|
||||
|
||||
```rust
|
||||
use gpui::*;
|
||||
|
||||
/// A search input with autocomplete
|
||||
pub struct SearchInput {
|
||||
query: String,
|
||||
suggestions: Vec<String>,
|
||||
selected_index: Option<usize>,
|
||||
on_search: Option<Box<dyn Fn(&str, &mut WindowContext)>>,
|
||||
}
|
||||
|
||||
impl SearchInput {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
query: String::new(),
|
||||
suggestions: Vec::new(),
|
||||
selected_index: None,
|
||||
on_search: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_search(mut self, handler: impl Fn(&str, &mut WindowContext) + 'static) -> Self {
|
||||
self.on_search = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, value: String, cx: &mut ViewContext<Self>) {
|
||||
self.query = value;
|
||||
self.update_suggestions(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn update_suggestions(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// Update suggestions based on query
|
||||
if let Some(handler) = &self.on_search {
|
||||
handler(&self.query, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) {
|
||||
match event.key.as_str() {
|
||||
"ArrowDown" => {
|
||||
self.selected_index = Some(
|
||||
self.selected_index
|
||||
.map(|i| (i + 1).min(self.suggestions.len() - 1))
|
||||
.unwrap_or(0)
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
"ArrowUp" => {
|
||||
self.selected_index = self.selected_index
|
||||
.and_then(|i| i.checked_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
"Enter" => {
|
||||
if let Some(index) = self.selected_index {
|
||||
self.query = self.suggestions[index].clone();
|
||||
self.suggestions.clear();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Implement Element/Render Traits
|
||||
|
||||
#### Stateless Component Render
|
||||
|
||||
```rust
|
||||
impl Render for Button {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let theme = cx.global::<Theme>();
|
||||
let (bg_color, text_color, hover_color) = match self.variant {
|
||||
ButtonVariant::Primary => (
|
||||
theme.primary,
|
||||
theme.primary_foreground,
|
||||
theme.primary_hover,
|
||||
),
|
||||
ButtonVariant::Secondary => (
|
||||
theme.secondary,
|
||||
theme.secondary_foreground,
|
||||
theme.secondary_hover,
|
||||
),
|
||||
ButtonVariant::Destructive => (
|
||||
theme.destructive,
|
||||
theme.destructive_foreground,
|
||||
theme.destructive_hover,
|
||||
),
|
||||
ButtonVariant::Ghost => (
|
||||
hsla(0.0, 0.0, 0.0, 0.0),
|
||||
theme.foreground,
|
||||
theme.muted,
|
||||
),
|
||||
};
|
||||
|
||||
div()
|
||||
.px_4()
|
||||
.py_2()
|
||||
.bg(bg_color)
|
||||
.text_color(text_color)
|
||||
.rounded_md()
|
||||
.font_medium()
|
||||
.when(!self.disabled, |this| {
|
||||
this.cursor_pointer()
|
||||
.hover(|this| this.bg(hover_color))
|
||||
})
|
||||
.when(self.disabled, |this| {
|
||||
this.opacity(0.5)
|
||||
.cursor_not_allowed()
|
||||
})
|
||||
.when_some(self.on_click.take(), |this, handler| {
|
||||
this.on_click(move |_, cx| {
|
||||
if !self.disabled {
|
||||
handler(cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(self.label.clone())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Stateful Component Render
|
||||
|
||||
```rust
|
||||
impl Render for SearchInput {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let theme = cx.global::<Theme>();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.bg(theme.background)
|
||||
.border_1()
|
||||
.border_color(theme.border)
|
||||
.rounded_md()
|
||||
.child(
|
||||
input()
|
||||
.flex_1()
|
||||
.placeholder("Search...")
|
||||
.value(&self.query)
|
||||
.on_input(cx.listener(|this, value, cx| {
|
||||
this.handle_input(value, cx);
|
||||
}))
|
||||
.on_key_down(cx.listener(|this, event, cx| {
|
||||
this.handle_key_down(event, cx);
|
||||
}))
|
||||
)
|
||||
)
|
||||
.when(!self.suggestions.is_empty(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_full()
|
||||
.left_0()
|
||||
.right_0()
|
||||
.mt_1()
|
||||
.bg(theme.background)
|
||||
.border_1()
|
||||
.border_color(theme.border)
|
||||
.rounded_md()
|
||||
.shadow_lg()
|
||||
.max_h_64()
|
||||
.overflow_y_auto()
|
||||
.children(
|
||||
self.suggestions.iter().enumerate().map(|(i, suggestion)| {
|
||||
div()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.cursor_pointer()
|
||||
.when(self.selected_index == Some(i), |this| {
|
||||
this.bg(theme.accent)
|
||||
})
|
||||
.hover(|this| this.bg(theme.muted))
|
||||
.child(suggestion.as_str())
|
||||
})
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Add State Management
|
||||
|
||||
For stateful components:
|
||||
|
||||
```rust
|
||||
impl SearchInput {
|
||||
pub fn set_suggestions(&mut self, suggestions: Vec<String>, cx: &mut ViewContext<Self>) {
|
||||
self.suggestions = suggestions;
|
||||
self.selected_index = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.query.clear();
|
||||
self.suggestions.clear();
|
||||
self.selected_index = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn query(&self) -> &str {
|
||||
&self.query
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Generate Styling
|
||||
|
||||
Create styled variants and theme integration:
|
||||
|
||||
```rust
|
||||
// Component-specific theme
|
||||
pub struct ButtonTheme {
|
||||
pub primary_bg: Hsla,
|
||||
pub primary_fg: Hsla,
|
||||
pub primary_hover: Hsla,
|
||||
pub secondary_bg: Hsla,
|
||||
pub secondary_fg: Hsla,
|
||||
pub secondary_hover: Hsla,
|
||||
pub border_radius: Pixels,
|
||||
pub padding_x: Pixels,
|
||||
pub padding_y: Pixels,
|
||||
}
|
||||
|
||||
impl ButtonTheme {
|
||||
pub fn from_app_theme(theme: &AppTheme) -> Self {
|
||||
Self {
|
||||
primary_bg: theme.colors.primary,
|
||||
primary_fg: theme.colors.primary_foreground,
|
||||
primary_hover: theme.colors.primary_hover,
|
||||
secondary_bg: theme.colors.secondary,
|
||||
secondary_fg: theme.colors.secondary_foreground,
|
||||
secondary_hover: theme.colors.secondary_hover,
|
||||
border_radius: px(6.0),
|
||||
padding_x: px(16.0),
|
||||
padding_y: px(8.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Create Documentation
|
||||
|
||||
Generate comprehensive documentation:
|
||||
|
||||
```rust
|
||||
//! Button Component
|
||||
//!
|
||||
//! A flexible, accessible button component with multiple variants and states.
|
||||
//!
|
||||
//! # Features
|
||||
//!
|
||||
//! - Multiple variants (Primary, Secondary, Destructive, Ghost)
|
||||
//! - Disabled state support
|
||||
//! - Customizable click handlers
|
||||
//! - Full keyboard accessibility
|
||||
//! - Theme integration
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ## Basic Usage
|
||||
//!
|
||||
//! ```rust
|
||||
//! let button = Button::new("Click me")
|
||||
//! .on_click(|cx| {
|
||||
//! println!("Button clicked!");
|
||||
//! });
|
||||
//! ```
|
||||
//!
|
||||
//! ## With Variants
|
||||
//!
|
||||
//! ```rust
|
||||
//! let primary = Button::new("Primary").variant(ButtonVariant::Primary);
|
||||
//! let secondary = Button::new("Secondary").variant(ButtonVariant::Secondary);
|
||||
//! let destructive = Button::new("Delete").variant(ButtonVariant::Destructive);
|
||||
//! ```
|
||||
//!
|
||||
//! ## Disabled State
|
||||
//!
|
||||
//! ```rust
|
||||
//! let button = Button::new("Disabled")
|
||||
//! .disabled(true);
|
||||
//! ```
|
||||
//!
|
||||
//! # Accessibility
|
||||
//!
|
||||
//! - Supports keyboard navigation (Enter/Space to activate)
|
||||
//! - Proper ARIA attributes
|
||||
//! - Focus indicators
|
||||
//! - Disabled state communicated to screen readers
|
||||
```
|
||||
|
||||
### 7. Provide Usage Examples
|
||||
|
||||
Create example usage code:
|
||||
|
||||
```rust
|
||||
// examples/button_example.rs
|
||||
use gpui::*;
|
||||
|
||||
fn main() {
|
||||
App::new("com.example.button-demo", |cx| {
|
||||
cx.open_window(
|
||||
WindowOptions::default(),
|
||||
|cx| cx.new_view(|cx| ButtonDemo::new(cx))
|
||||
)
|
||||
}).run();
|
||||
}
|
||||
|
||||
struct ButtonDemo;
|
||||
|
||||
impl ButtonDemo {
|
||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ButtonDemo {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.p_8()
|
||||
.child(
|
||||
Button::new("Primary Button")
|
||||
.variant(ButtonVariant::Primary)
|
||||
.on_click(|cx| {
|
||||
println!("Primary clicked!");
|
||||
})
|
||||
)
|
||||
.child(
|
||||
Button::new("Secondary Button")
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.on_click(|cx| {
|
||||
println!("Secondary clicked!");
|
||||
})
|
||||
)
|
||||
.child(
|
||||
Button::new("Destructive Button")
|
||||
.variant(ButtonVariant::Destructive)
|
||||
.on_click(|cx| {
|
||||
println!("Destructive clicked!");
|
||||
})
|
||||
)
|
||||
.child(
|
||||
Button::new("Disabled Button")
|
||||
.disabled(true)
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Add Component Tests
|
||||
|
||||
Generate tests for the component:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_button_creation() {
|
||||
App::test(|cx| {
|
||||
let button = Button::new("Test");
|
||||
assert_eq!(button.label, "Test");
|
||||
assert_eq!(button.variant, ButtonVariant::Primary);
|
||||
assert!(!button.disabled);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_button_click() {
|
||||
App::test(|cx| {
|
||||
let clicked = Rc::new(RefCell::new(false));
|
||||
let clicked_clone = clicked.clone();
|
||||
|
||||
let button = Button::new("Test")
|
||||
.on_click(move |_| {
|
||||
*clicked_clone.borrow_mut() = true;
|
||||
});
|
||||
|
||||
// Simulate click
|
||||
if let Some(handler) = button.on_click {
|
||||
handler(cx);
|
||||
}
|
||||
|
||||
assert!(*clicked.borrow());
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Generate Component Module
|
||||
|
||||
Create module file with exports:
|
||||
|
||||
```rust
|
||||
// src/ui/components/button/mod.rs
|
||||
|
||||
mod button;
|
||||
mod theme;
|
||||
|
||||
pub use button::{Button, ButtonVariant};
|
||||
pub use theme::ButtonTheme;
|
||||
```
|
||||
|
||||
### 10. Provide Integration Instructions
|
||||
|
||||
Output integration guide:
|
||||
|
||||
```
|
||||
✓ Created Button component
|
||||
|
||||
Files created:
|
||||
- src/ui/components/button/button.rs
|
||||
- src/ui/components/button/theme.rs
|
||||
- src/ui/components/button/mod.rs
|
||||
- examples/button_example.rs
|
||||
- tests/button_test.rs
|
||||
|
||||
Next steps:
|
||||
|
||||
1. Add to your components module:
|
||||
In src/ui/components/mod.rs:
|
||||
pub mod button;
|
||||
pub use button::Button;
|
||||
|
||||
2. Use in your views:
|
||||
use crate::ui::components::Button;
|
||||
|
||||
Button::new("Click me")
|
||||
.variant(ButtonVariant::Primary)
|
||||
.on_click(|cx| {
|
||||
// Handle click
|
||||
})
|
||||
|
||||
3. Run example:
|
||||
cargo run --example button_example
|
||||
|
||||
4. Run tests:
|
||||
cargo test button
|
||||
|
||||
Documentation: See generated component docs for full API
|
||||
```
|
||||
|
||||
## Component Types
|
||||
|
||||
### Stateless Components
|
||||
- No internal state
|
||||
- Pure rendering based on props
|
||||
- Examples: Button, Icon, Label
|
||||
|
||||
### Stateful Components
|
||||
- Internal state management
|
||||
- User input handling
|
||||
- Examples: Input, SearchBox, Dropdown
|
||||
|
||||
### Container Components
|
||||
- Manage child components
|
||||
- State coordination
|
||||
- Examples: Form, List, Tabs
|
||||
|
||||
### Composite Components
|
||||
- Combine multiple components
|
||||
- Complex functionality
|
||||
- Examples: DataTable, Dialog, Wizard
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Builder pattern for configuration
|
||||
- Theme integration
|
||||
- Accessibility attributes
|
||||
- Comprehensive documentation
|
||||
- Usage examples
|
||||
- Unit tests
|
||||
- Type safety
|
||||
- Error handling
|
||||
|
||||
## Example Usage
|
||||
|
||||
```bash
|
||||
# Generate stateless component
|
||||
/gpui-component Button
|
||||
|
||||
# Generate stateful component
|
||||
/gpui-component SearchInput stateful
|
||||
|
||||
# Generate with custom requirements
|
||||
/gpui-component DataTable stateful --with-examples --with-tests
|
||||
```
|
||||
Reference in New Issue
Block a user