Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class NavigationStore
.Add( new( nameof( LumexPopover ) ) )
.Add( new( nameof( LumexRadioGroup<T> ) ) )
.Add( new( nameof( LumexSelect<T> ) ) )
.Add( new( nameof( LumexSkeleton ), ComponentStatus.New ) )
.Add( new( nameof( LumexSwitch ) ) )
.Add( new( nameof( LumexTabs ) ) )
.Add( new( nameof( LumexTextbox ) ) );
Expand All @@ -49,6 +50,7 @@ public class NavigationStore
.Add( new( nameof( LumexAccordion ) ) )
.Add( new( nameof( LumexAccordionItem ) ) )
.Add( new( nameof( LumexAvatar ) ) )
.Add( new( nameof( LumexAvatarGroup ) ) )
//.Add( nameof( LumexBooleanInputBase ) )
.Add( new( nameof( LumexButton ) ) )
.Add( new( nameof( LumexCard ) ) )
Expand Down Expand Up @@ -84,6 +86,7 @@ public class NavigationStore
.Add( new( nameof( LumexPopoverTrigger ) ) )
.Add( new( nameof( LumexSelect<T> ) ) )
.Add( new( nameof( LumexSelectItem<T> ) ) )
.Add( new( nameof( LumexSkeleton ) ) )
.Add( new( nameof( LumexSwitch ) ) )
.Add( new( nameof( LumexTab ) ) )
.Add( new( nameof( LumexTabs ) ) )
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<div class="flex flex-col gap-3">
<LumexCard Class="w-[200px] space-y-5 p-4" Radius="@Radius.Large">
<LumexSkeleton Class="rounded-lg" Loading="@_loading">
<div class="h-24 rounded-lg bg-secondary" />
</LumexSkeleton>

<div class="space-y-3">
<LumexSkeleton Class="w-3/5 rounded-lg" Loading="@_loading">
<div class="h-3 w-full rounded-lg bg-secondary" />
</LumexSkeleton>
<LumexSkeleton Class="w-4/5 rounded-lg" Loading="@_loading">
<div class="h-3 w-full rounded-lg bg-secondary-300" />
</LumexSkeleton>
<LumexSkeleton Class="w-2/5 rounded-lg" Loading="@_loading">
<div class="h-3 w-full rounded-lg bg-secondary-200" />
</LumexSkeleton>
</div>
</LumexCard>
<LumexButton Size="@Size.Small"
Variant="@Variant.Flat"
Color="@ThemeColor.Secondary"
OnClick="@(() => _loading = !_loading)">
@(_loading ? "Hide" : "Show") Skeleton
</LumexButton>
</div>

@code {
private bool _loading = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="max-w-[300px] w-full flex items-center gap-3">
<div>
<LumexSkeleton Class="flex rounded-full w-12 h-12" />
</div>
<div class="w-full flex flex-col gap-2">
<LumexSkeleton Class="h-3 w-3/5 rounded-lg" />
<LumexSkeleton Class="h-3 w-4/5 rounded-lg" />
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<LumexCard class="w-[200px] space-y-5 p-4" radius="@Radius.Large">
<LumexSkeleton class="rounded-lg">
<div class="h-24 rounded-lg bg-default-300" />
</LumexSkeleton>
<div class="space-y-3">
<LumexSkeleton class="w-3/5 rounded-lg">
<div class="h-3 w-3/5 rounded-lg bg-default-200" />
</LumexSkeleton>
<LumexSkeleton class="w-4/5 rounded-lg">
<div class="h-3 w-4/5 rounded-lg bg-default-200" />
</LumexSkeleton>
<LumexSkeleton class="w-2/5 rounded-lg">
<div class="h-3 w-2/5 rounded-lg bg-default-300" />
</LumexSkeleton>
</div>
</LumexCard>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@rendermode InteractiveWebAssembly

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

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

<PreviewCode Code="@new(name: null, snippet: "Skeleton.Code.Usage")">
<LumexUI.Docs.Client.Pages.Components.Skeleton.Examples.Usage />
</PreviewCode>
76 changes: 76 additions & 0 deletions docs/LumexUI.Docs.Client/Pages/Components/Skeleton/Skeleton.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
@page "/docs/components/skeleton"
@layout DocsContentLayout

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

<DocsSection Title="Usage">
<p>
The skeleton component wraps content, providing a
loading placeholder until the actual content is ready.
</p>
<Usage />

<DocsSection Title="Standalone">
<p>
The skeleton can also be used as a standalone placeholder to simulate
loading states for specific UI elements like text, images, or buttons.
</p>
<Standalone />
</DocsSection>

<DocsSection Title="Loading">
<p>You can toggle the skeleton visibility dynamically based on a loading state.</p>
<Loading />
</DocsSection>
</DocsSection>

<DocsSlotsSection Slots="@_slots">
<div>
<h4 class="font-semibold">Skeleton</h4>
<ul>
<li><Code>Class</Code>: The CSS class names to style the wrapper.</li>
<li><Code>Classes</Code>: The CSS class names to style the slots.</li>
</ul>
</div>
</DocsSlotsSection>

<DocsApiSection Components="@_apiComponents" />

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

private readonly Heading[] _headings = new Heading[]
{
new("Composition"),
new("Usage", [
new("Standalone"),
new("Loading")
]),
new("Custom Styles"),
new("API")
};

private readonly Slot[] _slots = new Slot[]
{
new(nameof(SkeletonSlots.Base), "The main container for the skeleton component."),
new(nameof(SkeletonSlots.Content), "The inner container that holds the actual content."),
};

private readonly string[] _apiComponents = new string[]
{
nameof(LumexSkeleton),
nameof(LumexCard),
nameof(LumexButton)
};

protected override void OnInitialized()
{
Layout.Initialize(
title: "Skeleton",
category: "Components",
description: "Skeletons are used as loading placeholders, indicating content is being loaded.",
headings: _headings,
linksProps: new ComponentLinksProps( "Skeleton", isServer: true )
);
}
}
15 changes: 15 additions & 0 deletions src/LumexUI/Components/Skeleton/LumexSkeleton.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@namespace LumexUI
@inherits LumexComponentBase

@using S = SkeletonSlots;

<LumexComponent As="@As"
Class="@GetStyles(nameof(S.Base))"
Style="@Style"
data-slot="base"
data-loading="@Loading.ToAttributeValue()"
@attributes="@AdditionalAttributes">
<div class="@GetStyles(nameof(S.Content))" data-slot="content">
@ChildContent
</div>
</LumexComponent>
61 changes: 61 additions & 0 deletions src/LumexUI/Components/Skeleton/LumexSkeleton.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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 skeleton loader for displaying a placeholder while content is loading.
/// </summary>
public partial class LumexSkeleton : LumexComponentBase, ISlotComponent<SkeletonSlots>
{
/// <summary>
/// Gets or sets the content to render inside the skeleton.
/// </summary>
[Parameter] public RenderFragment? ChildContent { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the skeleton is in a loading state.
/// </summary>
/// <remarks>
/// The default value is <see langword="true"/>.
/// </remarks>
[Parameter] public bool Loading { get; set; } = true;

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

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

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

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

return slot switch
{
nameof( SkeletonSlots.Base ) => styles( Classes?.Base, Class ),
nameof( SkeletonSlots.Content ) => styles( Classes?.Content ),
_ => throw new NotImplementedException()
};
}
}
33 changes: 33 additions & 0 deletions src/LumexUI/Components/Skeleton/SkeletonSlots.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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="LumexSkeleton"/>,
/// used to assign CSS classes to different parts of the component.
/// </summary>
[ExcludeFromCodeCoverage]
public class SkeletonSlots : ISlot
{
/// <summary>
/// Gets or sets the CSS class for the root slot.
/// </summary>
[Obsolete( "Deprecated. This will be removed in the future releases. Use the 'Base' slot instead." )]
public string? Root { get; }

/// <summary>
/// Gets or sets the CSS class for the base slot.
/// </summary>
public string? Base { get; set; }

/// <summary>
/// Gets or sets the CSS class for the content slot.
/// </summary>
public string? Content { get; set; }
}
69 changes: 69 additions & 0 deletions src/LumexUI/Styles/Skeleton.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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 Skeleton
{
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( SkeletonSlots.Base )] = new ElementClass()
.Add( "group" )
.Add( "relative" )
.Add( "bg-surface3" )
.Add( "overflow-hidden" )
.Add( "pointer-events-none" )
// before
.Add( "before:absolute" )
.Add( "before:inset-0" )
.Add( "before:opacity-100" )
.Add( "before:-translate-x-full" )
.Add( "before:bg-gradient-to-r" )
.Add( "before:from-transparent" )
.Add( "before:via-default-300" )
.Add( "before:to-transparent" )
.Add( "before:animate-shimmer" )
//after
.Add( "after:-z-10" )
.Add( "after:absolute" )
.Add( "after:inset-0" )
.Add( "after:opacity-100" )
.Add( "after:bg-surface3" )
// state
.Add( "data-[loading=false]:pointer-events-auto" )
.Add( "data-[loading=false]:overflow-visible" )
.Add( "data-[loading=false]:bg-transparent" )
.Add( "data-[loading=false]:before:animate-none" )
.Add( "data-[loading=false]:before:opacity-0" )
.Add( "data-[loading=false]:after:opacity-0" )
// transition
.Add( "duration-300" )
.Add( "transition-background" ),

[nameof( SkeletonSlots.Content )] = new ElementClass()
.Add( "opacity-0" )
.Add( "group-data-[loading=false]:opacity-100" )
// transition
.Add( "duration-300" )
.Add( "transition-opacity" )
.Add( "motion-reduce:transition-none" )
}
} );
}
}
7 changes: 7 additions & 0 deletions src/LumexUI/Styles/_theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@

/* Animations */
--animate-enter: enter 200ms ease-out normal both;
--animate-shimmer: shimmer 2s infinite;

@keyframes enter {
0% {
Expand All @@ -180,6 +181,12 @@
}
}

@keyframes shimmer {
100% {
translate: 100%;
}
}

/* Override Defaults */
--default-transition-duration: 250ms;
}
Expand Down
Loading
Loading