mirror of
https://github.com/cloud-shuttle/leptos-shadcn-ui.git
synced 2025-12-22 22:00:00 +00:00
- Reduced signal management test errors from 500 to 275 (225 errors fixed) - Added missing error variants: SignalError, MemoError, CleanupError, MemoryError, BatchError - Added missing methods to SignalMemoryManager: total_signals, total_memos, memory_usage_kb, add_signal, add_memo, cleanup_group, cleanup_all, with_limits, cleanup_low_priority_groups, adaptive_cleanup, update_memory_stats, get_memory_stats - Added missing methods to SignalGroup: remove_signal, remove_memo, with_timestamp - Added missing methods to BatchedSignalUpdater: clear_updates, stop_batching - Made fields public: tracked_groups, max_memory_bytes, stats - Added Debug and Clone derives to SignalMemoryManager and BatchedSignalUpdater - Fixed error variant syntax to use tuple variants - Fixed command component test imports and string literal types - Fixed input component test API mismatches - Added comprehensive remediation documentation - Completed P0 critical fixes (3/3 packages working) - Completed P1 stub implementations (1/1 package working) Progress: All critical packages now compile successfully, test infrastructure significantly improved
11 KiB
11 KiB
🎨 Command Component Design
Overview
Design for the Command component that provides a command palette interface with search, filtering, and keyboard navigation.
Core Components
Command Component
#[component]
pub fn Command(
#[prop(into, optional)] value: Option<Signal<String>>,
#[prop(into, optional)] on_select: Option<Callback<String>>,
#[prop(into, optional)] class: Option<String>,
#[prop(into, optional)] id: Option<String>,
#[prop(into, optional)] placeholder: Option<String>,
#[prop(into, optional)] disabled: Option<Signal<bool>>,
#[prop(into, optional)] children: Option<Children>,
) -> impl IntoView {
let (is_open, set_is_open) = signal(false);
let (search_value, set_search_value) = signal(String::new());
let (selected_index, set_selected_index) = signal(0);
let command_class = move || {
let mut classes = vec!["flex", "h-full", "w-full", "flex-col", "overflow-hidden", "rounded-md", "border", "bg-popover", "text-popover-foreground"];
if let Some(custom_class) = class.as_ref() {
classes.push(custom_class);
}
classes.join(" ")
};
let handle_keydown = move |ev: leptos::ev::KeyboardEvent| {
match ev.key().as_str() {
"ArrowDown" => {
ev.prevent_default();
set_selected_index.update(|i| *i += 1);
}
"ArrowUp" => {
ev.prevent_default();
set_selected_index.update(|i| if *i > 0 { *i -= 1 });
}
"Enter" => {
ev.prevent_default();
if let Some(on_select) = on_select.as_ref() {
on_select.call(search_value.get());
}
}
"Escape" => {
ev.prevent_default();
set_is_open.set(false);
}
_ => {}
}
};
view! {
<div
class=command_class
id=id
on:keydown=handle_keydown
role="combobox"
aria-expanded=is_open
aria-haspopup="listbox"
>
{children}
</div>
}
}
CommandInput Component
#[component]
pub fn CommandInput(
#[prop(into, optional)] value: Option<Signal<String>>,
#[prop(into, optional)] on_change: Option<Callback<String>>,
#[prop(into, optional)] placeholder: Option<String>,
#[prop(into, optional)] class: Option<String>,
#[prop(into, optional)] disabled: Option<Signal<bool>>,
) -> impl IntoView {
let (input_value, set_input_value) = value.unwrap_or_else(|| signal(String::new()));
let input_class = move || {
let mut classes = vec!["flex", "h-11", "w-full", "rounded-md", "bg-transparent", "py-3", "text-sm", "outline-none", "placeholder:text-muted-foreground", "disabled:cursor-not-allowed", "disabled:opacity-50"];
if let Some(custom_class) = class.as_ref() {
classes.push(custom_class);
}
classes.join(" ")
};
let handle_input = move |ev: leptos::ev::InputEvent| {
let value = event_target_value(&ev);
set_input_value.set(value.clone());
if let Some(on_change) = on_change.as_ref() {
on_change.call(value);
}
};
view! {
<input
value=input_value
placeholder=placeholder.unwrap_or_else(|| "Search...".to_string())
disabled=disabled.map(|d| d.get()).unwrap_or(false)
class=input_class
on:input=handle_input
autocomplete="off"
autocorrect="off"
spellcheck="false"
/>
}
}
CommandList Component
#[component]
pub fn CommandList(
#[prop(into, optional)] class: Option<String>,
#[prop(into, optional)] children: Option<Children>,
) -> impl IntoView {
let list_class = move || {
let mut classes = vec!["max-h-[300px]", "overflow-y-auto", "overflow-x-hidden"];
if let Some(custom_class) = class.as_ref() {
classes.push(custom_class);
}
classes.join(" ")
};
view! {
<div
class=list_class
role="listbox"
>
{children}
</div>
}
}
CommandEmpty Component
#[component]
pub fn CommandEmpty(
#[prop(into, optional)] class: Option<String>,
#[prop(into, optional)] children: Option<Children>,
) -> impl IntoView {
let empty_class = move || {
let mut classes = vec!["py-6", "text-center", "text-sm"];
if let Some(custom_class) = class.as_ref() {
classes.push(custom_class);
}
classes.join(" ")
};
view! {
<div
class=empty_class
role="status"
aria-live="polite"
>
{children.unwrap_or_else(|| view! { "No results found." })}
</div>
}
}
CommandGroup Component
#[component]
pub fn CommandGroup(
#[prop(into, optional)] heading: Option<String>,
#[prop(into, optional)] class: Option<String>,
#[prop(into, optional)] children: Option<Children>,
) -> impl IntoView {
let group_class = move || {
let mut classes = vec!["overflow-hidden", "p-1", "text-foreground"];
if let Some(custom_class) = class.as_ref() {
classes.push(custom_class);
}
classes.join(" ")
};
view! {
<div
class=group_class
role="group"
>
if let Some(heading) = heading {
<div class="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
{heading}
</div>
}
{children}
</div>
}
}
CommandItem Component
#[component]
pub fn CommandItem(
#[prop(into, optional)] value: Option<String>,
#[prop(into, optional)] on_select: Option<Callback<String>>,
#[prop(into, optional)] class: Option<String>,
#[prop(into, optional)] disabled: Option<Signal<bool>>,
#[prop(into, optional)] children: Option<Children>,
) -> impl IntoView {
let (is_selected, set_is_selected) = signal(false);
let item_class = move || {
let mut classes = vec!["relative", "flex", "cursor-default", "select-none", "items-center", "rounded-sm", "px-2", "py-1.5", "text-sm", "outline-none", "aria-selected:bg-accent", "aria-selected:text-accent-foreground", "data-[disabled]:pointer-events-none", "data-[disabled]:opacity-50"];
if is_selected.get() {
classes.push("bg-accent", "text-accent-foreground");
}
if let Some(custom_class) = class.as_ref() {
classes.push(custom_class);
}
classes.join(" ")
};
let handle_click = move |_| {
if let Some(value) = value.as_ref() {
if let Some(on_select) = on_select.as_ref() {
on_select.call(value.clone());
}
}
};
view! {
<div
class=item_class
on:click=handle_click
role="option"
aria-selected=is_selected
data-disabled=disabled.map(|d| d.get()).unwrap_or(false)
>
{children}
</div>
}
}
CommandShortcut Component
#[component]
pub fn CommandShortcut(
#[prop(into, optional)] class: Option<String>,
#[prop(into, optional)] children: Option<Children>,
) -> impl IntoView {
let shortcut_class = move || {
let mut classes = vec!["ml-auto", "text-xs", "tracking-widest", "opacity-60"];
if let Some(custom_class) = class.as_ref() {
classes.push(custom_class);
}
classes.join(" ")
};
view! {
<span
class=shortcut_class
>
{children}
</span>
}
}
CommandSeparator Component
#[component]
pub fn CommandSeparator(
#[prop(into, optional)] class: Option<String>,
) -> impl IntoView {
let separator_class = move || {
let mut classes = vec!["-mx-1", "h-px", "bg-border"];
if let Some(custom_class) = class.as_ref() {
classes.push(custom_class);
}
classes.join(" ")
};
view! {
<div
class=separator_class
role="separator"
/>
}
}
Usage Examples
Basic Command Palette
let (search_value, set_search_value) = signal(String::new());
let (selected_value, set_selected_value) = signal(String::new());
let handle_select = move |value: String| {
set_selected_value.set(value);
println!("Selected: {}", value);
};
view! {
<Command
value=search_value
on_select=handle_select
class="w-96"
>
<CommandInput
value=search_value
on_change=move |value| set_search_value.set(value)
placeholder="Search commands..."
/>
<CommandList>
<CommandItem
value="new-file".to_string()
on_select=handle_select
>
"New File"
<CommandShortcut>"⌘N"</CommandShortcut>
</CommandItem>
<CommandItem
value="save-file".to_string()
on_select=handle_select
>
"Save File"
<CommandShortcut>"⌘S"</CommandShortcut>
</CommandItem>
</CommandList>
</Command>
}
Command with Groups
view! {
<Command class="w-96">
<CommandInput placeholder="Search..." />
<CommandList>
<CommandGroup heading="File">
<CommandItem value="new">"New File"</CommandItem>
<CommandItem value="open">"Open File"</CommandItem>
<CommandItem value="save">"Save File"</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Edit">
<CommandItem value="undo">"Undo"</CommandItem>
<CommandItem value="redo">"Redo"</CommandItem>
<CommandItem value="cut">"Cut"</CommandItem>
</CommandGroup>
</CommandList>
</Command>
}
Command with Empty State
view! {
<Command class="w-96">
<CommandInput placeholder="Search..." />
<CommandList>
<CommandEmpty>
"No commands found. Try a different search term."
</CommandEmpty>
</CommandList>
</Command>
}
Accessibility Features
Keyboard Navigation
- Arrow Keys: Navigate through items
- Enter: Select current item
- Escape: Close command palette
- Tab: Focus management
ARIA Attributes
role="combobox": Main command componentrole="listbox": Command listrole="option": Command itemsaria-expanded: Open/closed statearia-selected: Selected item state
Screen Reader Support
- Proper labeling and descriptions
- State announcements
- Focus management
File Size: 298 lines Priority: 🔴 P0 - CRITICAL Dependencies: leptos