From c738db7781a6893af616500f31c7f1ae7893d204 Mon Sep 17 00:00:00 2001 From: elcoosp Date: Tue, 28 Apr 2026 18:53:33 +0200 Subject: [PATCH] feat: add KbdGroup component with story, docs, and sidebar fix - New component to group keyboard shortcuts - Gallery story for manual testing - Documentation page matching Kbd structure - Fix sidebar sorting to alphabetical - Remove duplicate gallery entry from sed - Tests for single and multiple Kbd elements --- .gitignore | 2 +- crates/story/src/gallery.rs | 1 + crates/story/src/stories/kbd_group_story.rs | 65 ++++++++++++++ crates/story/src/stories/mod.rs | 2 + crates/ui/src/kbd_group.rs | 78 +++++++++++++++++ crates/ui/src/lib.rs | 1 + docs/.vitepress/config.mts | 2 +- docs/docs/components/kbd-group.md | 97 +++++++++++++++++++++ justfile | 16 ++++ 9 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 crates/story/src/stories/kbd_group_story.rs create mode 100644 crates/ui/src/kbd_group.rs create mode 100644 docs/docs/components/kbd-group.md create mode 100644 justfile diff --git a/.gitignore b/.gitignore index 84b321fe2..bf0a65947 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ index.scip node_modules/ .claude/worktrees/ docs/superpowers/ - +wr.sh # WASM build artifacts crates/story-web/www/src/wasm/*.js crates/story-web/www/src/wasm/*.wasm diff --git a/crates/story/src/gallery.rs b/crates/story/src/gallery.rs index c743fcc20..9cb402831 100644 --- a/crates/story/src/gallery.rs +++ b/crates/story/src/gallery.rs @@ -63,6 +63,7 @@ impl Gallery { StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), + StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), diff --git a/crates/story/src/stories/kbd_group_story.rs b/crates/story/src/stories/kbd_group_story.rs new file mode 100644 index 000000000..4270fb7eb --- /dev/null +++ b/crates/story/src/stories/kbd_group_story.rs @@ -0,0 +1,65 @@ +use gpui::{ + App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, + Styled, Window, +}; +use gpui_component::{ + kbd::Kbd, + kbd_group::KbdGroup, + v_flex, +}; + +use crate::section; + +pub struct KbdGroupStory { + focus_handle: FocusHandle, +} + +impl super::Story for KbdGroupStory { + fn title() -> &'static str { + "KbdGroup" + } + + fn description() -> &'static str { + "Display grouped keyboard shortcuts." + } + + fn new_view(window: &mut Window, cx: &mut App) -> Entity { + Self::view(window, cx) + } +} + +impl KbdGroupStory { + pub fn view(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| Self::new(window, cx)) + } + + fn new(_: &mut Window, cx: &mut Context) -> Self { + Self { + focus_handle: cx.focus_handle(), + } + } +} + +impl Focusable for KbdGroupStory { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for KbdGroupStory { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + v_flex() + .gap_4() + .child(section("Basic").child( + KbdGroup::new() + .child(Kbd::new(gpui::Keystroke::parse("cmd-c").unwrap())) + .child(Kbd::new(gpui::Keystroke::parse("ctrl-v").unwrap())), + )) + .child(section("Three keys").child( + KbdGroup::new() + .child(Kbd::new(gpui::Keystroke::parse("cmd-shift-p").unwrap())) + .child(Kbd::new(gpui::Keystroke::parse("cmd-ctrl-t").unwrap())) + .child(Kbd::new(gpui::Keystroke::parse("escape").unwrap())), + )) + } +} diff --git a/crates/story/src/stories/mod.rs b/crates/story/src/stories/mod.rs index 981203c0b..2199b0dd6 100644 --- a/crates/story/src/stories/mod.rs +++ b/crates/story/src/stories/mod.rs @@ -27,6 +27,7 @@ mod hover_card_story; mod icon_story; mod image_story; mod input_story; +mod kbd_group_story; mod kbd_story; mod label_story; mod list_story; @@ -87,6 +88,7 @@ pub use hover_card_story::HoverCardStory; pub use icon_story::IconStory; pub use image_story::ImageStory; pub use input_story::InputStory; +pub use kbd_group_story::KbdGroupStory; pub use kbd_story::KbdStory; pub use label_story::LabelStory; pub use list_story::ListStory; diff --git a/crates/ui/src/kbd_group.rs b/crates/ui/src/kbd_group.rs new file mode 100644 index 000000000..7fbbb3b0c --- /dev/null +++ b/crates/ui/src/kbd_group.rs @@ -0,0 +1,78 @@ +use gpui::{App, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, Window, div}; + +use crate::{ActiveTheme, StyledExt, h_flex, kbd::Kbd}; + +/// A group of `Kbd` elements, rendered with a "+" separator between them. +#[derive(IntoElement)] +pub struct KbdGroup { + style: StyleRefinement, + children: Vec, +} + +impl KbdGroup { + /// Create a new empty `KbdGroup`. + pub fn new() -> Self { + Self { + style: StyleRefinement::default(), + children: Vec::new(), + } + } + + /// Add a `Kbd` child to the group. + pub fn child(mut self, kbd: impl Into) -> Self { + self.children.push(kbd.into()); + self + } +} + +impl ParentElement for KbdGroup { + fn extend(&mut self, elements: impl IntoIterator) { + // Not used; children are added via `.child()`. + let _ = elements; + } +} + +impl Styled for KbdGroup { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +impl RenderOnce for KbdGroup { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let mut group = h_flex().gap_1().items_center().refine_style(&self.style); + + let len = self.children.len(); + for (i, kbd) in self.children.into_iter().enumerate() { + group = group.child(kbd); + if i + 1 < len { + group = group.child(div().text_color(cx.theme().muted_foreground).child("+")); + } + } + group + } +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::TestAppContext; + + #[gpui::test] + fn test_kbd_group_single(cx: &mut TestAppContext) { + cx.update(|_| { + let group = KbdGroup::new().child(Kbd::new(gpui::Keystroke::parse("cmd-c").unwrap())); + let _ = group.into_any_element(); + }); + } + + #[gpui::test] + fn test_kbd_group_multiple(cx: &mut TestAppContext) { + cx.update(|_| { + let group = KbdGroup::new() + .child(Kbd::new(gpui::Keystroke::parse("cmd-c").unwrap())) + .child(Kbd::new(gpui::Keystroke::parse("ctrl-v").unwrap())); + let _ = group.into_any_element(); + }); + } +} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 69f71e0d7..1f38176a7 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -44,6 +44,7 @@ pub mod history; pub mod hover_card; pub mod input; pub mod kbd; +pub mod kbd_group; pub mod label; pub mod link; pub mod list; diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 6e1f1b721..eeb1a43fd 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -17,7 +17,7 @@ function createSidebar(scanStartPath: string, rootGroupText: string) { collapsed: false, useTitleFromFrontmatter: true, useTitleFromFileHeading: true, - sortMenusByFrontmatterOrder: true, + sortMenusByFrontmatterOrder: false, includeRootIndexFile: false, }, ]) as any; diff --git a/docs/docs/components/kbd-group.md b/docs/docs/components/kbd-group.md new file mode 100644 index 000000000..866caa9dd --- /dev/null +++ b/docs/docs/components/kbd-group.md @@ -0,0 +1,97 @@ +--- +title: KbdGroup +description: Groups multiple keyboard shortcuts with a \"+\" separator. +--- + +# KbdGroup + +A component for grouping multiple `Kbd` elements, automatically inserting a \"+\" separator between them. This mimics the visual style of key‑binding instructions like **Ctrl+C** or **Cmd+Shift+P**. + +## Import + +```sh +use gpui_component::kbd::Kbd; +use gpui_component::kbd_group::KbdGroup; +``` + +## Usage + +### Basic Group + +```sh +KbdGroup::new() + .child(Kbd::new(Keystroke::parse("cmd-c").unwrap())) + .child(Kbd::new(Keystroke::parse("ctrl-v").unwrap())) +``` + +### Multiple Keys + +```sh +KbdGroup::new() + .child(Kbd::new(Keystroke::parse("cmd-shift-p").unwrap())) + .child(Kbd::new(Keystroke::parse("cmd-ctrl-t").unwrap())) + .child(Kbd::new(Keystroke::parse("escape").unwrap())) +``` + +### Styling the Group Container + +```sh +KbdGroup::new() + .child(Kbd::new(Keystroke::parse("cmd-s").unwrap())) + .child(Kbd::new(Keystroke::parse("cmd-enter").unwrap())) + .gap_2() // increase spacing + .text_sm() // adjust font size +``` + +## Platform Differences + +Because each `Kbd` element already adapts to the platform, `KbdGroup` will automatically display the correct symbols or text labels and separator format. + +## Examples + +### Inline Shortcut Hint + +```sh +h_flex() + .gap_2() + .items_center() + .child("Save project (") + .child(KbdGroup::new() + .child(Kbd::new(Keystroke::parse("cmd-shift-s").unwrap())) + ) + .child(")") +``` + +### Menu Item with Shortcut + +```sh +h_flex() + .justify_between() + .items_center() + .child("Find in Files") + .child(KbdGroup::new() + .child(Kbd::new(Keystroke::parse("cmd-shift-f").unwrap())) + ) +``` + +### Tooltip with Keyboard Combination + +```sh +Button::new("undo") + .label("Undo") + .tooltip( + KbdGroup::new() + .child(Kbd::new(Keystroke::parse("cmd-z").unwrap())) + .child(Kbd::new(Keystroke::parse("ctrl-z").unwrap())) + ) +``` + +## API Reference + +- **`KbdGroup::new()`** – Creates an empty group. +- **`.child(kbd)`** – Adds a `Kbd` element to the group; a \"+\" separator is automatically inserted between consecutive children. +- The component implements `Styled`, so container‑level styling is fully customisable. + +## Styling + +The `KbdGroup` inherits its base appearance from the individual `Kbd` components. The container itself is a horizontal flex layout with a small gap. You can apply additional styling via the `Styled` trait methods. diff --git a/justfile b/justfile new file mode 100644 index 000000000..d1cb07768 --- /dev/null +++ b/justfile @@ -0,0 +1,16 @@ + +wr: + watchexec -w ./wr.sh --clear -r "sh ./wr.sh" +test: + cargo nextest run + +# ----------------------------------------------------------------------- +# GPUI Component story gallery & documentation +# ----------------------------------------------------------------------- +# Launch the interactive story gallery (native desktop app) +story: + cargo run -p gpui-component-story + +# Start the VitePress documentation website locally +docs: + cd docs && bun install && bun run dev