Lumeo

Contributing

Thank you for your interest in contributing to Lumeo. This guide walks through setting up the project, understanding the codebase structure, creating components, writing documentation, and running tests.

Getting started

Prerequisites

  • .NET 10 SDK — Download from dotnet.microsoft.com.
  • Node.js — Required for Tailwind CSS processing. Any recent LTS version works.
  • IDE — Visual Studio 2022+, JetBrains Rider, or VS Code with the C# Dev Kit extension.

Clone and build

git clone https://github.com/aspect-build/lumeo.git cd lumeo # Build the component library dotnet build src/Lumeo/Lumeo.csproj # Run the docs site dotnet run --project docs/Lumeo.Docs/Lumeo.Docs.csproj

The docs site will be available at https://localhost:5001 (or the port shown in your console).

Build and pack

# Build the library dotnet build src/Lumeo/Lumeo.csproj # Create a NuGet package dotnet pack src/Lumeo/Lumeo.csproj

Project structure

The repository is organized into three main areas:

Path Description
src/Lumeo/ The component library. Published as the Lumeo NuGet package.
src/Lumeo/UI/ All components, organized by name. Each component lives in its own folder (e.g. UI/Button/, UI/Dialog/).
src/Lumeo/Services/ Shared services: ToastService, OverlayService, ThemeService, KeyboardShortcutService, ComponentInteropService.
src/Lumeo/wwwroot/ Static assets. CSS in css/, JavaScript in js/.
docs/Lumeo.Docs/ The documentation site (Blazor Server). Contains pages, layouts, and shared components.
tests/Lumeo.Tests/ Unit and integration tests using bUnit.

Creating a component

Every component follows a consistent structure. Here is a step-by-step guide.

1. Create the component folder

Create a new folder under src/Lumeo/UI/ named after your component. For example, src/Lumeo/UI/MyComponent/.

2. Follow the required conventions

Every .razor file must include these elements:

@namespace Lumeo <div class="@($"{BaseClasses} {Class}".Trim())" @attributes="AdditionalAttributes"> @ChildContent </div> @code { [Parameter] public RenderFragment? ChildContent { get; set; } [Parameter] public string? Class { get; set; } [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; } private string BaseClasses => "your-tailwind-classes-here"; }

Key rules:

  • @namespace Lumeo must be the first line of every .razor file.
  • Class parameter — Always include [Parameter] public string? Class { get; set; } for consumer CSS overrides.
  • AdditionalAttributes — Always include [Parameter(CaptureUnmatchedValues = true)] and spread it on the root element with @attributes.
  • CSS class combining — Use the pattern $"{BaseClasses} {Class}".Trim() to merge base and consumer classes.

3. Use CSS variables for all colors

Never use hardcoded hex or HSL values. Always reference CSS variables through Tailwind utility classes:

Do Do not
bg-primary bg-blue-500
text-foreground text-gray-900
border-border border-[#e5e7eb]
bg-muted bg-[hsl(210,40%,96%)]

Do not use Tailwind dark: prefixes. Dark mode is handled entirely through CSS variable swaps in lumeo.css.

4. Define enums inside the component

If your component needs variants, sizes, or other enumerated options, define the enum as a nested type inside the @code block:

@code { [Parameter] public AlertVariant Variant { get; set; } = AlertVariant.Default; public enum AlertVariant { Default, Destructive, Success, Warning } }

Consumers reference these as Alert.AlertVariant.Success.

5. Use CascadingValue for state

When child components need to read parent state, use CascadingValue with a context record. Define the record as a nested type in the parent component. Use IsFixed="false" when the state can change.

6. JS interop through ComponentInteropService

Never inject IJSRuntime directly in components. Instead, inject ComponentInteropService and call its methods:

@inject ComponentInteropService Interop @code { // Scroll locking for modals await Interop.LockScroll(); await Interop.UnlockScroll(); // Focus trapping for overlays await Interop.SetupFocusTrap(elementRef); await Interop.RemoveFocusTrap(elementRef); // Click-outside detection await Interop.RegisterClickOutside(elementRef, callback); }

7. Icons

Use the Blazicon component from the Blazicons.Lucide package:

<Blazicon Svg="Lucide.ChevronDown" class="h-4 w-4" /> <Blazicon Svg="Lucide.X" class="h-4 w-4" /> <Blazicon Svg="Lucide.Check" class="h-4 w-4" />

Adding documentation

Every component should have a corresponding documentation page in the docs site.

1. Create the page file

Add a new .razor file under docs/Lumeo.Docs/Pages/Components/. Use this template:

@page "/components/my-component" @namespace Lumeo.Docs.Pages.Components <PageTitle>MyComponent — Lumeo</PageTitle> <Container MaxWidth="3xl" Padding="0" Center="false"> <Stack Gap="10"> <!-- Header with title and description --> <!-- Live demos --> <!-- When to use section --> <!-- API reference table --> </Stack> </Container>

2. Required sections

  • Header — Component name as an H1, followed by a brief description.
  • Live demos — Interactive examples showing the component in action. Use bordered cards for each example.
  • When to use — A list of appropriate use cases for the component.
  • API reference — A table listing all parameters with their types, defaults, and descriptions.

3. Add to the navigation

Open docs/Lumeo.Docs/Layout/NavMenu.razor and add a <NavLink> in the appropriate category section.

Running tests

The test suite uses bUnit for component testing.

Run all tests

dotnet test

Run a specific test

dotnet test --filter "FullyQualifiedName~ButtonTests"

Test conventions

  • Place tests in tests/Lumeo.Tests/, mirroring the source folder structure.
  • Name test files {ComponentName}Tests.cs.
  • Use bUnit's RenderComponent<T> to render and assert component output.
  • Test default rendering, parameter variations, event callbacks, and accessibility attributes.

Example test

using Bunit; using Xunit; public class ButtonTests : TestContext { [Fact] public void Button_RendersDefaultVariant() { var cut = RenderComponent<Button>(parameters => parameters.AddChildContent("Click me")); cut.Find("button").MarkupMatches( "<button class='...'>Click me</button>"); } [Fact] public void Button_FiresOnClick() { var clicked = false; var cut = RenderComponent<Button>(parameters => parameters .AddChildContent("Click") .Add(p => p.OnClick, EventCallback.Factory.Create(this, () => clicked = true))); cut.Find("button").Click(); Assert.True(clicked); } }

Code style

Consistent code style keeps the codebase readable and maintainable. Here are the key conventions.

Area Convention
CSS framework Tailwind CSS v4. All utility classes applied inline.
Colors CSS variables only (bg-primary, text-foreground, etc.). No hardcoded hex/HSL values.
Dark mode Handled by CSS variable swaps in lumeo.css. Never use dark: Tailwind prefixes.
Namespace All component files use @namespace Lumeo.
Two-way binding Property + PropertyChanged EventCallback pairs.
JS interop Always through ComponentInteropService. Never inject IJSRuntime directly.
Enums Defined as nested types inside the component's @code block.
Context records Defined as nested public record inside the parent component.
Overlay cleanup Implement IAsyncDisposable. Handle JSDisconnectedException in cleanup.
Icons Use <Blazicon Svg="Lucide.X" /> from Blazicons.Lucide.

Pull request guidelines

  • One component per PR. Keep pull requests focused. If your change affects multiple components, split them into separate PRs.
  • Include documentation. New components should come with a documentation page. Changes to existing components should update the relevant docs.
  • Add tests. Include bUnit tests that cover the default rendering, parameter variations, and key interactions.
  • Build must pass. Run dotnet build src/Lumeo/Lumeo.csproj and dotnet test before submitting.
  • Descriptive commit messages. Use clear, concise commit messages that describe the "why" behind the change.