Skip to content

feat(components): add Kbd component #251

Merged
desmondinho merged 12 commits intoLumexUI:mainfrom
Denny09310:feature/kbd
Sep 15, 2025
Merged

feat(components): add Kbd component #251
desmondinho merged 12 commits intoLumexUI:mainfrom
Denny09310:feature/kbd

Conversation

@Denny09310
Copy link
Contributor

@Denny09310 Denny09310 commented Sep 14, 2025

Description

What's been done?

  • Added the Kbd component, adapted from HeroUI, into the LumexUI component library.
  • Implemented props and styling consistent with LumexUI’s design system.
  • Wrote inline documentation and usage examples for the Kbd component.
  • Added unit tests to cover rendering, props, and edge cases.

Checklist

  • My code follows the project's coding style and guidelines.
  • I have included inline docs for my changes, where applicable.
  • I have added, updated or removed tests according to my changes.
  • All tests are passing.
  • There's an open issue for the PR that I am making.

Additional Notes

No additional notes.

Summary by CodeRabbit

  • New Features

    • Introduced the LumexKbd component to render keyboard keys/combinations with glyphs, multi-key support, customizable slots, and slot-based styling.
    • Added a KeyboardKey enum and a built-in key-to-glyph mapping for consistent symbols.
  • Documentation

    • Added a Kbd docs page with Usage, Slots, API, interactive examples, and a navigation entry for Kbd.
  • Styles

    • Added default Kbd styling (spacing, typography, visuals).
  • Tests

    • Added tests validating rendering, child content, and multi-key display.

@coderabbitai
Copy link

coderabbitai bot commented Sep 14, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds a new LumexKbd keyboard-key component (component, slots, styles, constants, enum), tests, documentation pages/examples/preview wrappers, and a NavigationStore entry for LumexKbd.

Changes

Cohort / File(s) Summary
Docs Navigation
docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs
Inserts LumexKbd entry into Components with PageStatus.New.
Docs: Kbd page & previews
docs/LumexUI.Docs.Client/Pages/Components/Kbd/Kbd.razor, docs/LumexUI.Docs.Client/Pages/Components/Kbd/PreviewCodes/*, docs/LumexUI.Docs.Client/Pages/Components/Kbd/Examples/*
Adds Kbd documentation page, interactive preview wrappers, and Usage/Keys examples demonstrating single and multi-key badges.
Core enum
src/LumexUI/Common/Enums/KeyboardKey.cs
Adds public KeyboardKey enum enumerating common keyboard keys.
Component slots & constants
src/LumexUI/Components/Kbd/KbdSlots.cs, src/LumexUI/Components/Kbd/KbdConstants.cs
Adds KbdSlots (Abbr, Content) and KbdConstants mapping KeyboardKey → display symbols.
Component implementation
src/LumexUI/Components/Kbd/LumexKbd.razor, src/LumexUI/Components/Kbd/LumexKbd.razor.cs
New LumexKbd component: renders <kbd> wrapper, <abbr> per key (symbol lookup via KbdConstants), ChildContent, Keys parameter, and slot-style resolution logic.
Styles
src/LumexUI/Styles/Kbd.cs
Adds internal Kbd style variant builder (Base/Abbr/Content) using TailwindMerge.
Tests
tests/LumexUI.Tests/Components/Kbd/KbdTests.razor
Adds tests for rendering single key, children/content, and multiple keys asserting expected symbols and DOM structure.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App
  participant LumexKbd
  participant Styles as Styles.Kbd
  participant Consts as KbdConstants

  App->>LumexKbd: Render(Keys[], ChildContent)
  LumexKbd->>Styles: Style(TwMerge) 
  Styles-->>LumexKbd: slot resolvers (Base/Abbr/Content)
  loop keys
    LumexKbd->>Consts: KeyboardKeys.TryGetValue(key)
    Consts-->>LumexKbd: symbol or null
    LumexKbd-->>App: emit <abbr title=key>symbol</abbr>
  end
  LumexKbd-->>App: emit <span>ChildContent</span> inside <kbd>
  note right of App: Result: <kbd> with key badges and content
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

📦 Scope: Components, 🚀 Type: Feature

Poem

I nibble keys and stitch a badge,
⌘ and ⇧ snug on my page—
Small slots, bright glyphs, a tidy node,
Rabbit hops and ships this code. 🐇⌨️

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description contains a clear summary, a "What's been done?" list, a checklist, and "Additional Notes", but it does not include the required "Closes #{issue_number}" line from the repository template and does not indicate an issue number, so it does not fully conform to the template. Add the required "Closes #<issue_number>" line under the Description heading (or explicitly state "No issue" if there is none) and update the checklist to accurately reflect the presence or absence of an associated issue; optionally include CI/test run details or a link to the passing pipeline to substantiate "All tests are passing."
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "feat(components): add Kbd component" is concise, follows conventional commit style, and accurately summarizes the primary change (adding the Kbd component) present across the changeset, so it clearly communicates the main intent to reviewers.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2883985 and 6878d46.

📒 Files selected for processing (5)
  • docs/LumexUI.Docs.Client/Pages/Components/Kbd/Kbd.razor (1 hunks)
  • src/LumexUI/Components/Kbd/KbdConstants.cs (1 hunks)
  • src/LumexUI/Components/Kbd/LumexKbd.razor (1 hunks)
  • src/LumexUI/Components/Kbd/LumexKbd.razor.cs (1 hunks)
  • tests/LumexUI.Tests/Components/Kbd/KbdTests.razor (1 hunks)

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Sep 14, 2025

Codecov Report

❌ Patch coverage is 97.36842% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 93.24%. Comparing base (19b89dc) to head (6878d46).
⚠️ Report is 132 commits behind head on main.

Files with missing lines Patch % Lines
src/LumexUI/Components/Kbd/LumexKbd.razor 66.67% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #251      +/-   ##
==========================================
- Coverage   96.95%   93.24%   -3.72%     
==========================================
  Files          70      149      +79     
  Lines        1542     2617    +1075     
  Branches      150      392     +242     
==========================================
+ Hits         1495     2440     +945     
- Misses         28       92      +64     
- Partials       19       85      +66     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (16)
src/LumexUI/Common/Enums/KeyboardKey.cs (1)

10-106: Enum looks solid; consider cross‑platform aliases.

Add Alt/Meta/Windows (or aliases) to avoid platform ambiguity (Option vs Alt, Command vs Meta/Win). Ensure glyph/title mapping covers them.

Apply if desired:

 public enum KeyboardKey
 {
+    /// <summary>Alt key (⌥ on macOS as Option).</summary>
+    Alt,
+    /// <summary>Meta key (⌘ on macOS as Command).</summary>
+    Meta,
+    /// <summary>Windows key (⊞).</summary>
+    Windows,
     Command,
     Shift,
     Control,
     Option,
     Enter,
     Delete,
     Escape,
     Tab,
     CapsLock,
     Up,
     Right,
     Down,
     Left,
     PageUp,
     PageDown,
     Home,
     End,
     Help,
     Space,
 }
docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs (1)

37-37: Good addition to Components nav. Also add to Components API for parity.

Keep docs navigation consistent by exposing LumexKbd in Components API too.

 			.Add( new( nameof( LumexDropdownMenu ) ) )
+			.Add( new( nameof( LumexKbd ) ) )
 			//.Add( nameof( LumexInputBase<T> ) )
docs/LumexUI.Docs.Client/Pages/Components/Kbd/Examples/Usage.razor (1)

1-1: Add import and a11y label.

Ensure KeyboardKey is in scope and provide an aria-label for SR users.

-<LumexKbd Keys="@([KeyboardKey.Command])">K</LumexKbd>
+@using LumexUI.Common
+<LumexKbd Keys="@([KeyboardKey.Command])" aria-label="Command + K">K</LumexKbd>
docs/LumexUI.Docs.Client/Pages/Components/Kbd/Examples/Keys.razor (1)

1-5: Import enum and add aria-labels for combos.

Improves discoverability for screen readers.

-<div class="flex items-center gap-2">
-	<LumexKbd Keys="@([KeyboardKey.Command])">K</LumexKbd>
-	<LumexKbd Keys="@([KeyboardKey.Command, KeyboardKey.Shift])">N</LumexKbd>
-	<LumexKbd Keys="@([KeyboardKey.Option, KeyboardKey.Command])">P</LumexKbd>
-</div>
+@using LumexUI.Common
+<div class="flex items-center gap-2">
+	<LumexKbd Keys="@([KeyboardKey.Command])" aria-label="Command + K">K</LumexKbd>
+	<LumexKbd Keys="@([KeyboardKey.Command, KeyboardKey.Shift])" aria-label="Command + Shift + N">N</LumexKbd>
+	<LumexKbd Keys="@([KeyboardKey.Option, KeyboardKey.Command])" aria-label="Option + Command + P">P</LumexKbd>
+</div>
src/LumexUI/Components/Kbd/LumexKbd.razor (2)

6-16: Add an accessible label on the root.

Expose a combined keys label for SRs (e.g., “Command + K”). Either compute it or allow consumers to pass it via AdditionalAttributes.

If computing internally, consider:

-<LumexComponent As="@As" 
-				Class="@GetStyles( nameof( S.Base ) )"
-				Style="@RootStyle"
-				@attributes="AdditionalAttributes">
+<LumexComponent As="@As" 
+				Class="@GetStyles( nameof( S.Base ) )"
+				Style="@RootStyle"
+				aria-label="@KeysLabel"
+				@attributes="AdditionalAttributes">
@@
 	<span class="@GetStyles( nameof( S.Content ) )">@ChildContent</span>
 </LumexComponent>

And in code-behind expose protected string? KeysLabel => Keys is null ? null : string.Join(" + ", Keys.Select(k => k.ToString()));


10-13: Don’t rely on enum ToString for titles; use friendly names.

“Control” vs “Ctrl”, “Option” vs “Alt”, etc. Suggest a mapping function for titles.

-		<abbr class="@GetStyles( nameof( S.Abbr ) )" title="@key">@GetKeySymbol(key)</abbr>
+		<abbr class="@GetStyles( nameof( S.Abbr ) )" title="@GetKeyTitle(key)">@GetKeySymbol(key)</abbr>

Implement GetKeyTitle(KeyboardKey key) in the code-behind to return localized, platform-friendly labels.

docs/LumexUI.Docs.Client/Pages/Components/Kbd/Kbd.razor (1)

14-22: Minor copy tweak (optional).

“Class … styles the wrapper of the kbd contents.” Consider “Base class for the wrapper element.” Not blocking.

src/LumexUI/Components/Kbd/KbdSlots.cs (1)

16-27: Seal the slots type to lock the contract.

Prevent inheritance so the slot surface (Base/Abbr/Content) stays stable.

-[ExcludeFromCodeCoverage]
-public class KbdSlots : SlotBase
+[ExcludeFromCodeCoverage]
+public sealed class KbdSlots : SlotBase
src/LumexUI/Styles/Kbd.cs (2)

44-45: Prefer null over empty string for no-op slot.

Prevents merging an empty class token.

-                [nameof( KbdSlots.Content )] = "",
+                [nameof( KbdSlots.Content )] = null,

26-41: Optional: add select-none to avoid accidental text selection.

Minor UX polish; common for KBD pills.

                 [nameof( KbdSlots.Base )] = new ElementClass()
                     .Add( "px-1.5" )
                     .Add( "py-0.5" )
                     .Add( "inline-flex" )
+                    .Add( "select-none" )
                     .Add( "space-x-0.5" )
tests/LumexUI.Tests/Components/Kbd/KbdTests.razor (4)

4-10: Missing TailwindMerge using may break TwMerge registration.

Add TailwindMerge using to ensure TwMerge resolves in this file.

 @using LumexUI.Styles
+@using TailwindMerge

25-26: Remove unused local RenderFragment.

It’s defined but not used.

-        RenderFragment content = @<span data-testid="kbd-children" />;

42-57: Strengthen assertions for multiple keys.

Also assert the number of elements equals key count.

         var root = cut.FindByTestId("kbd-multikeys");
         root.Should().NotBeNull();
@@
         root.InnerHtml.Should().Contain("⌘");
         root.InnerHtml.Should().Contain("⇧");
         root.InnerHtml.Should().Contain("K");
+        cut.FindAll("abbr").Count.Should().Be(2);

12-21: Consider asserting base slot class is applied.

Quick check that Base slot styles resolve (e.g., bg-default-100).

src/LumexUI/Components/Kbd/LumexKbd.razor.cs (2)

34-34: Make _slots read-only to reflect intent.

We don’t mutate entries post-build; expose as IReadOnlyDictionary.

-private Dictionary<string, ComponentSlot> _slots = [];
+private IReadOnlyDictionary<string, ComponentSlot> _slots = new Dictionary<string, ComponentSlot>();

82-94: Throw a more precise exception for missing/unknown slots.

NotImplementedException obscures the cause; prefer KeyNotFound/ArgumentOutOfRange.

-        if( !_slots.TryGetValue( slot, out var styles ) )
-        {
-            throw new NotImplementedException();
-        }
+        if( !_slots.TryGetValue( slot, out var styles ) )
+            throw new KeyNotFoundException( $"Unknown slot '{slot}'." );
@@
-            _ => throw new NotImplementedException()
+            _ => throw new ArgumentOutOfRangeException( nameof( slot ), slot, "Unknown slot." )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fa3575 and df2004c.

📒 Files selected for processing (12)
  • docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs (1 hunks)
  • docs/LumexUI.Docs.Client/Pages/Components/Kbd/Examples/Keys.razor (1 hunks)
  • docs/LumexUI.Docs.Client/Pages/Components/Kbd/Examples/Usage.razor (1 hunks)
  • docs/LumexUI.Docs.Client/Pages/Components/Kbd/Kbd.razor (1 hunks)
  • docs/LumexUI.Docs.Client/Pages/Components/Kbd/PreviewCodes/Keys.razor (1 hunks)
  • docs/LumexUI.Docs.Client/Pages/Components/Kbd/PreviewCodes/Usage.razor (1 hunks)
  • src/LumexUI/Common/Enums/KeyboardKey.cs (1 hunks)
  • src/LumexUI/Components/Kbd/KbdSlots.cs (1 hunks)
  • src/LumexUI/Components/Kbd/LumexKbd.razor (1 hunks)
  • src/LumexUI/Components/Kbd/LumexKbd.razor.cs (1 hunks)
  • src/LumexUI/Styles/Kbd.cs (1 hunks)
  • tests/LumexUI.Tests/Components/Kbd/KbdTests.razor (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/LumexUI/Components/Kbd/KbdSlots.cs (1)
src/LumexUI/Components/Kbd/LumexKbd.razor.cs (1)
  • ExcludeFromCodeCoverage (79-94)
src/LumexUI/Styles/Kbd.cs (2)
src/LumexUI/Components/Kbd/LumexKbd.razor.cs (1)
  • ExcludeFromCodeCoverage (79-94)
src/LumexUI/Utilities/Variants/SlotCollection.cs (1)
  • SlotCollection (9-34)
src/LumexUI/Components/Kbd/LumexKbd.razor.cs (4)
src/LumexUI/Components/Bases/LumexComponentBase.cs (1)
  • LumexComponentBase (18-58)
src/LumexUI/Utilities/Variants/TwVariants.cs (1)
  • ComponentSlot (112-115)
src/LumexUI/Styles/Kbd.cs (1)
  • ExcludeFromCodeCoverage (13-48)
src/LumexUI/Components/Kbd/KbdSlots.cs (1)
  • ExcludeFromCodeCoverage (15-27)
🔇 Additional comments (5)
docs/LumexUI.Docs.Client/Pages/Components/Kbd/PreviewCodes/Usage.razor (1)

1-5: LGTM.

Matches existing preview patterns (rendermode + nested example).

docs/LumexUI.Docs.Client/Pages/Components/Kbd/PreviewCodes/Keys.razor (1)

1-5: LGTM.

Consistent with other preview code pages.

src/LumexUI/Components/Kbd/KbdSlots.cs (1)

11-27: LGTM — shape matches usage in LumexKbd and Styles.Kbd.

src/LumexUI/Components/Kbd/LumexKbd.razor.cs (1)

19-33: A11y: confirm markup uses semantic KBD + accessible labels.

Ensure each has a title or aria-label and symbols are aria-hidden with a text alternative (e.g., sr-only with key name).

src/LumexUI/Styles/Kbd.cs (1)

18-19: Verify DI lifetime of TwMerge is Singleton. ServiceCollectionExtensions.AddTwMerge calls services.AddTailwindMerge(...) (external extension). (github.com) Tests register TwMerge as a singleton (e.g. tests/LumexUI.Tests/Components/Accordion/AccordionTests.cs). Confirm AddTailwindMerge actually registers TwMerge as Singleton; if it does not, register Services.AddSingleton() or adjust/remove any style-caching to avoid surprising consumers.

Comment on lines +16 to +47
private static ComponentVariant? _variant;

public static ComponentVariant Style( TwMerge twMerge )
{
var twVariants = new TwVariants( twMerge );

return _variant ??= twVariants.Create( new VariantConfig()
{
Slots = new SlotCollection
{
[nameof( KbdSlots.Base )] = new ElementClass()
.Add( "px-1.5" )
.Add( "py-0.5" )
.Add( "inline-flex" )
.Add( "space-x-0.5" )
.Add( "rtl:space-x-reverse" )
.Add( "items-center" )
.Add( "font-sans" )
.Add( "font-normal" )
.Add( "text-center" )
.Add( "text-small" )
.Add( "shadow-small" )
.Add( "bg-default-100" )
.Add( "text-foreground-600" )
.Add( "rounded-small" ),

[nameof( KbdSlots.Abbr )] = "no-underline",

[nameof( KbdSlots.Content )] = "",
}
} );
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Cache is bound to first TwMerge instance — make it per‑instance.

Current static cache captures the first TwMerge; different DI lifetimes/configs won’t be respected thereafter.

-using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
@@
-internal static class Kbd
+internal static class Kbd
 {
-    private static ComponentVariant? _variant;
+    private static readonly ConditionalWeakTable<TwMerge, ComponentVariant> _cache = new();
@@
-    public static ComponentVariant Style( TwMerge twMerge )
+    public static ComponentVariant Style( TwMerge twMerge )
     {
-        var twVariants = new TwVariants( twMerge );
-
-        return _variant ??= twVariants.Create( new VariantConfig()
-        {
-            Slots = new SlotCollection
-            {
-                [nameof( KbdSlots.Base )] = new ElementClass()
+        if( !_cache.TryGetValue( twMerge, out var variant ) )
+        {
+            var twVariants = new TwVariants( twMerge );
+            variant = twVariants.Create( new VariantConfig()
+            {
+                Slots = new SlotCollection
+                {
+                    [nameof( KbdSlots.Base )] = new ElementClass()
                         .Add( "px-1.5" )
                         .Add( "py-0.5" )
                         .Add( "inline-flex" )
                         .Add( "space-x-0.5" )
                         .Add( "rtl:space-x-reverse" )
                         .Add( "items-center" )
                         .Add( "font-sans" )
                         .Add( "font-normal" )
                         .Add( "text-center" )
                         .Add( "text-small" )
                         .Add( "shadow-small" )
                         .Add( "bg-default-100" )
                         .Add( "text-foreground-600" )
-                        .Add( "rounded-small" ),
+                        .Add( "rounded-small" ),
 
-                [nameof( KbdSlots.Abbr )] = "no-underline",
+                    [nameof( KbdSlots.Abbr )] = "no-underline",
 
-                [nameof( KbdSlots.Content )] = "",
-            }
-        } );
+                    [nameof( KbdSlots.Content )] = null,
+                }
+            } );
+            _cache.Add( twMerge, variant );
+        }
+        return variant;
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static ComponentVariant? _variant;
public static ComponentVariant Style( TwMerge twMerge )
{
var twVariants = new TwVariants( twMerge );
return _variant ??= twVariants.Create( new VariantConfig()
{
Slots = new SlotCollection
{
[nameof( KbdSlots.Base )] = new ElementClass()
.Add( "px-1.5" )
.Add( "py-0.5" )
.Add( "inline-flex" )
.Add( "space-x-0.5" )
.Add( "rtl:space-x-reverse" )
.Add( "items-center" )
.Add( "font-sans" )
.Add( "font-normal" )
.Add( "text-center" )
.Add( "text-small" )
.Add( "shadow-small" )
.Add( "bg-default-100" )
.Add( "text-foreground-600" )
.Add( "rounded-small" ),
[nameof( KbdSlots.Abbr )] = "no-underline",
[nameof( KbdSlots.Content )] = "",
}
} );
}
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;
internal static class Kbd
{
private static readonly ConditionalWeakTable<TwMerge, ComponentVariant> _cache = new();
public static ComponentVariant Style( TwMerge twMerge )
{
if( !_cache.TryGetValue( twMerge, out var variant ) )
{
var twVariants = new TwVariants( twMerge );
variant = twVariants.Create( new VariantConfig()
{
Slots = new SlotCollection
{
[nameof( KbdSlots.Base )] = new ElementClass()
.Add( "px-1.5" )
.Add( "py-0.5" )
.Add( "inline-flex" )
.Add( "space-x-0.5" )
.Add( "rtl:space-x-reverse" )
.Add( "items-center" )
.Add( "font-sans" )
.Add( "font-normal" )
.Add( "text-center" )
.Add( "text-small" )
.Add( "shadow-small" )
.Add( "bg-default-100" )
.Add( "text-foreground-600" )
.Add( "rounded-small" ),
[nameof( KbdSlots.Abbr )] = "no-underline",
[nameof( KbdSlots.Content )] = null,
}
} );
_cache.Add( twMerge, variant );
}
return variant;
}
}
🤖 Prompt for AI Agents
In src/LumexUI/Styles/Kbd.cs around lines 16 to 47, the static field _variant
caches the ComponentVariant from the first TwMerge instance which prevents
different TwMerge configurations or DI lifetimes from being respected; change
the implementation to avoid a static/module-level cache — either create/return a
fresh variant per call (remove _variant) or maintain the cache per TwMerge
instance (e.g. a dictionary keyed by TwMerge identity or attach variant to
TwMerge) so each TwMerge configuration gets its own ComponentVariant, and ensure
thread-safety if using a shared dictionary.

Copy link
Contributor

@desmondinho desmondinho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fantastic job! I love it :)

I’ve pointed out just a few very small things, mostly related to code style.

Thank you so much for your contribution! ❤️

/// The Space key (Spacebar).
/// </summary>
Space,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also add Fn (fn), Win (Win) and Alt (⎇).

namespace LumexUI;

/// <summary>
/// Represents the slot names for the <see cref="LumexKbd"/>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please adjust this summary, so it follows the current convention (see BadgeSlots for reference)?

Class="@GetStyles( nameof( S.Base ) )"
Style="@RootStyle"
@attributes="AdditionalAttributes">
@foreach (var key in Keys)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file requires formatting.

namespace LumexUI;

/// <summary>
/// <see cref="LumexKbd"/> is a component to display which key or combination of keys performs a given action.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please adjust this summary, so it follows the current convention (see LumexBadge for reference)?

Something like "A component that represents a keyboard key or combination of keys used to perform a specific action."


private static string? GetKeySymbol( KeyboardKey key )
{
// It's better a dictionary?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, let's move it into the separate KeyboardKeys static file in the same folder. You can then move it directly in the razor file -- no need for this method.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/LumexUI/Components/Kbd/LumexKbd.razor.cs (1)

14-16: Summary text matches the convention—nice.

This addresses prior feedback to align with LumexBadge-style summaries.

🧹 Nitpick comments (7)
src/LumexUI/Components/Kbd/KbdConstants.cs (3)

18-42: Make the map read-only to prevent accidental mutation.

Expose an IReadOnlyDictionary to avoid downstream writes and concurrency surprises.

- public static readonly Dictionary<KeyboardKey, string> KeyboardKeys = new()
+ public static IReadOnlyDictionary<KeyboardKey, string> KeyboardKeys { get; } = new Dictionary<KeyboardKey, string>
 {
   [KeyboardKey.Command] = "⌘",
   [KeyboardKey.Shift] = "⇧",
   [KeyboardKey.Control] = "⌃",
   [KeyboardKey.Option] = "⌥",
   [KeyboardKey.Alt] = "⎇",
   [KeyboardKey.Win] = "⊞",
   [KeyboardKey.Fn] = "fn",
   [KeyboardKey.Enter] = "↵",
-  [KeyboardKey.Delete] = "⌫",
+  [KeyboardKey.Delete] = "⌫",
   [KeyboardKey.Escape] = "⎋",
   [KeyboardKey.Tab] = "⇥",
   [KeyboardKey.CapsLock] = "⇪",
   [KeyboardKey.Up] = "↑",
   [KeyboardKey.Right] = "→",
   [KeyboardKey.Down] = "↓",
   [KeyboardKey.Left] = "←",
   [KeyboardKey.PageUp] = "⇞",
   [KeyboardKey.PageDown] = "⇟",
   [KeyboardKey.Home] = "↖",
   [KeyboardKey.End] = "↘",
   [KeyboardKey.Help] = "?",
   [KeyboardKey.Space] = "␣",
- };
+ };

If needed, add: using System.Collections.Generic;


28-28: Confirm intended “Delete” semantics (Backspace vs Forward Delete).

Current glyph “⌫” is typically Backspace (macOS labels this key “Delete”). If you intended Forward Delete, consider “⌦”. Please confirm design choice across platforms before we lock tests/docs.

- [KeyboardKey.Delete] = "⌫",
+ // Use if you intend Forward Delete:
+ [KeyboardKey.Delete] = "⌦",

20-27: Mac-centric control glyphs—verify cross‑platform UX.

Symbols like ⌘, ⌃, ⌥ are Apple conventions; Windows users may expect “Ctrl”, “Alt”, “Win” text. Decide if this map is intentionally mac‑first, or if LumexKbd should offer a platform/style toggle.

src/LumexUI/Components/Kbd/LumexKbd.razor.cs (4)

17-18: Seal the component class.

Prevents unintended inheritance and enables minor runtime optimizations.

-public partial class LumexKbd : LumexComponentBase, ISlotComponent<KbdSlots>
+public sealed partial class LumexKbd : LumexComponentBase, ISlotComponent<KbdSlots>

34-34: Store slots as read-only to reflect usage.

The field isn’t mutated outside assignment; use IReadOnlyDictionary for clearer intent.

-private Dictionary<string, ComponentSlot> _slots = [];
+private IReadOnlyDictionary<string, ComponentSlot> _slots = new Dictionary<string, ComponentSlot>();

44-49: Initialize styles once.

Slots are variant-defined and independent of parameters; initialize in OnInitialized to avoid recomputing each parameter set.

-/// <inheritdoc/>
-protected override void OnParametersSet()
-{
-  var kbd = Styles.Kbd.Style( TwMerge );
-  _slots = kbd();
-}
+protected override void OnInitialized()
+{
+  var kbd = Styles.Kbd.Style( TwMerge );
+  _slots = kbd();
+}

51-66: Use more specific exceptions.

Key not found and unknown slot are not “not implemented” conditions.

 if( !_slots.TryGetValue( slot, out var styles ) )
 {
-  throw new NotImplementedException();
+  throw new KeyNotFoundException( $"Slot '{slot}' not found for {nameof(LumexKbd)}." );
 }
 ...
-      _ => throw new NotImplementedException()
+      _ => throw new ArgumentOutOfRangeException( nameof(slot), slot, "Unknown slot." )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df2004c and c507fce.

📒 Files selected for processing (5)
  • src/LumexUI/Common/Enums/KeyboardKey.cs (1 hunks)
  • src/LumexUI/Components/Kbd/KbdConstants.cs (1 hunks)
  • src/LumexUI/Components/Kbd/KbdSlots.cs (1 hunks)
  • src/LumexUI/Components/Kbd/LumexKbd.razor (1 hunks)
  • src/LumexUI/Components/Kbd/LumexKbd.razor.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/LumexUI/Components/Kbd/KbdSlots.cs
  • src/LumexUI/Common/Enums/KeyboardKey.cs
  • src/LumexUI/Components/Kbd/LumexKbd.razor
🧰 Additional context used
🧬 Code graph analysis (1)
src/LumexUI/Components/Kbd/LumexKbd.razor.cs (4)
src/LumexUI/Components/Bases/LumexComponentBase.cs (1)
  • LumexComponentBase (18-58)
src/LumexUI/Utilities/Variants/TwVariants.cs (1)
  • ComponentSlot (112-115)
src/LumexUI/Components/Kbd/KbdSlots.cs (1)
  • ExcludeFromCodeCoverage (14-26)
src/LumexUI/Styles/Kbd.cs (1)
  • ExcludeFromCodeCoverage (13-48)

Comment on lines +29 to +33
/// <summary>
/// Gets or sets the collection of keys to be used as input for the component.
/// </summary>
[Parameter] public IEnumerable<KeyboardKey> Keys { get; set; } = [];

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Guard against missing glyphs in the .razor markup.

If the markup indexes the dictionary directly (e.g., C.KeyboardKeys[key]), an unmapped enum value will throw. Prefer TryGetValue with a fallback (e.g., key.ToString()).

Suggested markup pattern:

@{ var ok = C.KeyboardKeys.TryGetValue(key, out var g); }
<abbr title="@key">@((ok ? g : key.ToString()))</abbr>

🏁 Script executed:

#!/bin/bash
# Verify no direct indexer use on KeyboardKeys that could throw at runtime.
rg -nP -C2 '(?:KbdConstants|KeyboardKeys)\s*\[' --glob '!**/bin/**' --glob '!**/obj/**'

Length of output: 148


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Searching for 'KeyboardKeys' occurrences..."
rg -n --hidden -S -C2 '\bKeyboardKeys\b' --glob '!**/bin/**' --glob '!**/obj/**' || true
echo "Searching for 'KeyboardKeys[' indexing..."
rg -n --hidden -S -C2 '\bKeyboardKeys\s*\[' --glob '!**/bin/**' --glob '!**/obj/**' || true
echo "Searching for 'KbdConstants' occurrences..."
rg -n --hidden -S -C2 '\bKbdConstants\b' --glob '!**/bin/**' --glob '!**/obj/**' || true
echo "Searching for '.KeyboardKeys[' and 'KeyboardKeys.TryGetValue' patterns..."
rg -n --hidden -S -C2 '\.KeyboardKeys\s*\[' --glob '!**/bin/**' --glob '!**/obj/**' || true
rg -n --hidden -S -C2 'KeyboardKeys\.TryGetValue' --glob '!**/bin/**' --glob '!**/obj/**' || true
echo "Searching for 'C.KeyboardKeys' pattern..."
rg -n --hidden -S -C2 'C\.KeyboardKeys' --glob '!**/bin/**' --glob '!**/obj/**' || true

Length of output: 1139


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "PWD: $(pwd)"
echo "----- top-level listing -----"
ls -la | sed -n '1,200p' || true

echo "----- git info -----"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  echo "Repo root: $(git rev-parse --show-toplevel)"
  echo "HEAD: $(git rev-parse --short HEAD)"
  echo "Tracked files: $(git ls-files | wc -l)"
else
  echo "Not inside a git work tree"
fi

echo "----- git grep (fast, if repo present) -----"
git grep -n --full-name -E 'KeyboardKeys|KbdConstants|C\.KeyboardKeys' || true
git grep -n --full-name -E 'KeyboardKeys\s*\[' || true
git grep -n --full-name 'KeyboardKeys.TryGetValue' || true

echo "----- ripgrep (no VCS ignore) -----"
rg --version || true
rg -n --no-ignore-vcs -S -C2 -F 'KeyboardKeys' --hidden --glob '!**/bin/**' --glob '!**/obj/**' || true
rg -n --no-ignore-vcs -S -C2 -E 'KeyboardKeys\s*\[' --hidden --glob '!**/bin/**' --glob '!**/obj/**' || true
rg -n --no-ignore-vcs -S -C2 'KeyboardKeys\.TryGetValue' --hidden --glob '!**/bin/**' --glob '!**/obj/**' || true
rg -n --no-ignore-vcs -S -C2 'C\.KeyboardKeys' --hidden --glob '!**/bin/**' --glob '!**/obj/**' || true

echo "----- Search .razor and .razor.cs files specifically -----"
find . -type f \( -iname '*.razor' -o -iname '*.razor.cs' \) -print0 | xargs -0 -r rg -n --no-ignore-vcs -S -C2 'KeyboardKeys' || true

echo "----- Find files by name patterns -----"
find . -type f \( -iname '*kbd*' -o -iname '*lumex*' -o -iname '*keyboard*' \) -maxdepth 6 -print || true

TARGET='src/LumexUI/Components/Kbd/LumexKbd.razor.cs'
if [ -f "$TARGET" ]; then
  echo "----- Showing $TARGET (first 200 lines) -----"
  sed -n '1,200p' "$TARGET"
else
  echo "$TARGET not found"
fi

Length of output: 10607


Guard against missing glyphs — stop indexing C.KeyboardKeys directly

Direct dictionary indexing at src/LumexUI/Components/Kbd/LumexKbd.razor:18 will throw for unmapped KeyboardKey values; use TryGetValue with a fallback (e.g., key.ToString()).

@{ var ok = C.KeyboardKeys.TryGetValue(key, out var g); }
@((ok ? g : key.ToString()))

🤖 Prompt for AI Agents
In src/LumexUI/Components/Kbd/LumexKbd.razor.cs around lines 29 to 33, the Keys
property is initialized with an invalid literal ([]) and elsewhere the code
indexes C.KeyboardKeys directly which will throw for unmapped KeyboardKey
values; change the default to Array.Empty<KeyboardKey>() or
Enumerable.Empty<KeyboardKey>() and replace direct dictionary indexing with
C.KeyboardKeys.TryGetValue(key, out var g) and use g when true or key.ToString()
as a fallback when false so missing glyphs don’t cause exceptions.

Copy link
Contributor

@desmondinho desmondinho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

@desmondinho
Copy link
Contributor

Huge thanks for your input! It's priceless. I will merge this PR later today once I get to my PC.

@desmondinho desmondinho merged commit d9dbf24 into LumexUI:main Sep 15, 2025
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants