diff --git a/docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs b/docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs index 652c86cd..7a633565 100644 --- a/docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs +++ b/docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs @@ -48,7 +48,8 @@ public class NavigationStore .Add( new( nameof( LumexSwitch ) ) ) .Add( new( nameof( LumexTabs ) ) ) .Add( new( nameof( LumexTextbox ) ) ) - .Add( new( nameof( LumexTooltip ), PageStatus.New ) ); + .Add( new( nameof( LumexTooltip ), PageStatus.New ) ) + .Add( new( nameof( LumexUser ), PageStatus.New ) ); private static NavigationCategory ComponentsApiCategory => new NavigationCategory( "Components API" ) @@ -106,7 +107,8 @@ public class NavigationStore .Add( new( nameof( LumexTabs ) ) ) .Add( new( nameof( LumexTextbox ) ) ) .Add( new( nameof( LumexThemeProvider ) ) ) - .Add( new( nameof( LumexTooltip ) ) ); + .Add( new( nameof( LumexTooltip ) ) ) + .Add( new( nameof( LumexUser ) ) ); public static Navigation GetNavigation() { diff --git a/docs/LumexUI.Docs.Client/Pages/Components/User/Examples/CustomDescription.razor b/docs/LumexUI.Docs.Client/Pages/Components/User/Examples/CustomDescription.razor new file mode 100644 index 00000000..47a1117a --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/User/Examples/CustomDescription.razor @@ -0,0 +1,10 @@ + + + + + + + @@desmondinho + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/User/Examples/Usage.razor b/docs/LumexUI.Docs.Client/Pages/Components/User/Examples/Usage.razor new file mode 100644 index 00000000..34cebb53 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/User/Examples/Usage.razor @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/docs/LumexUI.Docs.Client/Pages/Components/User/PreviewCodes/CustomDescription.razor b/docs/LumexUI.Docs.Client/Pages/Components/User/PreviewCodes/CustomDescription.razor new file mode 100644 index 00000000..1fdf3541 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/User/PreviewCodes/CustomDescription.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/User/PreviewCodes/Usage.razor b/docs/LumexUI.Docs.Client/Pages/Components/User/PreviewCodes/Usage.razor new file mode 100644 index 00000000..5f354443 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/User/PreviewCodes/Usage.razor @@ -0,0 +1,5 @@ +@rendermode InteractiveWebAssembly + + + + diff --git a/docs/LumexUI.Docs.Client/Pages/Components/User/User.razor b/docs/LumexUI.Docs.Client/Pages/Components/User/User.razor new file mode 100644 index 00000000..1e6e3eb9 --- /dev/null +++ b/docs/LumexUI.Docs.Client/Pages/Components/User/User.razor @@ -0,0 +1,70 @@ +@page "/docs/components/user" +@layout DocsContentLayout + +@using LumexUI.Docs.Client.Pages.Components.User.PreviewCodes + + + + + +

Use the DescriptionContent parameter to provide custom content for the user description.

+ +
+ + + + When both Description and DescriptionContent are provided, DescriptionContent takes precedence. + + +
+ + +
+

User

+ +
+
+ + + +@code { + [CascadingParameter] private DocsContentLayout Layout { get; set; } = default!; + + private readonly Heading[] _headings = new Heading[] + { + new("Usage", [ + new("Custom Description") + ]), + new("Custom Styles"), + new("API") + }; + + private readonly Slot[] _slots = new Slot[] + { + new(nameof( UserSlots.Base ), "The base slot of the user, it is the main container."), + new(nameof( UserSlots.Wrapper ), "The name and description wrapper."), + new(nameof( UserSlots.Name ), "The name of the user."), + new(nameof( UserSlots.Description ), "The description of the user."), + }; + + private readonly string[] _apiComponents = new string[] + { + nameof(LumexUser), + nameof(LumexAvatar), + nameof(LumexLink) + }; + + protected override void OnInitialized() + { + Layout.Initialize( + title: "User", + category: "Components", + description: "Display user information with avatar and name.", + headings: _headings, + linksProps: new ComponentLinksProps( "User", isServer: false ) + ); + } +} diff --git a/src/LumexUI/Components/User/LumexUser.razor b/src/LumexUI/Components/User/LumexUser.razor new file mode 100644 index 00000000..18d41bb6 --- /dev/null +++ b/src/LumexUI/Components/User/LumexUser.razor @@ -0,0 +1,29 @@ +@namespace LumexUI +@inherits LumexComponentBase + +@using S = UserSlots; + + + @AvatarContent +
+ + + @Name + + + + @if ( DescriptionContent is not null ) + { + @DescriptionContent + } + else + { + @Description + } + +
+
\ No newline at end of file diff --git a/src/LumexUI/Components/User/LumexUser.razor.cs b/src/LumexUI/Components/User/LumexUser.razor.cs new file mode 100644 index 00000000..190791ac --- /dev/null +++ b/src/LumexUI/Components/User/LumexUser.razor.cs @@ -0,0 +1,78 @@ +// 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; + +/// +/// A component that represents user information, such as an avatar, name, and email. +/// +public partial class LumexUser : LumexComponentBase, ISlotComponent +{ + /// + /// Gets or sets the content to render when using a custom description element. + /// + [Parameter] public RenderFragment? DescriptionContent { get; set; } + + /// + /// Gets or sets the content to render for the avatar element. + /// + [Parameter] public RenderFragment? AvatarContent { get; set; } + + /// + /// Gets or sets the name of the user. + /// + [Parameter] public string? Name { get; set; } + + /// + /// Gets or sets the description of the user. + /// + [Parameter] public string? Description { get; set; } + + /// + /// Gets or sets the whether the user is focusable. + /// + [Parameter] public bool IsFocusable { get; set; } + + /// + /// Gets or sets the CSS class names for the user slots. + /// + [Parameter] public UserSlots? Classes { get; set; } + + private Dictionary _slots = []; + + /// + protected override void OnParametersSet() + { + var user = Styles.User.Style( TwMerge ); + _slots = user( new() + { + [nameof( IsFocusable )] = IsFocusable.ToString(), + } ); + } + + [ExcludeFromCodeCoverage] + private string? GetStyles( string slot ) + { + if( !_slots.TryGetValue( slot, out var styles ) ) + { + throw new NotImplementedException(); + } + + return slot switch + { + nameof( UserSlots.Base ) => styles( Classes?.Base, Class ), + nameof( UserSlots.Wrapper ) => styles( Classes?.Wrapper ), + nameof( UserSlots.Name ) => styles( Classes?.Name ), + nameof( UserSlots.Description ) => styles( Classes?.Description ), + _ => throw new NotImplementedException() + }; + } +} \ No newline at end of file diff --git a/src/LumexUI/Components/User/UserSlots.cs b/src/LumexUI/Components/User/UserSlots.cs new file mode 100644 index 00000000..dd8a1152 --- /dev/null +++ b/src/LumexUI/Components/User/UserSlots.cs @@ -0,0 +1,31 @@ +// 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; + +/// +/// Represents the set of customizable slots for the component. +/// +[ExcludeFromCodeCoverage] +public class UserSlots : SlotBase +{ + /// + /// Gets or sets the CSS class for the wrapper slot. + /// + public string? Wrapper { get; set; } + + /// + /// Gets or sets the CSS class for the name slot. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the CSS class for the description slot. + /// + public string? Description { get; set; } +} diff --git a/src/LumexUI/Styles/User.cs b/src/LumexUI/Styles/User.cs new file mode 100644 index 00000000..dd1378fc --- /dev/null +++ b/src/LumexUI/Styles/User.cs @@ -0,0 +1,54 @@ +// 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 User +{ + 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(UserSlots.Base)] = new ElementClass() + .Add( "inline-flex" ) + .Add( "items-center" ) + .Add( "justify-center" ) + .Add( "gap-2" ) + .Add( "rounded-small" ) + .Add( "outline-solid" ) + .Add( "outline-transparent" ), + + [nameof(UserSlots.Wrapper)] = "inline-flex flex-col items-start", + + [nameof(UserSlots.Name)] = "text-small text-inherit", + + [nameof(UserSlots.Description)] = "text-tiny text-foreground-400", + }, + + Variants = new VariantCollection + { + [nameof(LumexUser.IsFocusable)] = new VariantValueCollection + { + [bool.TrueString] = new SlotCollection + { + [nameof(UserSlots.Base)] = Utils.FocusVisible, + } + } + } + } ); + } +} diff --git a/tests/LumexUI.Tests/Components/User/UserTests.razor b/tests/LumexUI.Tests/Components/User/UserTests.razor new file mode 100644 index 00000000..6c50f923 --- /dev/null +++ b/tests/LumexUI.Tests/Components/User/UserTests.razor @@ -0,0 +1,46 @@ +@namespace LumexUI.Tests.Components +@inherits TestContext + +@code { + public UserTests() + { + Services.AddSingleton(); + } + + [Fact] + public void ShouldRenderCorrectly() + { + var action = () => Render( + @ + ); + + action.Should().NotThrow(); + } + + [Fact] + public void ShouldRenderCorrectlyWithDescription() + { + var action = () => Render( + @ + ); + + action.Should().NotThrow(); + } + + [Fact] + public void ShouldRenderDescriptionContent() + { + var cut = Render( + @ + + + @@desmondinho + + + + ); + + cut.FindByTestId( "user-root" ).Should().NotBeNull(); + cut.FindByTestId( "user-link" ).Should().NotBeNull(); + } +}