Skip to content

Commit

Permalink
feat(components): add card component (#13)
Browse files Browse the repository at this point in the history
* feat(components): add the card component

* feat(components): add the card body component

* feat(components): add the card footer component

* feat(components): add the card header component

* feat(theme): add Radius configuration in the layout config

* feat(theme): add Shadow configuration in the layout config

* feat(components): add styles for the card component

* docs(theme): update the props for extended theme in the tailwind config

* test(components): add tests for the card component

* chore(components): update theme generation in the theme provider component

* chore(components): update button styles

* chore(components): nits

* test(components): remove redundant `Button_ChildContent_ShouldRenderCorrectly` test

* feat(components): introduce the concept of the components with slots
  • Loading branch information
desmondinho authored May 24, 2024
1 parent 417a2f5 commit 4e630bb
Show file tree
Hide file tree
Showing 24 changed files with 741 additions and 37 deletions.
166 changes: 165 additions & 1 deletion docs/LumexUI.Docs/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,174 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./Shared/**/*.razor",
"../../src/LumexUI/Styles/*.cs"
],
theme: {
extend: {},
extend: {
colors: {
/** Base */
background: {
DEFAULT: "hsl(var(--lumex-background) / <alpha-value>)"
},
foreground: {
50: "hsl(var(--lumex-foreground-50) / <alpha-value>)",
100: "hsl(var(--lumex-foreground-100) / <alpha-value>)",
200: "hsl(var(--lumex-foreground-200) / <alpha-value>)",
300: "hsl(var(--lumex-foreground-300) / <alpha-value>)",
400: "hsl(var(--lumex-foreground-400) / <alpha-value>)",
500: "hsl(var(--lumex-foreground-500) / <alpha-value>)",
600: "hsl(var(--lumex-foreground-600) / <alpha-value>)",
700: "hsl(var(--lumex-foreground-700) / <alpha-value>)",
800: "hsl(var(--lumex-foreground-800) / <alpha-value>)",
900: "hsl(var(--lumex-foreground-900) / <alpha-value>)",
DEFAULT: "hsl(var(--lumex-foreground) / <alpha-value>)"
},
focus: {
DEFAULT: "hsl(var(--lumex-focus) / <alpha-value>)"
},
overlay: {
DEFAULT: "hsl(var(--lumex-overlay) / <alpha-value>)"
},
divider: {
DEFAULT: "hsl(var(--lumex-divider) / var(--lumex-divider-opacity ,<alpha-value>))"
},

/** Default */
default: {
50: "hsl(var(--lumex-default-50) / <alpha-value>)",
100: "hsl(var(--lumex-default-100) / <alpha-value>)",
200: "hsl(var(--lumex-default-200) / <alpha-value>)",
300: "hsl(var(--lumex-default-300) / <alpha-value>)",
400: "hsl(var(--lumex-default-400) / <alpha-value>)",
500: "hsl(var(--lumex-default-500) / <alpha-value>)",
600: "hsl(var(--lumex-default-600) / <alpha-value>)",
700: "hsl(var(--lumex-default-700) / <alpha-value>)",
800: "hsl(var(--lumex-default-800) / <alpha-value>)",
900: "hsl(var(--lumex-default-900) / <alpha-value>)",
DEFAULT: "hsl(var(--lumex-default) / <alpha-value>)",
foreground: "hsl(var(--lumex-default-foreground) / <alpha-value>)"
},

/** Primary */
primary: {
50: "hsl(var(--lumex-primary-50) / <alpha-value>)",
100: "hsl(var(--lumex-primary-100) / <alpha-value>)",
200: "hsl(var(--lumex-primary-200) / <alpha-value>)",
300: "hsl(var(--lumex-primary-300) / <alpha-value>)",
400: "hsl(var(--lumex-primary-400) / <alpha-value>)",
500: "hsl(var(--lumex-primary-500) / <alpha-value>)",
600: "hsl(var(--lumex-primary-600) / <alpha-value>)",
700: "hsl(var(--lumex-primary-700) / <alpha-value>)",
800: "hsl(var(--lumex-primary-800) / <alpha-value>)",
900: "hsl(var(--lumex-primary-900) / <alpha-value>)",
DEFAULT: "hsl(var(--lumex-primary) / <alpha-value>)",
foreground: "hsl(var(--lumex-primary-foreground) / <alpha-value>)"
},

/** Secondary */
secondary: {
50: "hsl(var(--lumex-secondary-50) / <alpha-value>)",
100: "hsl(var(--lumex-secondary-100) / <alpha-value>)",
200: "hsl(var(--lumex-secondary-200) / <alpha-value>)",
300: "hsl(var(--lumex-secondary-300) / <alpha-value>)",
400: "hsl(var(--lumex-secondary-400) / <alpha-value>)",
500: "hsl(var(--lumex-secondary-500) / <alpha-value>)",
600: "hsl(var(--lumex-secondary-600) / <alpha-value>)",
700: "hsl(var(--lumex-secondary-700) / <alpha-value>)",
800: "hsl(var(--lumex-secondary-800) / <alpha-value>)",
900: "hsl(var(--lumex-secondary-900) / <alpha-value>)",
DEFAULT: "hsl(var(--lumex-secondary) / <alpha-value>)",
foreground: "hsl(var(--lumex-secondary-foreground) / <alpha-value>)"
},

/** Success */
success: {
50: "hsl(var(--lumex-success-50) / <alpha-value>)",
100: "hsl(var(--lumex-success-100) / <alpha-value>)",
200: "hsl(var(--lumex-success-200) / <alpha-value>)",
300: "hsl(var(--lumex-success-300) / <alpha-value>)",
400: "hsl(var(--lumex-success-400) / <alpha-value>)",
500: "hsl(var(--lumex-success-500) / <alpha-value>)",
600: "hsl(var(--lumex-success-600) / <alpha-value>)",
700: "hsl(var(--lumex-success-700) / <alpha-value>)",
800: "hsl(var(--lumex-success-800) / <alpha-value>)",
900: "hsl(var(--lumex-success-900) / <alpha-value>)",
DEFAULT: "hsl(var(--lumex-success) / <alpha-value>)",
foreground: "hsl(var(--lumex-success-foreground) / <alpha-value>)"
},

/** Warning */
warning: {
50: "hsl(var(--lumex-warning-50) / <alpha-value>)",
100: "hsl(var(--lumex-warning-100) / <alpha-value>)",
200: "hsl(var(--lumex-warning-200) / <alpha-value>)",
300: "hsl(var(--lumex-warning-300) / <alpha-value>)",
400: "hsl(var(--lumex-warning-400) / <alpha-value>)",
500: "hsl(var(--lumex-warning-500) / <alpha-value>)",
600: "hsl(var(--lumex-warning-600) / <alpha-value>)",
700: "hsl(var(--lumex-warning-700) / <alpha-value>)",
800: "hsl(var(--lumex-warning-800) / <alpha-value>)",
900: "hsl(var(--lumex-warning-900) / <alpha-value>)",
DEFAULT: "hsl(var(--lumex-warning) / <alpha-value>)",
foreground: "hsl(var(--lumex-warning-foreground) / <alpha-value>)"
},

/** Danger */
danger: {
50: "hsl(var(--lumex-danger-50) / <alpha-value>)",
100: "hsl(var(--lumex-danger-100) / <alpha-value>)",
200: "hsl(var(--lumex-danger-200) / <alpha-value>)",
300: "hsl(var(--lumex-danger-300) / <alpha-value>)",
400: "hsl(var(--lumex-danger-400) / <alpha-value>)",
500: "hsl(var(--lumex-danger-500) / <alpha-value>)",
600: "hsl(var(--lumex-danger-600) / <alpha-value>)",
700: "hsl(var(--lumex-danger-700) / <alpha-value>)",
800: "hsl(var(--lumex-danger-800) / <alpha-value>)",
900: "hsl(var(--lumex-danger-900) / <alpha-value>)",
DEFAULT: "hsl(var(--lumex-danger) / <alpha-value>)",
foreground: "hsl(var(--lumex-danger-foreground) / <alpha-value>)"
},

/** Info */
info: {
50: "hsl(var(--lumex-info-50) / <alpha-value>)",
100: "hsl(var(--lumex-info-100) / <alpha-value>)",
200: "hsl(var(--lumex-info-200) / <alpha-value>)",
300: "hsl(var(--lumex-info-300) / <alpha-value>)",
400: "hsl(var(--lumex-info-400) / <alpha-value>)",
500: "hsl(var(--lumex-info-500) / <alpha-value>)",
600: "hsl(var(--lumex-info-600) / <alpha-value>)",
700: "hsl(var(--lumex-info-700) / <alpha-value>)",
800: "hsl(var(--lumex-info-800) / <alpha-value>)",
900: "hsl(var(--lumex-info-900) / <alpha-value>)",
DEFAULT: "hsl(var(--lumex-info) / <alpha-value>)",
foreground: "hsl(var(--lumex-info-foreground) / <alpha-value>)"
}
},
fontSize: {
tiny: ["var(--lumex-font-size-tiny)", "var(--lumex-line-height-tiny)"],
small: ["var(--lumex-font-size-small)", "var(--lumex-line-height-small)"],
medium: ["var(--lumex-font-size-medium)", "var(--lumex-line-height-medium)"],
large: ["var(--lumex-font-size-large)", "var(--lumex-line-height-large)"]
},
borderRadius: {
small: "var(--lumex-radius-small)",
medium: "var(--lumex-radius-medium)",
large: "var(--lumex-radius-large)"
},
boxShadow: {
small: "var(--lumex-box-shadow-small)",
medium: "var(--lumex-box-shadow-medium)",
large: "var(--lumex-box-shadow-large)"
},
opacity: {
divider: "var(--lumex-divider-opacity)",
disabled: "var(--lumex-disabled-opacity)",
focus: "var(--lumex-focus-opacity)",
hover: "var(--lumex-hover-opacity)"
},
},
},
plugins: [],
}
16 changes: 16 additions & 0 deletions src/LumexUI/Common/Enums/Radius.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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;

public enum Radius
{
None,

Small,

Medium,

Large
}
16 changes: 16 additions & 0 deletions src/LumexUI/Common/Enums/Shadow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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;

public enum Shadow
{
None,

Small,

Medium,

Large
}
6 changes: 6 additions & 0 deletions src/LumexUI/Common/Interfaces/ISlot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace LumexUI.Common;

public interface ISlot
{
string? Root { get; }
}
6 changes: 6 additions & 0 deletions src/LumexUI/Common/Interfaces/ISlotComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace LumexUI.Common;

internal interface ISlotComponent<T> where T : ISlot
{
T? Classes { get; }
}
2 changes: 1 addition & 1 deletion src/LumexUI/Components/Bases/LumexComponentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public abstract class LumexComponentBase : ComponentBase
/// Gets or sets a collection of additional attributes that will be applied to the component.
/// </summary>
[Parameter( CaptureUnmatchedValues = true )]
public IReadOnlyDictionary<string, object?>? AdditionalAttributes { get; set; }
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }

[Inject] protected TwMerge TwMerge { get; set; } = default!;

Expand Down
15 changes: 15 additions & 0 deletions src/LumexUI/Components/Card/CardContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace LumexUI;

internal class CardContext( LumexCard owner )
{
public LumexCard Owner { get; } = owner;

public static void ThrowMissingParentComponentException( CardContext context, string componentName )
{
if( context is null )
{
throw new InvalidOperationException(
$"<{componentName} /> component must be used within a <{nameof( LumexCard )} /> component." );
}
}
}
11 changes: 11 additions & 0 deletions src/LumexUI/Components/Card/CardSlots.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using LumexUI.Common;

namespace LumexUI;

public class CardSlots : ISlot
{
public string? Root { get; set; }
public string? Header { get; set; }
public string? Body { get; set; }
public string? Footer { get; set; }
}
11 changes: 11 additions & 0 deletions src/LumexUI/Components/Card/LumexCard.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@namespace LumexUI
@inherits LumexComponentBase

<CascadingValue TValue="CardContext" Value="@_context" IsFixed="@true">
<LumexComponent Class="@RootClass"
Style="@RootStyle"
@attributes="@AdditionalAttributes"
data-slot="root">
@ChildContent
</LumexComponent>
</CascadingValue>
59 changes: 59 additions & 0 deletions src/LumexUI/Components/Card/LumexCard.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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;
using LumexUI.Styles;

using Microsoft.AspNetCore.Components;

namespace LumexUI;

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

/// <summary>
/// Gets or sets the border radius of the card.
/// </summary>
/// <remarks>
/// Default value is <see cref="Radius.Large"/>
/// </remarks>
[Parameter] public Radius Radius { get; set; } = Radius.Large;

/// <summary>
/// Gets or sets the shadow of the card.
/// </summary>
/// <remarks>
/// Default value is <see cref="Shadow.Medium"/>
/// </remarks>
[Parameter] public Shadow Shadow { get; set; } = Shadow.Small;

/// <summary>
/// Gets or sets a value indicating whether the card is full-width.
/// </summary>
[Parameter] public bool FullWidth { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the card is blurred.
/// </summary>
[Parameter] public bool Blurred { get; set; }

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

private protected override string? RootClass
=> TwMerge.Merge( Card.GetStyles( this ) );

private readonly CardContext _context;

public LumexCard()
{
_context = new CardContext( this );
}
}
9 changes: 9 additions & 0 deletions src/LumexUI/Components/Card/LumexCardBody.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@namespace LumexUI
@inherits LumexComponentBase

<LumexComponent Class="@RootClass"
Style="@RootStyle"
@attributes="@AdditionalAttributes"
data-slot="body">
@ChildContent
</LumexComponent>
27 changes: 27 additions & 0 deletions src/LumexUI/Components/Card/LumexCardBody.razor.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 LumexUI.Styles;

using Microsoft.AspNetCore.Components;

namespace LumexUI;

public partial class LumexCardBody : LumexComponentBase
{
/// <summary>
/// Gets or sets content to be rendered inside the card body.
/// </summary>
[Parameter] public RenderFragment? ChildContent { get; set; }

[CascadingParameter] internal CardContext Context { get; set; } = default!;

private protected override string? RootClass
=> TwMerge.Merge( Card.GetBodyStyles( this ) );

protected override void OnInitialized()
{
CardContext.ThrowMissingParentComponentException( Context, nameof( LumexCardBody ) );
}
}
9 changes: 9 additions & 0 deletions src/LumexUI/Components/Card/LumexCardFooter.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@namespace LumexUI
@inherits LumexComponentBase

<LumexComponent Class="@RootClass"
Style="@RootStyle"
@attributes="@AdditionalAttributes"
data-slot="footer">
@ChildContent
</LumexComponent>
Loading

0 comments on commit 4e630bb

Please sign in to comment.