Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class NavigationStore
.Add( new( nameof( LumexDataGrid<T> ) ) )
.Add( new( nameof( LumexDivider ) ) )
.Add( new( nameof( LumexDropdown ) ) )
.Add( new( nameof( LumexKbd ), PageStatus.New ) )
.Add( new( nameof( LumexLink ) ) )
.Add( new( nameof( LumexListbox<T> ) ) )
.Add( new( nameof( LumexNavbar ) ) )
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<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>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<LumexKbd Keys="@([KeyboardKey.Command])">K</LumexKbd>
59 changes: 59 additions & 0 deletions docs/LumexUI.Docs.Client/Pages/Components/Kbd/Kbd.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@page "/docs/components/kbd"
@layout DocsContentLayout

@using LumexUI.Docs.Client.Pages.Components.Kbd.PreviewCodes

<DocsSection Title="Usage">
<Usage />

<DocsSection Title="Keys">
<Keys />
</DocsSection>
</DocsSection>

<DocsSlotsSection Slots="@_slots" >
<div>
<h4 class="font-semibold">Kbd</h4>
<ul>
<li><code>Class</code>: The basic CSS class name styles the wrapper of the kbd contents.</li>
<li><code>Classes</code>: The CSS class names for the kbd slots style the entire kbd at once.</li>
</ul>
</div>
</DocsSlotsSection>

<DocsApiSection Components="@_apiComponents" />

@code {
[CascadingParameter] private DocsContentLayout Layout { get; set; } = default!;

private readonly Heading[] _headings = new Heading[]
{
new("Usage", [
new("Keys")
]),
new("API")
};

private readonly Slot[] _slots = new Slot[]
{
new(nameof( KbdSlots.Base ), "Kbd wrapper, it handles alignment, placement, and general appearance."),
new(nameof( KbdSlots.Abbr ), "The keys wrapper that handles the appearance of the keys."),
new(nameof( KbdSlots.Content ), "The children wrapper that handles the appearance of the content."),
};

private readonly string[] _apiComponents = new string[]
{
nameof(LumexKbd)
};

protected override void OnInitialized()
{
Layout.Initialize(
title: "Kbd",
category: "Components",
description: "Keyboard key component for displaying keyboard shortcuts and input combinations.",
headings: _headings,
linksProps: new ComponentLinksProps("Kbd", isServer: true)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@rendermode InteractiveWebAssembly

<PreviewCode Code="@new(name: null, snippet: "Kbd.Code.Keys")">
<LumexUI.Docs.Client.Pages.Components.Kbd.Examples.Keys />
</PreviewCode>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@rendermode InteractiveWebAssembly

<PreviewCode Code="@new(name: null, snippet: "Kbd.Code.Usage")">
<LumexUI.Docs.Client.Pages.Components.Kbd.Examples.Usage />
</PreviewCode>
106 changes: 106 additions & 0 deletions src/LumexUI/Common/Enums/KeyboardKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) LumexUI 2024
// LumexUI licenses this file to you under the MIT license
// See the license here https://github.com/LumexUI/lumexui/blob/main/LICENSE

namespace LumexUI.Common;

/// <summary>
/// Represents common keyboard keys for UI interactions.
/// </summary>
public enum KeyboardKey
{
/// <summary>
/// The Command key (⌘), commonly found on Apple keyboards.
/// </summary>
Command,

/// <summary>
/// The Shift key.
/// </summary>
Shift,

/// <summary>
/// The Control key (Ctrl).
/// </summary>
Control,

/// <summary>
/// The Option key (⌥), commonly found on Apple keyboards.
/// </summary>
Option,

/// <summary>
/// The Enter key.
/// </summary>
Enter,

/// <summary>
/// The Delete key.
/// </summary>
Delete,

/// <summary>
/// The Escape key (Esc).
/// </summary>
Escape,

/// <summary>
/// The Tab key.
/// </summary>
Tab,

/// <summary>
/// The Caps Lock key.
/// </summary>
CapsLock,

/// <summary>
/// The Up Arrow key.
/// </summary>
Up,

/// <summary>
/// The Right Arrow key.
/// </summary>
Right,

/// <summary>
/// The Down Arrow key.
/// </summary>
Down,

/// <summary>
/// The Left Arrow key.
/// </summary>
Left,

/// <summary>
/// The Page Up key.
/// </summary>
PageUp,

/// <summary>
/// The Page Down key.
/// </summary>
PageDown,

/// <summary>
/// The Home key.
/// </summary>
Home,

/// <summary>
/// The End key.
/// </summary>
End,

/// <summary>
/// The Help key.
/// </summary>
Help,

/// <summary>
/// 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 (⎇).

27 changes: 27 additions & 0 deletions src/LumexUI/Components/Kbd/KbdSlots.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) LumexUI 2024
// LumexUI licenses this file to you under the MIT license
// See the license here https://github.com/LumexUI/lumexui/blob/main/LICENSE

using System.Diagnostics.CodeAnalysis;

using LumexUI.Common;

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)?

/// used to assign CSS classes to different parts of the component.
/// </summary>
[ExcludeFromCodeCoverage]
public class KbdSlots : SlotBase
{
/// <summary>
/// Gets or sets the CSS class for the abbr slot.
/// </summary>
public string? Abbr { get; set; }

/// <summary>
/// Gets or sets the CSS class for the content slot.
/// </summary>
public string? Content { get; set; }
}
16 changes: 16 additions & 0 deletions src/LumexUI/Components/Kbd/LumexKbd.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@namespace LumexUI
@inherits LumexComponentBase

@using S = KbdSlots;

<LumexComponent As="@As"
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.

{
<abbr class="@GetStyles( nameof( S.Abbr ) )" title="@key">@GetKeySymbol(key)</abbr>
}

<span class="@GetStyles( nameof( S.Content ) )">@ChildContent</span>
</LumexComponent>
95 changes: 95 additions & 0 deletions src/LumexUI/Components/Kbd/LumexKbd.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) LumexUI 2024
// LumexUI licenses this file to you under the MIT license
// See the license here https://github.com/LumexUI/lumexui/blob/main/LICENSE

using System.Diagnostics.CodeAnalysis;

using LumexUI.Common;
using LumexUI.Utilities;

using Microsoft.AspNetCore.Components;

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."

/// </summary>
public partial class LumexKbd : LumexComponentBase, ISlotComponent<KbdSlots>
{
/// <summary>
/// Gets or sets the content to be rendered inside the component.
/// </summary>
[Parameter] public RenderFragment? ChildContent { get; set; }

/// <summary>
/// Gets or sets the CSS class names for the kbd slots.
/// </summary>
[Parameter] public KbdSlots? Classes { get; set; }

/// <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.

private Dictionary<string, ComponentSlot> _slots = [];

/// <summary>
/// Initializes a new instance of the <see cref="LumexKbd"/> class, representing a keyboard element.
/// </summary>
public LumexKbd()
{
As = "kbd";
}

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

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.

return key switch
{
KeyboardKey.Command => "⌘",
KeyboardKey.Shift => "⇧",
KeyboardKey.Control => "⌃",
KeyboardKey.Option => "⌥",
KeyboardKey.Enter => "↵",
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 => "␣",
_ => null
};
}

[ExcludeFromCodeCoverage]
private string? GetStyles( string slot )
{
if( !_slots.TryGetValue( slot, out var styles ) )
{
throw new NotImplementedException();
}

return slot switch
{
nameof( KbdSlots.Base ) => styles( Classes?.Base, Class ),
nameof( KbdSlots.Abbr ) => styles( Classes?.Abbr ),
nameof( KbdSlots.Content ) => styles( Classes?.Content ),
_ => throw new NotImplementedException()
};
}
}
48 changes: 48 additions & 0 deletions src/LumexUI/Styles/Kbd.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) LumexUI 2024
// LumexUI licenses this file to you under the MIT license
// See the license here https://github.com/LumexUI/lumexui/blob/main/LICENSE

using System.Diagnostics.CodeAnalysis;

using LumexUI.Utilities;

using TailwindMerge;

namespace LumexUI.Styles;

[ExcludeFromCodeCoverage]
internal static class Kbd
{
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 )] = "",
}
} );
}
Comment on lines +16 to +47
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.

}
Loading
Loading