Aggiunte skills per agente
This commit is contained in:
@@ -0,0 +1,822 @@
|
||||
---
|
||||
name: blazor-specialist
|
||||
description: Blazor UI development specialist. Use for building Blazor Server or WebAssembly apps, component development, state management, or form handling.
|
||||
---
|
||||
|
||||
# Blazor UI Specialist Skill
|
||||
|
||||
Specialized agent for Blazor Server and Blazor WebAssembly development, component design, and state management.
|
||||
|
||||
## Role
|
||||
|
||||
You are a Blazor UI Specialist responsible for building interactive web interfaces using Blazor, managing component lifecycle, implementing state management, handling forms and validation, and integrating with backend APIs.
|
||||
|
||||
## Expertise Areas
|
||||
|
||||
- Blazor Server architecture
|
||||
- Blazor WebAssembly (WASM)
|
||||
- Component lifecycle and rendering
|
||||
- State management (FluxState, Fluxor)
|
||||
- Form validation and submission
|
||||
- JavaScript interop
|
||||
- SignalR integration
|
||||
- Component libraries (FluentUI, MudBlazor)
|
||||
- Performance optimization
|
||||
- Authentication in Blazor
|
||||
- Responsive design patterns
|
||||
|
||||
## Responsibilities
|
||||
|
||||
1. **Component Development**
|
||||
- Create reusable Blazor components
|
||||
- Manage component parameters and events
|
||||
- Implement component lifecycle methods
|
||||
- Handle component state
|
||||
- Create child/parent component communication
|
||||
|
||||
2. **State Management**
|
||||
- Implement global state management
|
||||
- Use dependency injection for services
|
||||
- Manage component-level state
|
||||
- Handle application-wide events
|
||||
- Implement undo/redo patterns
|
||||
|
||||
3. **Forms and Validation**
|
||||
- Build forms with EditForm
|
||||
- Implement data annotations validation
|
||||
- Handle custom validation
|
||||
- Display validation messages
|
||||
- Submit forms to API
|
||||
|
||||
4. **API Integration**
|
||||
- Call backend APIs from Blazor
|
||||
- Handle authentication tokens
|
||||
- Display loading states
|
||||
- Handle errors gracefully
|
||||
- Implement optimistic UI updates
|
||||
|
||||
## Load Additional Patterns
|
||||
|
||||
- `.claude/patterns/api-patterns.md`
|
||||
|
||||
## Critical Rules
|
||||
|
||||
### Blazor Best Practices
|
||||
- Use `@rendermode` appropriately (Server, WebAssembly, Auto)
|
||||
- Dispose of resources in components (IDisposable)
|
||||
- Avoid blocking the UI thread
|
||||
- Use `StateHasChanged()` sparingly
|
||||
- Minimize JavaScript interop
|
||||
- Use cascading parameters for shared data
|
||||
- Implement proper error boundaries
|
||||
|
||||
### Component Design
|
||||
- Keep components focused (single responsibility)
|
||||
- Use parameters for component inputs
|
||||
- Use EventCallback for component outputs
|
||||
- Make components reusable
|
||||
- Separate presentation from logic
|
||||
- Use code-behind for complex logic
|
||||
|
||||
### Performance
|
||||
- Use `@key` directive for list items
|
||||
- Virtualize long lists
|
||||
- Lazy load routes and components
|
||||
- Minimize re-renders
|
||||
- Use OnInitializedAsync for async initialization
|
||||
- Stream large datasets
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Basic Component
|
||||
```razor
|
||||
@* File: Components/BudgetCard.razor *@
|
||||
@namespace {ApplicationName}.UI.Components
|
||||
|
||||
<div class="budget-card">
|
||||
<h3>@Budget.Name</h3>
|
||||
<p class="amount">@Budget.Amount.ToString("C")</p>
|
||||
<p class="created">Created: @Budget.CreatedDate.ToString("d")</p>
|
||||
|
||||
<button @onclick="OnEditClicked">Edit</button>
|
||||
<button @onclick="OnDeleteClicked" class="danger">Delete</button>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public BudgetResponse Budget { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<BudgetResponse> OnEdit { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<Guid> OnDelete { get; set; }
|
||||
|
||||
private async Task OnEditClicked()
|
||||
{
|
||||
if (OnEdit.HasDelegate)
|
||||
await OnEdit.InvokeAsync(Budget);
|
||||
}
|
||||
|
||||
private async Task OnDeleteClicked()
|
||||
{
|
||||
if (OnDelete.HasDelegate)
|
||||
await OnDelete.InvokeAsync(Budget.BudgetId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Component with Code-Behind
|
||||
```razor
|
||||
@* File: Pages/Budgets/BudgetList.razor *@
|
||||
@page "/budgets"
|
||||
@namespace {ApplicationName}.UI.Pages.Budgets
|
||||
@inherits BudgetListBase
|
||||
|
||||
<PageTitle>Budgets</PageTitle>
|
||||
|
||||
<div class="budget-list-page">
|
||||
<h1>Budgets</h1>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<p>Loading budgets...</p>
|
||||
}
|
||||
else if (ErrorMessage is not null)
|
||||
{
|
||||
<div class="error">@ErrorMessage</div>
|
||||
}
|
||||
else if (!Budgets.Any())
|
||||
{
|
||||
<p>No budgets found. Create your first budget!</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="budget-grid">
|
||||
@foreach (var budget in Budgets)
|
||||
{
|
||||
<BudgetCard
|
||||
Budget="@budget"
|
||||
OnEdit="@HandleEditBudget"
|
||||
OnDelete="@HandleDeleteBudget" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<button @onclick="HandleCreateBudget" class="primary">Create Budget</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```csharp
|
||||
// File: Pages/Budgets/BudgetList.razor.cs
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using {ApplicationName}.UI.Services;
|
||||
|
||||
namespace {ApplicationName}.UI.Pages.Budgets;
|
||||
|
||||
public class BudgetListBase : ComponentBase, IDisposable
|
||||
{
|
||||
[Inject]
|
||||
protected IBudgetService BudgetService { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected NavigationManager Navigation { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected ILogger<BudgetListBase> Logger { get; set; } = default!;
|
||||
|
||||
protected List<BudgetResponse> Budgets { get; set; } = new();
|
||||
protected bool IsLoading { get; set; }
|
||||
protected string? ErrorMessage { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadBudgetsAsync();
|
||||
}
|
||||
|
||||
protected async Task LoadBudgetsAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
ErrorMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
Budgets = await BudgetService.GetBudgetsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error loading budgets");
|
||||
ErrorMessage = "Failed to load budgets. Please try again.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected void HandleCreateBudget()
|
||||
{
|
||||
Navigation.NavigateTo("/budgets/create");
|
||||
}
|
||||
|
||||
protected void HandleEditBudget(BudgetResponse budget)
|
||||
{
|
||||
Navigation.NavigateTo($"/budgets/{budget.BudgetId}/edit");
|
||||
}
|
||||
|
||||
protected async Task HandleDeleteBudget(Guid budgetId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await BudgetService.DeleteBudgetAsync(budgetId);
|
||||
await LoadBudgetsAsync(); // Reload list
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error deleting budget {BudgetId}", budgetId);
|
||||
ErrorMessage = "Failed to delete budget. Please try again.";
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Clean up subscriptions, timers, etc.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Form Validation Pattern
|
||||
|
||||
```razor
|
||||
@* File: Pages/Budgets/CreateBudget.razor *@
|
||||
@page "/budgets/create"
|
||||
@namespace {ApplicationName}.UI.Pages.Budgets
|
||||
|
||||
<PageTitle>Create Budget</PageTitle>
|
||||
|
||||
<div class="create-budget-page">
|
||||
<h1>Create Budget</h1>
|
||||
|
||||
<EditForm Model="@Model" OnValidSubmit="@HandleValidSubmit" FormName="CreateBudgetForm">
|
||||
<DataAnnotationsValidator />
|
||||
<ValidationSummary />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Name:</label>
|
||||
<InputText id="name" @bind-Value="Model.Name" class="form-control" />
|
||||
<ValidationMessage For="@(() => Model.Name)" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="amount">Amount:</label>
|
||||
<InputNumber id="amount" @bind-Value="Model.Amount" class="form-control" />
|
||||
<ValidationMessage For="@(() => Model.Amount)" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="startDate">Start Date:</label>
|
||||
<InputDate id="startDate" @bind-Value="Model.StartDate" class="form-control" />
|
||||
<ValidationMessage For="@(() => Model.StartDate)" />
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" disabled="@IsSubmitting">
|
||||
@if (IsSubmitting)
|
||||
{
|
||||
<span>Creating...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Create</span>
|
||||
}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" @onclick="@Cancel">Cancel</button>
|
||||
</div>
|
||||
|
||||
@if (ErrorMessage is not null)
|
||||
{
|
||||
<div class="alert alert-danger mt-3">@ErrorMessage</div>
|
||||
}
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject]
|
||||
private IBudgetService BudgetService { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
private NavigationManager Navigation { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromForm]
|
||||
private CreateBudgetModel Model { get; set; } = new();
|
||||
|
||||
private bool IsSubmitting { get; set; }
|
||||
private string? ErrorMessage { get; set; }
|
||||
|
||||
private async Task HandleValidSubmit()
|
||||
{
|
||||
IsSubmitting = true;
|
||||
ErrorMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
var command = new CreateBudgetCommand(
|
||||
Model.Name,
|
||||
Model.Amount,
|
||||
Model.StartDate);
|
||||
|
||||
var result = await BudgetService.CreateBudgetAsync(command);
|
||||
|
||||
Navigation.NavigateTo($"/budgets/{result.BudgetId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = "Failed to create budget. Please try again.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSubmitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
Navigation.NavigateTo("/budgets");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// File: Models/CreateBudgetModel.cs
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
public class CreateBudgetModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(100, MinimumLength = 3)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[Range(0.01, 1_000_000)]
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTimeOffset StartDate { get; set; } = DateTimeOffset.Now;
|
||||
}
|
||||
```
|
||||
|
||||
## State Management (Fluxor)
|
||||
|
||||
### Install Fluxor
|
||||
```bash
|
||||
dotnet add package Fluxor.Blazor.Web
|
||||
```
|
||||
|
||||
### Define State
|
||||
```csharp
|
||||
// File: Store/BudgetState/BudgetState.cs
|
||||
namespace {ApplicationName}.UI.Store.BudgetState;
|
||||
|
||||
public record BudgetState
|
||||
{
|
||||
public List<BudgetResponse> Budgets { get; init; } = new();
|
||||
public bool IsLoading { get; init; }
|
||||
public string? ErrorMessage { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
### Define Actions
|
||||
```csharp
|
||||
// File: Store/BudgetState/BudgetActions.cs
|
||||
namespace {ApplicationName}.UI.Store.BudgetState;
|
||||
|
||||
public record LoadBudgetsAction;
|
||||
public record LoadBudgetsSuccessAction(List<BudgetResponse> Budgets);
|
||||
public record LoadBudgetsFailureAction(string ErrorMessage);
|
||||
```
|
||||
|
||||
### Implement Reducer
|
||||
```csharp
|
||||
// File: Store/BudgetState/BudgetReducers.cs
|
||||
using Fluxor;
|
||||
|
||||
namespace {ApplicationName}.UI.Store.BudgetState;
|
||||
|
||||
public static class BudgetReducers
|
||||
{
|
||||
[ReducerMethod]
|
||||
public static BudgetState ReduceLoadBudgetsAction(BudgetState state, LoadBudgetsAction action) =>
|
||||
state with { IsLoading = true, ErrorMessage = null };
|
||||
|
||||
[ReducerMethod]
|
||||
public static BudgetState ReduceLoadBudgetsSuccessAction(BudgetState state, LoadBudgetsSuccessAction action) =>
|
||||
state with { IsLoading = false, Budgets = action.Budgets };
|
||||
|
||||
[ReducerMethod]
|
||||
public static BudgetState ReduceLoadBudgetsFailureAction(BudgetState state, LoadBudgetsFailureAction action) =>
|
||||
state with { IsLoading = false, ErrorMessage = action.ErrorMessage };
|
||||
}
|
||||
```
|
||||
|
||||
### Implement Effects
|
||||
```csharp
|
||||
// File: Store/BudgetState/BudgetEffects.cs
|
||||
using Fluxor;
|
||||
|
||||
namespace {ApplicationName}.UI.Store.BudgetState;
|
||||
|
||||
public class BudgetEffects(IBudgetService budgetService)
|
||||
{
|
||||
[EffectMethod]
|
||||
public async Task HandleLoadBudgetsAction(LoadBudgetsAction action, IDispatcher dispatcher)
|
||||
{
|
||||
try
|
||||
{
|
||||
var budgets = await budgetService.GetBudgetsAsync();
|
||||
dispatcher.Dispatch(new LoadBudgetsSuccessAction(budgets));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
dispatcher.Dispatch(new LoadBudgetsFailureAction(ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Register Fluxor
|
||||
```csharp
|
||||
// Program.cs
|
||||
builder.Services.AddFluxor(options =>
|
||||
{
|
||||
options.ScanAssemblies(typeof(Program).Assembly);
|
||||
options.UseReduxDevTools();
|
||||
});
|
||||
```
|
||||
|
||||
### Use in Component
|
||||
```razor
|
||||
@inherits FluxorComponent
|
||||
@inject IState<BudgetState> BudgetState
|
||||
@inject IDispatcher Dispatcher
|
||||
|
||||
<div>
|
||||
@if (BudgetState.Value.IsLoading)
|
||||
{
|
||||
<p>Loading...</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var budget in BudgetState.Value.Budgets)
|
||||
{
|
||||
<BudgetCard Budget="@budget" />
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
Dispatcher.Dispatch(new LoadBudgetsAction());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Service Pattern
|
||||
|
||||
```csharp
|
||||
// File: Services/BudgetService.cs
|
||||
namespace {ApplicationName}.UI.Services;
|
||||
|
||||
using System.Net.Http.Json;
|
||||
|
||||
public interface IBudgetService
|
||||
{
|
||||
Task<List<BudgetResponse>> GetBudgetsAsync(CancellationToken cancellationToken = default);
|
||||
Task<BudgetResponse> GetBudgetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<CreateBudgetResponse> CreateBudgetAsync(CreateBudgetCommand command, CancellationToken cancellationToken = default);
|
||||
Task UpdateBudgetAsync(Guid id, UpdateBudgetCommand command, CancellationToken cancellationToken = default);
|
||||
Task DeleteBudgetAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class BudgetService(
|
||||
HttpClient httpClient,
|
||||
ILogger<BudgetService> logger
|
||||
) : IBudgetService
|
||||
{
|
||||
public async Task<List<BudgetResponse>> GetBudgetsAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var budgets = await httpClient.GetFromJsonAsync<List<BudgetResponse>>(
|
||||
"/budgets",
|
||||
cancellationToken);
|
||||
|
||||
return budgets ?? new List<BudgetResponse>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error fetching budgets");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BudgetResponse> GetBudgetByIdAsync(
|
||||
Guid id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var budget = await httpClient.GetFromJsonAsync<BudgetResponse>(
|
||||
$"/budgets/{id}",
|
||||
cancellationToken);
|
||||
|
||||
return budget ?? throw new InvalidOperationException("Budget not found");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error fetching budget {BudgetId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CreateBudgetResponse> CreateBudgetAsync(
|
||||
CreateBudgetCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await httpClient.PostAsJsonAsync(
|
||||
"/budgets",
|
||||
command,
|
||||
cancellationToken);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<CreateBudgetResponse>(
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
return result ?? throw new InvalidOperationException("Failed to create budget");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error creating budget");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateBudgetAsync(
|
||||
Guid id,
|
||||
UpdateBudgetCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await httpClient.PutAsJsonAsync(
|
||||
$"/budgets/{id}",
|
||||
command,
|
||||
cancellationToken);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error updating budget {BudgetId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteBudgetAsync(
|
||||
Guid id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await httpClient.DeleteAsync(
|
||||
$"/budgets/{id}",
|
||||
cancellationToken);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error deleting budget {BudgetId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register service
|
||||
builder.Services.AddScoped<IBudgetService, BudgetService>();
|
||||
```
|
||||
|
||||
## JavaScript Interop
|
||||
|
||||
```razor
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<button @onclick="ShowAlert">Show Alert</button>
|
||||
<button @onclick="GetLocalStorage">Get from LocalStorage</button>
|
||||
|
||||
@code {
|
||||
private async Task ShowAlert()
|
||||
{
|
||||
await JS.InvokeVoidAsync("alert", "Hello from Blazor!");
|
||||
}
|
||||
|
||||
private async Task GetLocalStorage()
|
||||
{
|
||||
var value = await JS.InvokeAsync<string>("localStorage.getItem", "myKey");
|
||||
Console.WriteLine($"Value from localStorage: {value}");
|
||||
}
|
||||
|
||||
private async Task SetLocalStorage()
|
||||
{
|
||||
await JS.InvokeVoidAsync("localStorage.setItem", "myKey", "myValue");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// wwwroot/js/interop.js
|
||||
window.budgetApp = {
|
||||
formatCurrency: function (amount) {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(amount);
|
||||
},
|
||||
|
||||
showConfirmDialog: function (message) {
|
||||
return confirm(message);
|
||||
},
|
||||
|
||||
downloadFile: function (filename, content) {
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Authentication in Blazor
|
||||
|
||||
```razor
|
||||
@* File: Pages/Login.razor *@
|
||||
@page "/login"
|
||||
@inject NavigationManager Navigation
|
||||
@inject IAuthenticationService AuthService
|
||||
|
||||
<div class="login-page">
|
||||
<h1>Login</h1>
|
||||
|
||||
<EditForm Model="@loginModel" OnValidSubmit="@HandleLogin">
|
||||
<DataAnnotationsValidator />
|
||||
<ValidationSummary />
|
||||
|
||||
<div class="form-group">
|
||||
<label>Email:</label>
|
||||
<InputText @bind-Value="loginModel.Email" class="form-control" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Password:</label>
|
||||
<InputText type="password" @bind-Value="loginModel.Password" class="form-control" />
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private LoginModel loginModel = new();
|
||||
|
||||
private async Task HandleLogin()
|
||||
{
|
||||
var result = await AuthService.LoginAsync(loginModel.Email, loginModel.Password);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
Navigation.NavigateTo("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```razor
|
||||
@* File: Components/AuthorizeView.razor *@
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<p>Hello, @context.User.Identity?.Name!</p>
|
||||
<a href="/logout">Logout</a>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<a href="/login">Login</a>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Virtualization
|
||||
```razor
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
|
||||
<Virtualize Items="@budgets" Context="budget">
|
||||
<BudgetCard Budget="@budget" />
|
||||
</Virtualize>
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
```razor
|
||||
@* File: App.razor *@
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</NotFound>
|
||||
</Router>
|
||||
```
|
||||
|
||||
## Common Blazor Pitfalls
|
||||
|
||||
### ❌ Avoid These Mistakes
|
||||
|
||||
1. **Not Disposing Components**
|
||||
- ❌ Subscribe to events without unsubscribing
|
||||
- ✅ Implement IDisposable and clean up
|
||||
|
||||
2. **Blocking UI Thread**
|
||||
- ❌ Using `Task.Result` or `.Wait()`
|
||||
- ✅ Always use `await`
|
||||
|
||||
3. **Overusing StateHasChanged**
|
||||
- ❌ Calling `StateHasChanged()` everywhere
|
||||
- ✅ Let Blazor handle rendering automatically
|
||||
|
||||
4. **Missing @key Directive**
|
||||
- ❌ Rendering lists without `@key`
|
||||
- ✅ Use `@key` for dynamic lists
|
||||
|
||||
5. **Not Handling Errors**
|
||||
- ❌ No error boundaries
|
||||
- ✅ Use ErrorBoundary component
|
||||
|
||||
## Blazor Checklist
|
||||
|
||||
### Component Development
|
||||
- [ ] Components are focused and reusable
|
||||
- [ ] Parameters use `[Parameter]` attribute
|
||||
- [ ] EventCallbacks for component events
|
||||
- [ ] Code-behind for complex logic
|
||||
- [ ] IDisposable implemented where needed
|
||||
|
||||
### Forms & Validation
|
||||
- [ ] EditForm used for forms
|
||||
- [ ] Data annotations validation
|
||||
- [ ] ValidationSummary displayed
|
||||
- [ ] Submit button disabled during submission
|
||||
- [ ] Error messages displayed
|
||||
|
||||
### State Management
|
||||
- [ ] Global state managed (Fluxor/FluxState)
|
||||
- [ ] Component state localized
|
||||
- [ ] Services injected via DI
|
||||
- [ ] State changes trigger re-renders
|
||||
|
||||
### API Integration
|
||||
- [ ] HttpClient configured
|
||||
- [ ] Loading states displayed
|
||||
- [ ] Error handling implemented
|
||||
- [ ] Authentication tokens included
|
||||
- [ ] Retry logic for transient failures
|
||||
|
||||
### Performance
|
||||
- [ ] Virtualization for long lists
|
||||
- [ ] Lazy loading for routes
|
||||
- [ ] @key directive on lists
|
||||
- [ ] Minimal JavaScript interop
|
||||
- [ ] Component disposal implemented
|
||||
|
||||
## Checklist Before Completion
|
||||
|
||||
- [ ] All components render correctly
|
||||
- [ ] Forms validate and submit
|
||||
- [ ] API calls successful
|
||||
- [ ] Loading states displayed
|
||||
- [ ] Error handling functional
|
||||
- [ ] Authentication working
|
||||
- [ ] State management functional
|
||||
- [ ] Performance optimized
|
||||
- [ ] Responsive design implemented
|
||||
- [ ] Documentation complete
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Frontend Design
|
||||
|
||||
Use this when the task is not just "make it work" but "make it look designed."
|
||||
|
||||
This skill is for product pages, dashboards, app shells, components, or visual systems that need a clear point of view instead of generic AI-looking UI.
|
||||
|
||||
## When To Use
|
||||
|
||||
- building a landing page, dashboard, or app surface from scratch
|
||||
- upgrading a bland interface into something intentional and memorable
|
||||
- translating a product concept into a concrete visual direction
|
||||
- implementing a frontend where typography, composition, and motion matter
|
||||
|
||||
## Core Principle
|
||||
|
||||
Pick a direction and commit to it.
|
||||
|
||||
Safe-average UI is usually worse than a strong, coherent aesthetic with a few bold choices.
|
||||
|
||||
## Design Workflow
|
||||
|
||||
### 1. Frame the interface first
|
||||
|
||||
Before coding, settle:
|
||||
|
||||
- purpose
|
||||
- audience
|
||||
- emotional tone
|
||||
- visual direction
|
||||
- one thing the user should remember
|
||||
|
||||
Possible directions:
|
||||
|
||||
- brutally minimal
|
||||
- editorial
|
||||
- industrial
|
||||
- luxury
|
||||
- playful
|
||||
- geometric
|
||||
- retro-futurist
|
||||
- soft and organic
|
||||
- maximalist
|
||||
|
||||
Do not mix directions casually. Choose one and execute it cleanly.
|
||||
|
||||
### 2. Build the visual system
|
||||
|
||||
Define:
|
||||
|
||||
- type hierarchy
|
||||
- color variables
|
||||
- spacing rhythm
|
||||
- layout logic
|
||||
- motion rules
|
||||
- surface / border / shadow treatment
|
||||
|
||||
Use CSS variables or the project's token system so the interface stays coherent as it grows.
|
||||
|
||||
### 3. Compose with intention
|
||||
|
||||
Prefer:
|
||||
|
||||
- asymmetry when it sharpens hierarchy
|
||||
- overlap when it creates depth
|
||||
- strong whitespace when it clarifies focus
|
||||
- dense layouts only when the product benefits from density
|
||||
|
||||
Avoid defaulting to a symmetrical card grid unless it is clearly the right fit.
|
||||
|
||||
### 4. Make motion meaningful
|
||||
|
||||
Use animation to:
|
||||
|
||||
- reveal hierarchy
|
||||
- stage information
|
||||
- reinforce user action
|
||||
- create one or two memorable moments
|
||||
|
||||
Do not scatter generic micro-interactions everywhere. One well-directed load sequence is usually stronger than twenty random hover effects.
|
||||
|
||||
## Strong Defaults
|
||||
|
||||
### Typography
|
||||
|
||||
- pick fonts with character
|
||||
- pair a distinctive display face with a readable body face when appropriate
|
||||
- avoid generic defaults when the page is design-led
|
||||
|
||||
### Color
|
||||
|
||||
- commit to a clear palette
|
||||
- one dominant field with selective accents usually works better than evenly weighted rainbow palettes
|
||||
- avoid cliché purple-gradient-on-white unless the product genuinely calls for it
|
||||
|
||||
### Background
|
||||
|
||||
Use atmosphere:
|
||||
|
||||
- gradients
|
||||
- meshes
|
||||
- textures
|
||||
- subtle noise
|
||||
- patterns
|
||||
- layered transparency
|
||||
|
||||
Flat empty backgrounds are rarely the best answer for a product-facing page.
|
||||
|
||||
### Layout
|
||||
|
||||
- break the grid when the composition benefits from it
|
||||
- use diagonals, offsets, and grouping intentionally
|
||||
- keep reading flow obvious even when the layout is unconventional
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
Never default to:
|
||||
|
||||
- interchangeable SaaS hero sections
|
||||
- generic card piles with no hierarchy
|
||||
- random accent colors without a system
|
||||
- placeholder-feeling typography
|
||||
- motion that exists only because animation was easy to add
|
||||
|
||||
## Execution Rules
|
||||
|
||||
- preserve the established design system when working inside an existing product
|
||||
- match technical complexity to the visual idea
|
||||
- keep accessibility and responsiveness intact
|
||||
- frontends should feel deliberate on desktop and mobile
|
||||
|
||||
## Quality Gate
|
||||
|
||||
Before delivering:
|
||||
|
||||
- the interface has a clear visual point of view
|
||||
- typography and spacing feel intentional
|
||||
- color and motion support the product instead of decorating it randomly
|
||||
- the result does not read like generic AI UI
|
||||
- the implementation is production-grade, not just visually interesting
|
||||
Reference in New Issue
Block a user