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>
121 changes: 121 additions & 0 deletions src/LumexUI/Common/Enums/KeyboardKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// 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 Alt key (⎇), commonly found on Windows and Linux keyboards.
/// </summary>
Alt,

/// <summary>
/// The Windows key (⊞).
/// </summary>
Win,

/// <summary>
/// The Function (fn) key, often found on laptops.
/// </summary>
Fn,

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

43 changes: 43 additions & 0 deletions src/LumexUI/Components/Kbd/KbdConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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 LumexUI.Common;

namespace LumexUI;

/// <summary>
/// Provides a collection of constants for mapping keyboard keys to their corresponding display symbols.
/// </summary>
public static class KbdConstants
{
/// <summary>
/// Provides a mapping of keyboard keys to their corresponding display symbols for use in user interfaces or
/// documentation.
/// </summary>
public static readonly Dictionary<KeyboardKey, string> KeyboardKeys = new()
{
[KeyboardKey.Command] = "⌘",
[KeyboardKey.Shift] = "⇧",
[KeyboardKey.Control] = "⌃",
[KeyboardKey.Option] = "⌥",
[KeyboardKey.Alt] = "⎇",
[KeyboardKey.Win] = "⊞",
[KeyboardKey.Fn] = "fn",
[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] = "␣",
};
}
26 changes: 26 additions & 0 deletions src/LumexUI/Components/Kbd/KbdSlots.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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 set of customizable slots for the <see cref="LumexKbd"/> 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; }
}
27 changes: 27 additions & 0 deletions src/LumexUI/Components/Kbd/LumexKbd.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@namespace LumexUI
@inherits LumexComponentBase

@using S = KbdSlots;
@using C = KbdConstants;

<LumexComponent As="@As"
Class="@GetStyles( nameof( S.Base ) )"
Style="@RootStyle"
data-slot="kbd"
@attributes="AdditionalAttributes">

@foreach ( var key in Keys )
{
<abbr class="@GetStyles( nameof( S.Abbr ) )"
title="@key"
data-slot="abbr">
@C.KeyboardKeys[key]
</abbr>
}

<span class="@GetStyles( nameof( S.Content ) )"
data-slot="content">
@ChildContent
</span>

</LumexComponent>
67 changes: 67 additions & 0 deletions src/LumexUI/Components/Kbd/LumexKbd.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// 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>
/// 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();
}

[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()
};
}
}
Loading
Loading