Skip to content

Commit 4ae1e08

Browse files
authored
feat(components): introduce Skeleton component (#202)
* feat(skeleton): add baseline implementation of the component * feat(skeleton): add slots and styles * feat(skeleton): add XML summaries * fix(skeleton): return back `after` pseudo CSS classes to prevent flickering on state change * docs(skeleton): add Skeleton page * test(skeleton): add tests * docs(skeleton): fix Loading example button text
1 parent 28a755f commit 4ae1e08

File tree

14 files changed

+352
-0
lines changed

14 files changed

+352
-0
lines changed

docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class NavigationStore
4040
.Add( new( nameof( LumexPopover ) ) )
4141
.Add( new( nameof( LumexRadioGroup<T> ) ) )
4242
.Add( new( nameof( LumexSelect<T> ) ) )
43+
.Add( new( nameof( LumexSkeleton ), ComponentStatus.New ) )
4344
.Add( new( nameof( LumexSwitch ) ) )
4445
.Add( new( nameof( LumexTabs ) ) )
4546
.Add( new( nameof( LumexTextbox ) ) );
@@ -49,6 +50,7 @@ public class NavigationStore
4950
.Add( new( nameof( LumexAccordion ) ) )
5051
.Add( new( nameof( LumexAccordionItem ) ) )
5152
.Add( new( nameof( LumexAvatar ) ) )
53+
.Add( new( nameof( LumexAvatarGroup ) ) )
5254
//.Add( nameof( LumexBooleanInputBase ) )
5355
.Add( new( nameof( LumexButton ) ) )
5456
.Add( new( nameof( LumexCard ) ) )
@@ -84,6 +86,7 @@ public class NavigationStore
8486
.Add( new( nameof( LumexPopoverTrigger ) ) )
8587
.Add( new( nameof( LumexSelect<T> ) ) )
8688
.Add( new( nameof( LumexSelectItem<T> ) ) )
89+
.Add( new( nameof( LumexSkeleton ) ) )
8790
.Add( new( nameof( LumexSwitch ) ) )
8891
.Add( new( nameof( LumexTab ) ) )
8992
.Add( new( nameof( LumexTabs ) ) )
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<div class="flex flex-col gap-3">
2+
<LumexCard Class="w-[200px] space-y-5 p-4" Radius="@Radius.Large">
3+
<LumexSkeleton Class="rounded-lg" Loading="@_loading">
4+
<div class="h-24 rounded-lg bg-secondary" />
5+
</LumexSkeleton>
6+
7+
<div class="space-y-3">
8+
<LumexSkeleton Class="w-3/5 rounded-lg" Loading="@_loading">
9+
<div class="h-3 w-full rounded-lg bg-secondary" />
10+
</LumexSkeleton>
11+
<LumexSkeleton Class="w-4/5 rounded-lg" Loading="@_loading">
12+
<div class="h-3 w-full rounded-lg bg-secondary-300" />
13+
</LumexSkeleton>
14+
<LumexSkeleton Class="w-2/5 rounded-lg" Loading="@_loading">
15+
<div class="h-3 w-full rounded-lg bg-secondary-200" />
16+
</LumexSkeleton>
17+
</div>
18+
</LumexCard>
19+
<LumexButton Size="@Size.Small"
20+
Variant="@Variant.Flat"
21+
Color="@ThemeColor.Secondary"
22+
OnClick="@(() => _loading = !_loading)">
23+
@(_loading ? "Hide" : "Show") Skeleton
24+
</LumexButton>
25+
</div>
26+
27+
@code {
28+
private bool _loading = true;
29+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div class="max-w-[300px] w-full flex items-center gap-3">
2+
<div>
3+
<LumexSkeleton Class="flex rounded-full w-12 h-12" />
4+
</div>
5+
<div class="w-full flex flex-col gap-2">
6+
<LumexSkeleton Class="h-3 w-3/5 rounded-lg" />
7+
<LumexSkeleton Class="h-3 w-4/5 rounded-lg" />
8+
</div>
9+
</div>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<LumexCard class="w-[200px] space-y-5 p-4" radius="@Radius.Large">
2+
<LumexSkeleton class="rounded-lg">
3+
<div class="h-24 rounded-lg bg-default-300" />
4+
</LumexSkeleton>
5+
<div class="space-y-3">
6+
<LumexSkeleton class="w-3/5 rounded-lg">
7+
<div class="h-3 w-3/5 rounded-lg bg-default-200" />
8+
</LumexSkeleton>
9+
<LumexSkeleton class="w-4/5 rounded-lg">
10+
<div class="h-3 w-4/5 rounded-lg bg-default-200" />
11+
</LumexSkeleton>
12+
<LumexSkeleton class="w-2/5 rounded-lg">
13+
<div class="h-3 w-2/5 rounded-lg bg-default-300" />
14+
</LumexSkeleton>
15+
</div>
16+
</LumexCard>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@rendermode InteractiveWebAssembly
2+
3+
<PreviewCode Code="@new(name: null, snippet: "Skeleton.Code.Loading")">
4+
<LumexUI.Docs.Client.Pages.Components.Skeleton.Examples.Loading />
5+
</PreviewCode>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@rendermode InteractiveWebAssembly
2+
3+
<PreviewCode Code="@new(name: null, snippet: "Skeleton.Code.Standalone")">
4+
<LumexUI.Docs.Client.Pages.Components.Skeleton.Examples.Standalone />
5+
</PreviewCode>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@rendermode InteractiveWebAssembly
2+
3+
<PreviewCode Code="@new(name: null, snippet: "Skeleton.Code.Usage")">
4+
<LumexUI.Docs.Client.Pages.Components.Skeleton.Examples.Usage />
5+
</PreviewCode>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
@page "/docs/components/skeleton"
2+
@layout DocsContentLayout
3+
4+
@using LumexUI.Docs.Client.Pages.Components.Skeleton.PreviewCodes
5+
6+
<DocsSection Title="Usage">
7+
<p>
8+
The skeleton component wraps content, providing a
9+
loading placeholder until the actual content is ready.
10+
</p>
11+
<Usage />
12+
13+
<DocsSection Title="Standalone">
14+
<p>
15+
The skeleton can also be used as a standalone placeholder to simulate
16+
loading states for specific UI elements like text, images, or buttons.
17+
</p>
18+
<Standalone />
19+
</DocsSection>
20+
21+
<DocsSection Title="Loading">
22+
<p>You can toggle the skeleton visibility dynamically based on a loading state.</p>
23+
<Loading />
24+
</DocsSection>
25+
</DocsSection>
26+
27+
<DocsSlotsSection Slots="@_slots">
28+
<div>
29+
<h4 class="font-semibold">Skeleton</h4>
30+
<ul>
31+
<li><Code>Class</Code>: The CSS class names to style the wrapper.</li>
32+
<li><Code>Classes</Code>: The CSS class names to style the slots.</li>
33+
</ul>
34+
</div>
35+
</DocsSlotsSection>
36+
37+
<DocsApiSection Components="@_apiComponents" />
38+
39+
@code {
40+
[CascadingParameter] private DocsContentLayout Layout { get; set; } = default!;
41+
42+
private readonly Heading[] _headings = new Heading[]
43+
{
44+
new("Composition"),
45+
new("Usage", [
46+
new("Standalone"),
47+
new("Loading")
48+
]),
49+
new("Custom Styles"),
50+
new("API")
51+
};
52+
53+
private readonly Slot[] _slots = new Slot[]
54+
{
55+
new(nameof(SkeletonSlots.Base), "The main container for the skeleton component."),
56+
new(nameof(SkeletonSlots.Content), "The inner container that holds the actual content."),
57+
};
58+
59+
private readonly string[] _apiComponents = new string[]
60+
{
61+
nameof(LumexSkeleton),
62+
nameof(LumexCard),
63+
nameof(LumexButton)
64+
};
65+
66+
protected override void OnInitialized()
67+
{
68+
Layout.Initialize(
69+
title: "Skeleton",
70+
category: "Components",
71+
description: "Skeletons are used as loading placeholders, indicating content is being loaded.",
72+
headings: _headings,
73+
linksProps: new ComponentLinksProps( "Skeleton", isServer: true )
74+
);
75+
}
76+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
@namespace LumexUI
2+
@inherits LumexComponentBase
3+
4+
@using S = SkeletonSlots;
5+
6+
<LumexComponent As="@As"
7+
Class="@GetStyles(nameof(S.Base))"
8+
Style="@Style"
9+
data-slot="base"
10+
data-loading="@Loading.ToAttributeValue()"
11+
@attributes="@AdditionalAttributes">
12+
<div class="@GetStyles(nameof(S.Content))" data-slot="content">
13+
@ChildContent
14+
</div>
15+
</LumexComponent>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) LumexUI 2024
2+
// LumexUI licenses this file to you under the MIT license
3+
// See the license here https://github.com/LumexUI/lumexui/blob/main/LICENSE
4+
5+
using System.Diagnostics.CodeAnalysis;
6+
7+
using LumexUI.Common;
8+
using LumexUI.Utilities;
9+
10+
using Microsoft.AspNetCore.Components;
11+
12+
namespace LumexUI;
13+
14+
/// <summary>
15+
/// A component that represents a skeleton loader for displaying a placeholder while content is loading.
16+
/// </summary>
17+
public partial class LumexSkeleton : LumexComponentBase, ISlotComponent<SkeletonSlots>
18+
{
19+
/// <summary>
20+
/// Gets or sets the content to render inside the skeleton.
21+
/// </summary>
22+
[Parameter] public RenderFragment? ChildContent { get; set; }
23+
24+
/// <summary>
25+
/// Gets or sets a value indicating whether the skeleton is in a loading state.
26+
/// </summary>
27+
/// <remarks>
28+
/// The default value is <see langword="true"/>.
29+
/// </remarks>
30+
[Parameter] public bool Loading { get; set; } = true;
31+
32+
/// <summary>
33+
/// Gets or sets the CSS class names for the skeleton slots.
34+
/// </summary>
35+
[Parameter] public SkeletonSlots? Classes { get; set; }
36+
37+
private Dictionary<string, ComponentSlot> _slots = [];
38+
39+
/// <inheritdoc />
40+
protected override void OnParametersSet()
41+
{
42+
var skeleton = Styles.Skeleton.Style( TwMerge );
43+
_slots = skeleton();
44+
}
45+
46+
[ExcludeFromCodeCoverage]
47+
private string? GetStyles( string slot )
48+
{
49+
if( !_slots.TryGetValue( slot, out var styles ) )
50+
{
51+
throw new NotImplementedException();
52+
}
53+
54+
return slot switch
55+
{
56+
nameof( SkeletonSlots.Base ) => styles( Classes?.Base, Class ),
57+
nameof( SkeletonSlots.Content ) => styles( Classes?.Content ),
58+
_ => throw new NotImplementedException()
59+
};
60+
}
61+
}

0 commit comments

Comments
 (0)