diff --git a/.agents/rules/api-conventions.md b/.agents/rules/api-conventions.md new file mode 100644 index 0000000000..11eedf5d7d --- /dev/null +++ b/.agents/rules/api-conventions.md @@ -0,0 +1,65 @@ +--- +paths: + - "src/Modules/**/Features/**/*" + - "src/Modules/**/*Endpoint*.cs" +--- + +# API Conventions + +Rules for API endpoints in FSH. + +## Endpoint Requirements + +Every endpoint MUST have: + +```csharp +endpoints.MapPost("/", handler) + .WithName(nameof(CommandOrQuery)) // Required: Unique name + .WithSummary("Description") // Required: OpenAPI description + .RequirePermission(Permission) // Required: Or .AllowAnonymous() +``` + +## HTTP Method Mapping + +| Operation | Method | Return | +|-----------|--------|--------| +| Create | `MapPost` | `TypedResults.Created(...)` | +| Read single | `MapGet` | `TypedResults.Ok(...)` | +| Read list | `MapGet` | `TypedResults.Ok(...)` | +| Update | `MapPut` | `TypedResults.Ok(...)` or `NoContent()` | +| Delete | `MapDelete` | `TypedResults.NoContent()` | + +## Route Patterns + +``` +/api/v1/{module}/{entities} # Collection +/api/v1/{module}/{entities}/{id} # Single item +/api/v1/{module}/{entities}/{id}/sub # Sub-resource +``` + +## Response Types + +Always use `TypedResults`: +- `TypedResults.Ok(data)` +- `TypedResults.Created($"/path/{id}", data)` +- `TypedResults.NoContent()` +- `TypedResults.NotFound()` +- `TypedResults.BadRequest(errors)` + +Never return raw objects or use `Results.Ok()`. + +## Permission Format + +```csharp +.RequirePermission({Module}Permissions.{Entity}.{Action}) +``` + +Actions: `View`, `Create`, `Update`, `Delete` + +## Query Parameters + +Use `[AsParameters]` for complex queries: + +```csharp +endpoints.MapGet("/", async ([AsParameters] GetProductsQuery query, ...) => ...) +``` diff --git a/.agents/rules/architecture.md b/.agents/rules/architecture.md new file mode 100644 index 0000000000..ba8205deeb --- /dev/null +++ b/.agents/rules/architecture.md @@ -0,0 +1,246 @@ +--- +paths: + - "src/**" +--- + +# Architecture Rules + +FSH is a **Modular Monolith** — NOT microservices, NOT a traditional layered architecture. + +## Core Principles + +### 1. Modular Monolith + +``` +Single deployment unit + ↓ +Multiple bounded contexts (modules) + ↓ +Each module is self-contained + ↓ +Communication via Contracts (interfaces/DTOs) +``` + +**Modules:** +- Identity (users, roles, permissions) +- Multitenancy (tenants, subscriptions) +- Auditing (audit trails) +- Your business modules (e.g., Catalog, Orders) + +**Rules:** +- Modules CANNOT reference other module internals +- Modules CAN reference other module Contracts +- Modules share BuildingBlocks (framework code) + +### 2. CQRS (Mediator Library) + +**Commands** (write operations): +```csharp +public record CreateUserCommand(string Email) : ICommand; + +public class CreateUserHandler : ICommandHandler +{ + public async ValueTask Handle(CreateUserCommand cmd, CancellationToken ct) + { + // Write to database + return user.Id; + } +} +``` + +**Queries** (read operations): +```csharp +public record GetUserQuery(Guid Id) : IQuery; + +public class GetUserHandler : IQueryHandler +{ + public async ValueTask Handle(GetUserQuery query, CancellationToken ct) + { + // Read from database + return userDto; + } +} +``` + +⚠️ **NOT MediatR:** FSH uses `Mediator` library (different interfaces!) + +### 3. Domain-Driven Design + +**Entities** inherit `BaseEntity`: +```csharp +public class Product : BaseEntity, IAuditable +{ + public string Name { get; private set; } = default!; + public Money Price { get; private set; } = default!; + + public static Product Create(string name, Money price) + { + // Factory method, enforce invariants + return new Product { Name = name, Price = price }; + } +} +``` + +**Value Objects** (immutable): +```csharp +public record Money(decimal Amount, string Currency); +``` + +**Aggregates:** +- Root entity controls access to child entities +- Enforce business rules +- Transaction boundary + +### 4. Multi-Tenancy + +**Finbuckle.MultiTenant:** +- Shared database, tenant isolation via TenantId +- Automatic query filtering +- Tenant resolution from HTTP header or claim + +```csharp +// Tenant-aware entity +public class Order : BaseEntity, IMustHaveTenant +{ + public Guid TenantId { get; set; } // Auto-filtered +} +``` + +**Tenant Resolution Order:** +1. HTTP header: `X-Tenant` +2. JWT claim: `tenant` +3. Host/route strategy (optional) + +### 5. Vertical Slice Architecture + +Each feature = complete slice (command/handler/validator/endpoint in one folder). + +``` +Features/v1/CreateProduct/ +├── CreateProductCommand.cs +├── CreateProductHandler.cs +├── CreateProductValidator.cs +└── CreateProductEndpoint.cs +``` + +**Benefits:** +- High cohesion (related code together) +- Low coupling (features don't depend on each other) +- Easy to find/modify + +### 6. BuildingBlocks (Shared Kernel) + +11 packages providing cross-cutting concerns: + +| Package | Purpose | +|---------|---------| +| Core | Base entities, interfaces, exceptions | +| Persistence | EF Core, repositories, specifications | +| Caching | Redis/memory caching | +| Mailing | Email templates, MailKit integration | +| Jobs | Hangfire background jobs | +| Storage | File storage (AWS S3, local) | +| Web | API conventions, filters, middleware | +| Eventing | Domain events, message bus | + +| Shared | DTOs, constants | +| Eventing.Abstractions | Event contracts | + +**Protected:** BuildingBlocks should NOT be modified without approval. See `.claude/rules/buildingblocks-protection.md`. + +### 7. Dependency Flow + +``` +API Layer (Minimal APIs) + ↓ +Application Layer (Commands/Queries/Handlers) + ↓ +Domain Layer (Entities/Value Objects) + ↓ +Infrastructure Layer (Persistence/External Services) +``` + +**Rules:** +- Domain CANNOT depend on infrastructure +- Application CANNOT depend on infrastructure directly +- Infrastructure implements domain interfaces + +### 8. Persistence Strategy + +**DbContext per Module:** +- IdentityDbContext +- MultitenancyDbContext +- AuditingDbContext +- Your module DbContexts + +**Repository Pattern:** +```csharp +public interface IRepository where T : BaseEntity +{ + Task GetByIdAsync(Guid id, CancellationToken ct); + Task> ListAsync(Specification spec, CancellationToken ct); + Task AddAsync(T entity, CancellationToken ct); + Task UpdateAsync(T entity, CancellationToken ct); + Task DeleteAsync(T entity, CancellationToken ct); +} +``` + +**Specification Pattern** (queries): +```csharp +public class ActiveProductsSpec : Specification +{ + public ActiveProductsSpec() + { + Query.Where(p => !p.IsDeleted && p.IsActive); + } +} +``` + +## Architectural Tests + +`Architecture.Tests` project enforces rules: + +```csharp +[Fact] +public void Modules_Should_Not_Reference_Other_Modules() +{ + // Fails if Module A references Module B directly +} + +[Fact] +public void Domain_Should_Not_Depend_On_Infrastructure() +{ + // Fails if domain entities reference EF Core +} +``` + +## Technology Stack + +- **.NET 10** (latest LTS) +- **EF Core 10** (PostgreSQL provider) +- **Mediator** (CQRS) +- **FluentValidation** (input validation) +- **Hangfire** (background jobs) +- **Finbuckle.MultiTenant** (multi-tenancy) +- **MailKit** (email) +- **Scalar** (OpenAPI docs) +- **Serilog** (logging) +- **OpenTelemetry** (observability) +- **Aspire** (orchestration) + +## Key Takeaways + +1. **Modular Monolith** ≠ Microservices. Modules share process, database, infrastructure. +2. **CQRS** separates reads/writes. Use `ICommand`/`IQuery`, not `IRequest`. +3. **DDD** enforces business rules in domain. Entities control their state. +4. **Multi-Tenancy** is built-in. Every entity is either tenant-aware or shared. +5. **Vertical Slices** keep features independent. No shared "services" layer. +6. **BuildingBlocks** provide infrastructure. Don't reinvent, reuse. +7. **Tests enforce architecture**. Violate rules → build fails. + +--- + +For implementation details, see: +- `ARCHITECTURE_ANALYSIS.md` (deep dive) +- `.claude/rules/modules.md` (module patterns) +- `.claude/rules/persistence.md` (data access patterns) diff --git a/.agents/rules/buildingblocks-protection.md b/.agents/rules/buildingblocks-protection.md new file mode 100644 index 0000000000..e50ed4bf5f --- /dev/null +++ b/.agents/rules/buildingblocks-protection.md @@ -0,0 +1,36 @@ +--- +paths: + - "src/BuildingBlocks/**/*" +--- + +# ⚠️ BuildingBlocks Protection + +**STOP. You are modifying BuildingBlocks.** + +Changes to BuildingBlocks affect ALL modules across the entire framework. These are core abstractions that many projects depend on. + +## Before Proceeding + +1. **Confirm explicit approval** - Has the user specifically approved this change? +2. **Consider alternatives** - Can this be done in the module instead? +3. **Assess impact** - What modules will this affect? + +## If Approved + +- Make minimal, focused changes +- Ensure backward compatibility +- Update all affected modules +- Run full test suite: `dotnet test src/FSH.Starter.slnx` +- Document the change + +## Alternatives to Consider + +| Instead of... | Consider... | +|---------------|-------------| +| Modifying Core | Extension method in module | +| Changing Persistence | Custom repository in module | +| Updating Web | Module-specific middleware | + +## If Not Approved + +Do not proceed. Suggest alternatives that don't require BuildingBlocks modifications. diff --git a/.agents/rules/modules.md b/.agents/rules/modules.md new file mode 100644 index 0000000000..b8358a4bd5 --- /dev/null +++ b/.agents/rules/modules.md @@ -0,0 +1,375 @@ +--- +paths: + - "src/Modules/**" +--- + +# Module Rules + +Modules are **bounded contexts** in the modular monolith. Each module is self-contained. + +## Module Structure + +``` +Modules/{ModuleName}/ +├── {ModuleName}.Contracts/ # Public interface (DTOs, events) +│ ├── {Entity}Dto.cs +│ ├── I{Module}Service.cs +│ └── {Module}Events.cs +├── {ModuleName}/ # Implementation (internal) +│ ├── Features/ # CQRS features +│ │ └── v1/{Feature}/ +│ │ ├── {Action}Command.cs +│ │ ├── {Action}Handler.cs +│ │ ├── {Action}Validator.cs +│ │ └── {Action}Endpoint.cs +│ ├── Entities/ # Domain models +│ ├── Persistence/ # DbContext, configurations +│ ├── Permissions/ # Permission constants +│ └── Extensions.cs # DI registration +``` + +## Module Independence + +### ✅ Allowed + +```csharp +// Reference Contracts project +using FSH.Modules.Identity.Contracts; + +public record UserDto(Guid Id, string Email); // Public DTO +``` + +```csharp +// Use BuildingBlocks +using FSH.BuildingBlocks.Core; +using FSH.BuildingBlocks.Persistence; +``` + +### ❌ Forbidden + +```csharp +// Direct reference to another module's internals +using FSH.Modules.Identity; // ❌ NO! Use .Contracts instead + +using FSH.Modules.Identity.Entities; // ❌ Domain models are internal +``` + +## Communication Between Modules + +### Option 1: Contracts (Preferred) + +**Identity.Contracts:** +```csharp +public interface IUserService +{ + Task GetUserByIdAsync(Guid userId); +} +``` + +**Identity implementation:** +```csharp +internal class UserService : IUserService +{ + public async Task GetUserByIdAsync(Guid userId) + { + // Query database + return userDto; + } +} +``` + +**Other module uses it:** +```csharp +public class OrderHandler(IUserService userService) +{ + public async ValueTask Handle(...) + { + var user = await userService.GetUserByIdAsync(userId); + } +} +``` + +### Option 2: Domain Events + +**Identity module raises event:** +```csharp +public record UserCreatedEvent(Guid UserId, string Email) : DomainEvent; + +// In handler +await eventBus.PublishAsync(new UserCreatedEvent(user.Id, user.Email)); +``` + +**Other module subscribes:** +```csharp +public class UserCreatedEventHandler : IEventHandler +{ + public async Task Handle(UserCreatedEvent evt, CancellationToken ct) + { + // React to user creation (e.g., send welcome email) + } +} +``` + +## Creating a New Module + +### 1. Create Projects + +```bash +# Contracts (public interface) +dotnet new classlib -n FSH.Modules.Catalog.Contracts -o src/Modules/Catalog/Modules.Catalog.Contracts + +# Implementation (internal) +dotnet new classlib -n FSH.Modules.Catalog -o src/Modules/Catalog/Modules.Catalog +``` + +### 2. Add to Solution + +```bash +dotnet sln src/FSH.Starter.slnx add \ + src/Modules/Catalog/Modules.Catalog.Contracts/Modules.Catalog.Contracts.csproj \ + src/Modules/Catalog/Modules.Catalog/Modules.Catalog.csproj +``` + +### 3. Reference BuildingBlocks + +```xml + + + + + + +``` + +### 4. Create Entities + +```csharp +namespace FSH.Modules.Catalog.Entities; + +public class Product : BaseEntity, IAuditable, IMustHaveTenant +{ + public string Name { get; private set; } = default!; + public string Description { get; private set; } = default!; + public Money Price { get; private set; } = default!; + public Guid TenantId { get; set; } + + public static Product Create(string name, string description, Money price) + { + return new Product + { + Name = name, + Description = description, + Price = price + }; + } + + public void Update(string name, string description, Money price) + { + Name = name; + Description = description; + Price = price; + } +} +``` + +### 5. Create DbContext + +```csharp +namespace FSH.Modules.Catalog.Persistence; + +public class CatalogDbContext(DbContextOptions options) + : BaseDbContext(options) +{ + public DbSet Products => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("catalog"); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(CatalogDbContext).Assembly); + base.OnModelCreating(modelBuilder); + } +} +``` + +### 6. Create Entity Configuration + +```csharp +namespace FSH.Modules.Catalog.Persistence.Configurations; + +public class ProductConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("products", "catalog"); + + builder.Property(p => p.Name) + .IsRequired() + .HasMaxLength(200); + + builder.OwnsOne(p => p.Price, price => + { + price.Property(m => m.Amount).HasColumnName("price_amount"); + price.Property(m => m.Currency).HasColumnName("price_currency"); + }); + } +} +``` + +### 7. Register Module (Extensions.cs) + +```csharp +namespace FSH.Modules.Catalog; + +public static class Extensions +{ + public static IServiceCollection AddCatalogModule(this IServiceCollection services) + { + // Register DbContext + services.AddDbContext(); + + // Register repositories + services.AddScoped, Repository>(); + + // Register services (if any) + // services.AddScoped(); + + return services; + } + + public static IEndpointRouteBuilder MapCatalogEndpoints(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/v1/catalog") + .WithTags("Catalog"); + + // Map feature endpoints here + // group.MapCreateProductEndpoint(); + + return endpoints; + } +} +``` + +### 8. Wire Up in Program.cs + +```csharp +// In FSH.Starter.Api/Program.cs +builder.Services.AddCatalogModule(); + +// ... + +app.MapCatalogEndpoints(); +``` + +## Module Boundaries + +### Namespace Convention + +- **Public:** `FSH.Modules.{Module}.Contracts` +- **Internal:** `FSH.Modules.{Module}.*` + +### Assembly Internals + +Mark module types as `internal` unless explicitly needed externally: + +```csharp +internal class ProductService { } // ✅ Internal by default +public record ProductDto { } // ✅ Public DTO in Contracts +``` + +### Dependency Direction + +``` +Other Modules → Module.Contracts + ↑ + Module (implements Contracts) + ↑ + BuildingBlocks +``` + +**Never:** +- Module A → Module B (direct reference) +- Module → Host (implementation referencing host) + +## Testing Modules + +**Architecture Test:** +```csharp +[Fact] +public void Catalog_Module_Should_Not_Reference_Identity_Module() +{ + var catalog = Types.InAssembly(typeof(CatalogDbContext).Assembly); + var identity = Types.InAssembly(typeof(IdentityDbContext).Assembly); + + catalog.Should().NotHaveDependencyOn(identity.Assemblies); +} +``` + +**Unit Test:** +```csharp +public class ProductTests +{ + [Fact] + public void Create_Should_Set_Properties() + { + var product = Product.Create("Test", "Description", new Money(100, "USD")); + + product.Name.Should().Be("Test"); + product.Price.Amount.Should().Be(100); + } +} +``` + +## Common Patterns + +### Permissions + +```csharp +namespace FSH.Modules.Catalog.Permissions; + +public static class CatalogPermissions +{ + public static class Products + { + public const string View = "catalog.products.view"; + public const string Create = "catalog.products.create"; + public const string Update = "catalog.products.update"; + public const string Delete = "catalog.products.delete"; + } +} +``` + +### DTOs (in Contracts) + +```csharp +namespace FSH.Modules.Catalog.Contracts; + +public record ProductDto( + Guid Id, + string Name, + string Description, + decimal Price, + string Currency, + DateTime CreatedAt); +``` + +### Events (in Contracts) + +```csharp +namespace FSH.Modules.Catalog.Contracts; + +public record ProductCreatedEvent(Guid ProductId, string Name) : DomainEvent; +public record ProductUpdatedEvent(Guid ProductId) : DomainEvent; +public record ProductDeletedEvent(Guid ProductId) : DomainEvent; +``` + +## Key Rules + +1. **Contracts are public**, internals are `internal` +2. **Modules communicate via Contracts or events**, never direct references +3. **Each module has its own DbContext** +4. **Features are vertical slices** within modules +5. **BuildingBlocks are shared**, modules are independent + +--- + +For scaffolding help: Use `/add-module` skill or `module-creator` agent. diff --git a/.agents/rules/persistence.md b/.agents/rules/persistence.md new file mode 100644 index 0000000000..d7b6ac66ac --- /dev/null +++ b/.agents/rules/persistence.md @@ -0,0 +1,431 @@ +--- +paths: + - "src/**/Persistence/**" + - "src/**/Entities/**" +--- + +# Persistence Rules + +EF Core patterns and repository usage in FSH. + +## DbContext Pattern + +### One DbContext Per Module + +```csharp +namespace FSH.Modules.Catalog.Persistence; + +public class CatalogDbContext(DbContextOptions options) + : BaseDbContext(options) +{ + public DbSet Products => Set(); + public DbSet Categories => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("catalog"); // ✅ Module-specific schema + modelBuilder.ApplyConfigurationsFromAssembly(typeof(CatalogDbContext).Assembly); + base.OnModelCreating(modelBuilder); + } +} +``` + +### BaseDbContext Features + +Inherited from `BuildingBlocks.Persistence`: +- Automatic tenant filtering +- Audit trail (Created/Modified timestamps) +- Soft delete support +- Domain event publishing + +## Entity Configuration + +### Use Fluent API (NOT Data Annotations) + +```csharp +namespace FSH.Modules.Catalog.Persistence.Configurations; + +public class ProductConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("products", "catalog"); + + // Primary key + builder.HasKey(p => p.Id); + + // Properties + builder.Property(p => p.Name) + .IsRequired() + .HasMaxLength(200); + + builder.Property(p => p.Description) + .HasMaxLength(2000); + + // Value object (owned type) + builder.OwnsOne(p => p.Price, price => + { + price.Property(m => m.Amount) + .HasColumnName("price_amount") + .HasPrecision(18, 2); + + price.Property(m => m.Currency) + .HasColumnName("price_currency") + .HasMaxLength(3); + }); + + // Relationships + builder.HasOne(p => p.Category) + .WithMany() + .HasForeignKey(p => p.CategoryId); + + // Indexes + builder.HasIndex(p => p.Name); + builder.HasIndex(p => p.TenantId); // ✅ For multi-tenancy + } +} +``` + +## Repository Pattern + +### Generic Repository (Provided by BuildingBlocks) + +```csharp +public interface IRepository where T : BaseEntity +{ + Task GetByIdAsync(Guid id, CancellationToken ct = default); + Task> ListAsync(CancellationToken ct = default); + Task> ListAsync(Specification spec, CancellationToken ct = default); + Task AddAsync(T entity, CancellationToken ct = default); + Task UpdateAsync(T entity, CancellationToken ct = default); + Task DeleteAsync(T entity, CancellationToken ct = default); + Task CountAsync(Specification spec, CancellationToken ct = default); + Task AnyAsync(Specification spec, CancellationToken ct = default); +} +``` + +### Usage in Handlers + +```csharp +public class CreateProductHandler(IRepository productRepo) + : ICommandHandler +{ + public async ValueTask Handle(CreateProductCommand cmd, CancellationToken ct) + { + var product = Product.Create(cmd.Name, cmd.Description, cmd.Price); + + await productRepo.AddAsync(product, ct); + + return product.Id; + } +} +``` + +## Specification Pattern + +### Creating Specifications + +```csharp +namespace FSH.Modules.Catalog.Specifications; + +public class ProductsByNameSpec : Specification +{ + public ProductsByNameSpec(string searchTerm) + { + Query + .Where(p => p.Name.Contains(searchTerm)) + .OrderBy(p => p.Name); + } +} + +public class ActiveProductsSpec : Specification +{ + public ActiveProductsSpec() + { + Query + .Where(p => !p.IsDeleted && p.IsActive) + .Include(p => p.Category) + .OrderByDescending(p => p.CreatedAt); + } +} +``` + +### Using Specifications + +```csharp +public class GetProductsHandler(IRepository repo) + : IQueryHandler> +{ + public async ValueTask> Handle(GetProductsQuery query, CancellationToken ct) + { + var spec = new ActiveProductsSpec(); + var products = await repo.ListAsync(spec, ct); + + return products.Select(p => p.ToDto()).ToList(); + } +} +``` + +### Pagination Specification + +```csharp +public class ProductsPaginatedSpec : Specification +{ + public ProductsPaginatedSpec(int pageNumber, int pageSize) + { + Query + .Where(p => !p.IsDeleted) + .OrderBy(p => p.Name) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize); + } +} +``` + +## Entity Base Classes + +### BaseEntity + +```csharp +public abstract class BaseEntity +{ + public Guid Id { get; set; } + public DateTime CreatedAt { get; set; } + public Guid? CreatedBy { get; set; } + public DateTime? ModifiedAt { get; set; } + public Guid? ModifiedBy { get; set; } +} +``` + +### IAuditable + +```csharp +public interface IAuditable +{ + DateTime CreatedAt { get; set; } + Guid? CreatedBy { get; set; } + DateTime? ModifiedAt { get; set; } + Guid? ModifiedBy { get; set; } +} +``` + +### IMustHaveTenant + +```csharp +public interface IMustHaveTenant +{ + Guid TenantId { get; set; } // ✅ Automatically filtered by Finbuckle +} +``` + +### ISoftDelete + +```csharp +public interface ISoftDelete +{ + bool IsDeleted { get; set; } + DateTime? DeletedAt { get; set; } + Guid? DeletedBy { get; set; } +} +``` + +## Multi-Tenancy + +### Tenant-Aware Entities + +```csharp +public class Order : BaseEntity, IAuditable, IMustHaveTenant +{ + public Guid TenantId { get; set; } // ✅ Required for tenant isolation + public string OrderNumber { get; private set; } = default!; + public decimal Total { get; private set; } + + // ... +} +``` + +### Global Query Filter (Automatic) + +BaseDbContext automatically applies: +```csharp +modelBuilder.Entity() + .HasQueryFilter(e => e.TenantId == currentTenantId); +``` + +**Result:** All queries automatically filter by current tenant. No need to add `.Where(x => x.TenantId == ...)` everywhere. + +### Shared Entities (No Tenant) + +```csharp +public class Country : BaseEntity // ❌ No IMustHaveTenant +{ + public string Name { get; private set; } = default!; + public string Code { get; private set; } = default!; +} +``` + +## Migrations + +### Creating Migrations + +```bash +# From solution root +dotnet ef migrations add InitialCatalog \ + --project src/Host/FSH.Starter.Migrations.PostgreSQL \ + --context CatalogDbContext \ + --output-dir Migrations/Catalog +``` + +### Applying Migrations + +```bash +# Automatic on startup (FSH.Starter.Api) +# Or manually: +dotnet ef database update \ + --project src/Host/FSH.Starter.Migrations.PostgreSQL \ + --context CatalogDbContext +``` + +### Migration Project Pattern + +FSH uses a separate migrations project (`Migrations.PostgreSQL`) to: +- Keep migrations out of module code +- Support multiple database providers +- Simplify deployment + +## Transactions + +### Implicit Transactions + +Commands automatically run in a transaction: +```csharp +public async ValueTask Handle(CreateOrderCommand cmd, CancellationToken ct) +{ + var order = Order.Create(...); + await orderRepo.AddAsync(order, ct); + + var payment = Payment.Create(...); + await paymentRepo.AddAsync(payment, ct); + + // ✅ Both saved in one transaction automatically + return order.Id; +} +``` + +### Explicit Transactions + +```csharp +await using var transaction = await dbContext.Database.BeginTransactionAsync(ct); + +try +{ + await orderRepo.AddAsync(order, ct); + await paymentRepo.AddAsync(payment, ct); + + await transaction.CommitAsync(ct); +} +catch +{ + await transaction.RollbackAsync(ct); + throw; +} +``` + +## Performance Patterns + +### Projection (DTO Mapping) + +```csharp +// ❌ Bad: Load full entity, map in memory +var products = await repo.ListAsync(spec, ct); +return products.Select(p => new ProductDto(...)).ToList(); + +// ✅ Good: Project in database +var query = dbContext.Products + .Where(p => !p.IsDeleted) + .Select(p => new ProductDto(p.Id, p.Name, p.Price.Amount)); +return await query.ToListAsync(ct); +``` + +### AsNoTracking for Read-Only + +```csharp +public class ProductsReadOnlySpec : Specification +{ + public ProductsReadOnlySpec() + { + Query + .AsNoTracking() // ✅ Faster for queries + .Where(p => !p.IsDeleted); + } +} +``` + +### Batch Operations + +```csharp +// ✅ Good: Batch delete +await dbContext.Products + .Where(p => p.CategoryId == categoryId) + .ExecuteDeleteAsync(ct); + +// ✅ Good: Batch update +await dbContext.Products + .Where(p => p.CategoryId == categoryId) + .ExecuteUpdateAsync(p => p.SetProperty(x => x.IsActive, false), ct); +``` + +## Common Pitfalls + +### ❌ Tracking Issues + +```csharp +// ❌ Don't detach entities manually +dbContext.Entry(product).State = EntityState.Detached; + +// ✅ Use repository pattern +await repo.UpdateAsync(product, ct); +``` + +### ❌ N+1 Queries + +```csharp +// ❌ Bad: N+1 +var orders = await repo.ListAsync(ct); +foreach (var order in orders) +{ + var customer = await customerRepo.GetByIdAsync(order.CustomerId, ct); // N queries! +} + +// ✅ Good: Eager loading +var spec = new OrdersWithCustomersSpec(); // Includes .Include(o => o.Customer) +var orders = await repo.ListAsync(spec, ct); +``` + +### ❌ Lazy Loading + +```csharp +// ❌ Lazy loading is DISABLED in FSH +var order = await repo.GetByIdAsync(orderId, ct); +var customer = order.Customer; // ❌ NULL! Not loaded + +// ✅ Explicit loading via specification +var spec = new OrderByIdWithCustomerSpec(orderId); +var order = await repo.FirstOrDefaultAsync(spec, ct); +var customer = order.Customer; // ✅ Loaded +``` + +## Key Rules + +1. **One DbContext per module**, separate schemas +2. **Fluent API for configuration**, not data annotations +3. **Repository pattern for writes**, direct DbContext for complex reads +4. **Specifications for reusable queries** +5. **Tenant isolation is automatic** (via IMustHaveTenant) +6. **Migrations in separate project** (Migrations.PostgreSQL) +7. **AsNoTracking for read-only queries** +8. **Project to DTOs in database** (avoid loading full entities) + +--- + +For migration help: Use `migration-helper` agent or see EF Core docs. diff --git a/.agents/rules/testing-rules.md b/.agents/rules/testing-rules.md new file mode 100644 index 0000000000..c7018746e7 --- /dev/null +++ b/.agents/rules/testing-rules.md @@ -0,0 +1,77 @@ +--- +paths: + - "src/Tests/**/*" +--- + +# Testing Rules + +Rules for tests in FSH. + +## Test Organization + +``` +src/Tests/ +├── Architecture.Tests/ # Layering enforcement (mandatory) +├── {Module}.Tests/ # Module-specific tests +└── Generic.Tests/ # Shared utilities +``` + +## Naming Conventions + +| Type | Pattern | +|------|---------| +| Test class | `{ClassUnderTest}Tests` | +| Test method | `{Method}_{Scenario}_{ExpectedResult}` | +| Test file | Same as class name | + +## Test Structure + +Always use Arrange-Act-Assert: + +```csharp +[Fact] +public async Task Handle_ValidCommand_ReturnsId() +{ + // Arrange + var command = new CreateProductCommand("Test", 10m); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Id.Should().NotBeEmpty(); +} +``` + +## Required Tests + +### For Handlers +- Happy path with valid input +- Edge cases (empty, null, boundary values) +- Repository interactions verified + +### For Validators +- Each validation rule has a test +- Valid input passes +- Invalid input fails with correct property + +### For Entities +- Factory method creates valid entity +- Invalid input throws appropriate exception +- Domain events raised correctly + +## Libraries + +- **xUnit** - Test framework +- **FluentAssertions** - `.Should()` assertions +- **Moq** - `Mock` for dependencies + +## Architecture Tests + +Architecture tests in `Architecture.Tests/` are mandatory and enforce: +- Module boundary isolation +- No cross-module internal dependencies +- Handlers/validators are sealed +- Contracts don't depend on implementations + +These run on every build and PR. diff --git a/.agents/skills/add-entity/SKILL.md b/.agents/skills/add-entity/SKILL.md new file mode 100644 index 0000000000..9096d4eaef --- /dev/null +++ b/.agents/skills/add-entity/SKILL.md @@ -0,0 +1,164 @@ +--- +name: add-entity +description: Create a domain entity with multi-tenancy, auditing, soft-delete, and domain events. Use when adding new database entities to a module. +argument-hint: [ModuleName] [EntityName] +--- + +# Add Entity + +Create a domain entity following FSH patterns with full multi-tenancy support. + +## Entity Template + +```csharp +public sealed class {Entity} : AggregateRoot, IHasTenant, IAuditableEntity, ISoftDeletable +{ + // Domain properties + public string Name { get; private set; } = null!; + public decimal Price { get; private set; } + public string? Description { get; private set; } + + // IHasTenant - automatic tenant isolation + public string TenantId { get; private set; } = null!; + + // IAuditableEntity - automatic audit trails + public DateTimeOffset CreatedAt { get; set; } + public string? CreatedBy { get; set; } + public DateTimeOffset? LastModifiedAt { get; set; } + public string? LastModifiedBy { get; set; } + + // ISoftDeletable - automatic soft deletes + public DateTimeOffset? DeletedAt { get; set; } + public string? DeletedBy { get; set; } + + // Private constructor for EF Core + private {Entity}() { } + + // Factory method - the only way to create + public static {Entity} Create(string name, decimal price, string tenantId) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(price); + + var entity = new {Entity} + { + Id = Guid.NewGuid(), + Name = name, + Price = price, + TenantId = tenantId + }; + + entity.AddDomainEvent(new {Entity}CreatedEvent(entity.Id)); + return entity; + } + + // Domain methods for state changes + public void UpdateDetails(string name, decimal price, string? description) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(price); + + Name = name; + Price = price; + Description = description; + + AddDomainEvent(new {Entity}UpdatedEvent(Id)); + } +} +``` + +## Domain Events + +```csharp +public sealed record {Entity}CreatedEvent(Guid {Entity}Id) : IDomainEvent; +public sealed record {Entity}UpdatedEvent(Guid {Entity}Id) : IDomainEvent; +public sealed record {Entity}DeletedEvent(Guid {Entity}Id) : IDomainEvent; +``` + +## EF Core Configuration + +```csharp +public sealed class {Entity}Configuration : IEntityTypeConfiguration<{Entity}> +{ + public void Configure(EntityTypeBuilder<{Entity}> builder) + { + builder.ToTable("{entities}"); + + builder.HasKey(x => x.Id); + + builder.Property(x => x.Name) + .IsRequired() + .HasMaxLength(200); + + builder.Property(x => x.Price) + .HasPrecision(18, 2); + + builder.Property(x => x.TenantId) + .IsRequired() + .HasMaxLength(64); + + builder.HasIndex(x => x.TenantId); + + // Global query filter for soft-delete + builder.HasQueryFilter(x => x.DeletedAt == null); + } +} +``` + +## Register in DbContext + +```csharp +public sealed class {Module}DbContext : DbContext +{ + public DbSet<{Entity}> {Entities} => Set<{Entity}>(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("{module}"); + modelBuilder.ApplyConfigurationsFromAssembly(typeof({Module}DbContext).Assembly); + } +} +``` + +## Add Migration + +```bash +dotnet ef migrations add Add{Entity} \ + --project src/Host/FSH.Starter.Migrations.PostgreSQL \ + --startup-project src/Host/FSH.Starter.Api + +dotnet ef database update \ + --project src/Host/FSH.Starter.Migrations.PostgreSQL \ + --startup-project src/Host/FSH.Starter.Api +``` + +## Interfaces Reference + +| Interface | Purpose | Auto-Handled | +|-----------|---------|--------------| +| `IHasTenant` | Tenant isolation | Query filtering | +| `IAuditableEntity` | Created/Modified tracking | SaveChanges interceptor | +| `ISoftDeletable` | Soft delete support | Delete interceptor | +| `AggregateRoot` | Domain events support | Event dispatcher | + +## Key Rules + +1. **Private constructor** - EF Core needs it, but users use factory methods +2. **Factory methods** - All creation goes through `Create()` static method +3. **Domain methods** - State changes through methods, not property setters +4. **Domain events** - Raise events for significant state changes +5. **Validation in methods** - Validate in factory/domain methods, not entity +6. **No public setters** - Properties are `private set` + +## Checklist + +- [ ] Implements `AggregateRoot` +- [ ] Implements `IHasTenant` for tenant isolation +- [ ] Implements `IAuditableEntity` for audit trails +- [ ] Implements `ISoftDeletable` for soft deletes +- [ ] Has private constructor +- [ ] Has static factory method +- [ ] Domain events raised for state changes +- [ ] EF configuration created +- [ ] Added to DbContext +- [ ] Migration created diff --git a/.agents/skills/add-feature/SKILL.md b/.agents/skills/add-feature/SKILL.md new file mode 100644 index 0000000000..671cde2164 --- /dev/null +++ b/.agents/skills/add-feature/SKILL.md @@ -0,0 +1,117 @@ +--- +name: add-feature +description: Create a new API endpoint with Command, Handler, Validator, and Endpoint following FSH vertical slice architecture. Use when adding any new feature, API endpoint, or business operation. +argument-hint: [ModuleName] [FeatureName] +--- + +# Add Feature + +Create a complete vertical slice feature with all required files. + +## File Structure + +``` +src/Modules/{Module}/Features/v1/{FeatureName}/ +├── {Action}{Entity}Command.cs # or Get{Entity}Query.cs +├── {Action}{Entity}Handler.cs +├── {Action}{Entity}Validator.cs # Commands only +└── {Action}{Entity}Endpoint.cs +``` + +## Step 1: Create Command or Query + +**For state changes (POST/PUT/DELETE):** +```csharp +public sealed record Create{Entity}Command( + string Name, + decimal Price) : ICommand; +``` + +**For reads (GET):** +```csharp +public sealed record Get{Entity}Query(Guid Id) : IQuery<{Entity}Dto>; +``` + +## Step 2: Create Handler + +```csharp +public sealed class Create{Entity}Handler( + IRepository<{Entity}> repository, + ICurrentUser currentUser) : ICommandHandler +{ + public async ValueTask Handle( + Create{Entity}Command command, + CancellationToken ct) + { + var entity = {Entity}.Create(command.Name, command.Price, currentUser.TenantId); + await repository.AddAsync(entity, ct); + return new Create{Entity}Response(entity.Id); + } +} +``` + +## Step 3: Create Validator (Commands Only) + +```csharp +public sealed class Create{Entity}Validator : AbstractValidator +{ + public Create{Entity}Validator() + { + RuleFor(x => x.Name).NotEmpty().MaximumLength(200); + RuleFor(x => x.Price).GreaterThan(0); + } +} +``` + +## Step 4: Create Endpoint + +```csharp +public static class Create{Entity}Endpoint +{ + public static RouteHandlerBuilder Map(this IEndpointRouteBuilder endpoints) => + endpoints.MapPost("/", async ( + Create{Entity}Command command, + IMediator mediator, + CancellationToken ct) => TypedResults.Created( + $"/{entities}/{(await mediator.Send(command, ct)).Id}")) + .WithName(nameof(Create{Entity}Command)) + .WithSummary("Create a new {entity}") + .RequirePermission({Module}Permissions.{Entities}.Create); +} +``` + +## Step 5: Add DTOs to Contracts + +In `src/Modules/{Module}/Modules.{Module}.Contracts/`: + +```csharp +public sealed record Create{Entity}Response(Guid Id); +public sealed record {Entity}Dto(Guid Id, string Name, decimal Price); +``` + +## Step 6: Wire Endpoint in Module + +In `{Module}Module.cs` MapEndpoints method: + +```csharp +var entities = endpoints.MapGroup("/{entities}").WithTags("{Entities}"); +entities.Map{Action}{Entity}Endpoint(); +``` + +## Step 7: Verify + +```bash +dotnet build src/FSH.Starter.slnx # Must be 0 warnings +dotnet test src/FSH.Starter.slnx +``` + +## Checklist + +- [ ] Command/Query uses `ICommand` or `IQuery` (NOT MediatR's IRequest) +- [ ] Handler uses `ICommandHandler` or `IQueryHandler` +- [ ] Handler returns `ValueTask` (NOT `Task`) +- [ ] Validator exists for commands +- [ ] Endpoint has `.RequirePermission()` or `.AllowAnonymous()` +- [ ] Endpoint has `.WithName()` and `.WithSummary()` +- [ ] DTOs in Contracts project, not internal +- [ ] Build passes with 0 warnings diff --git a/.agents/skills/add-module/SKILL.md b/.agents/skills/add-module/SKILL.md new file mode 100644 index 0000000000..8887f52cef --- /dev/null +++ b/.agents/skills/add-module/SKILL.md @@ -0,0 +1,176 @@ +--- +name: add-module +description: Create a new module (bounded context) with proper project structure, permissions, DbContext, and registration. Use when adding a new business domain that needs its own entities and endpoints. +argument-hint: [ModuleName] +--- + +# Add Module + +Create a new bounded context with full project structure. + +## When to Create a New Module + +- Has its own domain entities +- Could be deployed independently +- Represents a distinct business domain + +If it's just a feature in an existing domain, use `add-feature` instead. + +## Project Structure + +``` +src/Modules/{Name}/ +├── Modules.{Name}/ +│ ├── Modules.{Name}.csproj +│ ├── {Name}Module.cs +│ ├── {Name}PermissionConstants.cs +│ ├── {Name}DbContext.cs +│ ├── Domain/ +│ │ └── {Entity}.cs +│ └── Features/v1/ +│ └── {Feature}/ +└── Modules.{Name}.Contracts/ + ├── Modules.{Name}.Contracts.csproj + └── DTOs/ +``` + +## Step 1: Create Projects + +### Main Module Project +`src/Modules/{Name}/Modules.{Name}/Modules.{Name}.csproj`: +```xml + + + net10.0 + + + + + + + + +``` + +### Contracts Project +`src/Modules/{Name}/Modules.{Name}.Contracts/Modules.{Name}.Contracts.csproj`: +```xml + + + net10.0 + + +``` + +## Step 2: Implement IModule + +```csharp +public sealed class {Name}Module : IModule +{ + public void ConfigureServices(IHostApplicationBuilder builder) + { + // Register DbContext + builder.Services.AddDbContext<{Name}DbContext>((sp, options) => + { + var dbOptions = sp.GetRequiredService>().Value; + options.UseNpgsql(dbOptions.ConnectionString); + }); + + // Register repositories + builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); + builder.Services.AddScoped(typeof(IReadRepository<>), typeof(Repository<>)); + } + + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/v1/{name}"); + // Map feature endpoints here + } +} +``` + +## Step 3: Add Permission Constants + +```csharp +public static class {Name}PermissionConstants +{ + public static class {Entities} + { + public const string View = "{Entities}.View"; + public const string Create = "{Entities}.Create"; + public const string Update = "{Entities}.Update"; + public const string Delete = "{Entities}.Delete"; + } +} +``` + +## Step 4: Create DbContext + +```csharp +public sealed class {Name}DbContext : DbContext +{ + public {Name}DbContext(DbContextOptions<{Name}DbContext> options) : base(options) { } + + public DbSet<{Entity}> {Entities} => Set<{Entity}>(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("{name}"); + modelBuilder.ApplyConfigurationsFromAssembly(typeof({Name}DbContext).Assembly); + } +} +``` + +## Step 5: Register in Program.cs + +```csharp +// Add to moduleAssemblies array +var moduleAssemblies = new Assembly[] +{ + typeof(IdentityModule).Assembly, + typeof(MultitenancyModule).Assembly, + typeof(AuditingModule).Assembly, + typeof({Name}Module).Assembly, // Add here +}; + +// Add Mediator assemblies +builder.Services.AddMediator(o => +{ + o.Assemblies = [ + // ... existing + typeof({Name}Module).Assembly, + ]; +}); +``` + +## Step 6: Add to Solution + +```bash +dotnet sln src/FSH.Starter.slnx add src/Modules/{Name}/Modules.{Name}/Modules.{Name}.csproj +dotnet sln src/FSH.Starter.slnx add src/Modules/{Name}/Modules.{Name}.Contracts/Modules.{Name}.Contracts.csproj +``` + +## Step 7: Reference from API + +In `src/Host/FSH.Starter.Api/FSH.Starter.Api.csproj`: +```xml + +``` + +## Step 8: Verify + +```bash +dotnet build src/FSH.Starter.slnx # Must be 0 warnings +dotnet test src/FSH.Starter.slnx +``` + +## Checklist + +- [ ] Both projects created (main + contracts) +- [ ] IModule implemented with ConfigureServices and MapEndpoints +- [ ] Permission constants defined +- [ ] DbContext created with proper schema +- [ ] Registered in Program.cs moduleAssemblies +- [ ] Added to solution file +- [ ] Referenced from FSH.Starter.Api +- [ ] Build passes with 0 warnings diff --git a/.agents/skills/mediator-reference/SKILL.md b/.agents/skills/mediator-reference/SKILL.md new file mode 100644 index 0000000000..295fccc3dd --- /dev/null +++ b/.agents/skills/mediator-reference/SKILL.md @@ -0,0 +1,129 @@ +--- +name: mediator-reference +description: Mediator library patterns and interfaces for FSH. This project uses the Mediator source generator, NOT MediatR. Reference when implementing commands, queries, and handlers. +user-invocable: false +--- + +# Mediator Reference + +⚠️ **FSH uses the `Mediator` source generator library, NOT `MediatR`.** + +These are different libraries with different interfaces. Using MediatR interfaces will cause build errors. + +## Interface Comparison + +| Purpose | ✅ Mediator (Use This) | ❌ MediatR (Don't Use) | +|---------|------------------------|------------------------| +| Command | `ICommand` | `IRequest` | +| Query | `IQuery` | `IRequest` | +| Command Handler | `ICommandHandler` | `IRequestHandler` | +| Query Handler | `IQueryHandler` | `IRequestHandler` | +| Notification | `INotification` | `INotification` | +| Notification Handler | `INotificationHandler` | `INotificationHandler` | + +## Command Pattern + +```csharp +// ✅ Correct - Mediator +public sealed record CreateUserCommand(string Email, string Name) : ICommand; + +public sealed class CreateUserHandler : ICommandHandler +{ + public async ValueTask Handle(CreateUserCommand command, CancellationToken ct) + { + // Implementation + } +} + +// ❌ Wrong - MediatR +public sealed record CreateUserCommand(string Email, string Name) : IRequest; + +public sealed class CreateUserHandler : IRequestHandler +{ + public async Task Handle(CreateUserCommand request, CancellationToken ct) + { + // This won't work! + } +} +``` + +## Query Pattern + +```csharp +// ✅ Correct - Mediator +public sealed record GetUserQuery(Guid Id) : IQuery; + +public sealed class GetUserHandler : IQueryHandler +{ + public async ValueTask Handle(GetUserQuery query, CancellationToken ct) + { + // Implementation + } +} +``` + +## Key Differences + +| Aspect | Mediator | MediatR | +|--------|----------|---------| +| Return type | `ValueTask` | `Task` | +| Parameter name | `command` / `query` | `request` | +| Registration | Source generated | Runtime reflection | +| Performance | Faster (compile-time) | Slower (runtime) | + +## Sending Commands/Queries + +```csharp +// In endpoint +public static async Task Handle( + CreateUserCommand command, + IMediator mediator, // Same interface name as MediatR + CancellationToken ct) +{ + var result = await mediator.Send(command, ct); + return TypedResults.Created($"/users/{result}"); +} +``` + +## Registration + +```csharp +// In Program.cs +builder.Services.AddMediator(options => +{ + options.Assemblies = + [ + typeof(IdentityModule).Assembly, + typeof(MultitenancyModule).Assembly, + // Add your module assemblies here + ]; +}); +``` + +## Common Errors + +### Error: `IRequest` not found +**Cause:** Using MediatR interface +**Fix:** Change to `ICommand` or `IQuery` + +### Error: `IRequestHandler` not found +**Cause:** Using MediatR interface +**Fix:** Change to `ICommandHandler` or `IQueryHandler` + +### Error: Handler not found at runtime +**Cause:** Assembly not registered in AddMediator +**Fix:** Add assembly to `options.Assemblies` array + +### Error: `Task` vs `ValueTask` +**Cause:** Using MediatR return type +**Fix:** Change handler return type to `ValueTask` + +## Namespaces + +```csharp +// ✅ Correct +using Mediator; + +// ❌ Wrong +using MediatR; +``` diff --git a/.agents/skills/query-patterns/SKILL.md b/.agents/skills/query-patterns/SKILL.md new file mode 100644 index 0000000000..f2ce63cacb --- /dev/null +++ b/.agents/skills/query-patterns/SKILL.md @@ -0,0 +1,176 @@ +--- +name: query-patterns +description: Query patterns including pagination, search, filtering, and specifications for FSH. Use when implementing GET endpoints that return lists or need filtering. +--- + +# Query Patterns + +Reference for implementing queries with pagination, search, and filtering. + +## Basic Paginated Query + +```csharp +// Query +public sealed record Get{Entities}Query( + string? Search, + int PageNumber = 1, + int PageSize = 10) : IQuery>; + +// Handler +public sealed class Get{Entities}Handler( + IReadRepository<{Entity}> repository) : IQueryHandler> +{ + public async ValueTask> Handle( + Get{Entities}Query query, + CancellationToken ct) + { + var spec = new {Entity}SearchSpec(query.Search, query.PageNumber, query.PageSize); + return await repository.PaginatedListAsync(spec, ct); + } +} +``` + +## Specification Pattern + +```csharp +public sealed class {Entity}SearchSpec : EntitiesByPaginationFilterSpec<{Entity}, {Entity}Dto> +{ + public {Entity}SearchSpec(string? search, int pageNumber, int pageSize) + : base(new PaginationFilter(pageNumber, pageSize)) + { + Query + .OrderByDescending(x => x.CreatedAt) + .Where(x => string.IsNullOrEmpty(search) || + x.Name.Contains(search) || + x.Description!.Contains(search)); + } +} +``` + +## Get Single Entity + +```csharp +// Query +public sealed record Get{Entity}Query(Guid Id) : IQuery<{Entity}Dto>; + +// Handler +public sealed class Get{Entity}Handler( + IReadRepository<{Entity}> repository) : IQueryHandler +{ + public async ValueTask<{Entity}Dto> Handle(Get{Entity}Query query, CancellationToken ct) + { + var spec = new {Entity}ByIdSpec(query.Id); + var entity = await repository.FirstOrDefaultAsync(spec, ct); + + return entity ?? throw new NotFoundException($"{Entity} {query.Id} not found"); + } +} + +// Specification +public sealed class {Entity}ByIdSpec : Specification<{Entity}, {Entity}Dto>, ISingleResultSpecification<{Entity}> +{ + public {Entity}ByIdSpec(Guid id) + { + Query.Where(x => x.Id == id); + } +} +``` + +## Advanced Filtering + +```csharp +public sealed record Get{Entities}Query( + string? Search, + Guid? CategoryId, + decimal? MinPrice, + decimal? MaxPrice, + DateTimeOffset? CreatedAfter, + bool? IsActive, + string? SortBy, + bool SortDescending = false, + int PageNumber = 1, + int PageSize = 10) : IQuery>; + +public sealed class {Entity}FilterSpec : EntitiesByPaginationFilterSpec<{Entity}, {Entity}Dto> +{ + public {Entity}FilterSpec(Get{Entities}Query query) + : base(new PaginationFilter(query.PageNumber, query.PageSize)) + { + Query + .Where(x => string.IsNullOrEmpty(query.Search) || x.Name.Contains(query.Search)) + .Where(x => !query.CategoryId.HasValue || x.CategoryId == query.CategoryId) + .Where(x => !query.MinPrice.HasValue || x.Price >= query.MinPrice) + .Where(x => !query.MaxPrice.HasValue || x.Price <= query.MaxPrice) + .Where(x => !query.IsActive.HasValue || x.IsActive == query.IsActive); + + ApplySorting(query.SortBy, query.SortDescending); + } + + private void ApplySorting(string? sortBy, bool descending) + { + switch (sortBy?.ToLowerInvariant()) + { + case "name": + if (descending) Query.OrderByDescending(x => x.Name); + else Query.OrderBy(x => x.Name); + break; + case "price": + if (descending) Query.OrderByDescending(x => x.Price); + else Query.OrderBy(x => x.Price); + break; + default: + Query.OrderByDescending(x => x.CreatedAt); + break; + } + } +} +``` + +## Endpoint Patterns + +### List Endpoint +```csharp +public static RouteHandlerBuilder MapGet{Entities}Endpoint(this IEndpointRouteBuilder endpoints) => + endpoints.MapGet("/", async ( + [AsParameters] Get{Entities}Query query, + IMediator mediator, + CancellationToken ct) => TypedResults.Ok(await mediator.Send(query, ct))) + .WithName(nameof(Get{Entities}Query)) + .WithSummary("Get paginated list of {entities}") + .RequirePermission({Module}Permissions.{Entities}.View); +``` + +### Single Entity Endpoint +```csharp +public static RouteHandlerBuilder MapGet{Entity}Endpoint(this IEndpointRouteBuilder endpoints) => + endpoints.MapGet("/{id:guid}", async ( + Guid id, + IMediator mediator, + CancellationToken ct) => TypedResults.Ok(await mediator.Send(new Get{Entity}Query(id), ct))) + .WithName(nameof(Get{Entity}Query)) + .WithSummary("Get {entity} by ID") + .RequirePermission({Module}Permissions.{Entities}.View); +``` + +## Response Types + +```csharp +// In Contracts project +public sealed record {Entity}Dto( + Guid Id, + string Name, + decimal Price, + string? Description, + DateTimeOffset CreatedAt); + +// PagedList is from BuildingBlocks +// Returns: Items, PageNumber, PageSize, TotalCount, TotalPages +``` + +## Key Points + +1. **Use specifications** - Don't write raw LINQ in handlers +2. **Tenant filtering is automatic** - Framework handles `IHasTenant` +3. **Soft delete filtering is automatic** - DeletedAt != null filtered out +4. **Use `[AsParameters]`** - For query parameters in endpoints +5. **Project to DTOs** - Never return entities directly diff --git a/.agents/skills/testing-guide/SKILL.md b/.agents/skills/testing-guide/SKILL.md new file mode 100644 index 0000000000..67cc6bae52 --- /dev/null +++ b/.agents/skills/testing-guide/SKILL.md @@ -0,0 +1,223 @@ +--- +name: testing-guide +description: Write unit tests, integration tests, and architecture tests for FSH features. Use when adding tests or understanding the testing strategy. +--- + +# Testing Guide + +FSH uses a layered testing strategy with architecture tests as guardrails. + +## Test Project Structure + +``` +src/Tests/ +├── Architecture.Tests/ # Enforces layering rules +├── Generic.Tests/ # Shared test utilities +├── Identity.Tests/ # Identity module tests +├── Multitenancy.Tests/ # Multitenancy module tests +└── Auditing.Tests/ # Auditing module tests +``` + +## Architecture Tests + +Architecture tests enforce module boundaries and layering. They run on every build. + +```csharp +public class ArchitectureTests +{ + [Fact] + public void Modules_ShouldNot_DependOnOtherModules() + { + var result = Types.InAssembly(typeof(IdentityModule).Assembly) + .ShouldNot() + .HaveDependencyOn("Modules.Multitenancy") + .GetResult(); + + result.IsSuccessful.Should().BeTrue(); + } + + [Fact] + public void Contracts_ShouldNot_DependOnImplementation() + { + var result = Types.InAssembly(typeof(UserDto).Assembly) + .ShouldNot() + .HaveDependencyOn("Modules.Identity") + .GetResult(); + + result.IsSuccessful.Should().BeTrue(); + } + + [Fact] + public void Handlers_ShouldBe_Sealed() + { + var result = Types.InAssembly(typeof(IdentityModule).Assembly) + .That() + .ImplementInterface(typeof(ICommandHandler<,>)) + .Or() + .ImplementInterface(typeof(IQueryHandler<,>)) + .Should() + .BeSealed() + .GetResult(); + + result.IsSuccessful.Should().BeTrue(); + } +} +``` + +## Unit Test Patterns + +### Handler Tests + +```csharp +public class Create{Entity}HandlerTests +{ + private readonly Mock> _repositoryMock; + private readonly Mock _currentUserMock; + private readonly Create{Entity}Handler _handler; + + public Create{Entity}HandlerTests() + { + _repositoryMock = new Mock>(); + _currentUserMock = new Mock(); + _currentUserMock.Setup(x => x.TenantId).Returns("test-tenant"); + + _handler = new Create{Entity}Handler( + _repositoryMock.Object, + _currentUserMock.Object); + } + + [Fact] + public async Task Handle_ValidCommand_Returns{Entity}Id() + { + // Arrange + var command = new Create{Entity}Command("Test", 99.99m); + _repositoryMock + .Setup(x => x.AddAsync(It.IsAny<{Entity}>(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Id.Should().NotBeEmpty(); + _repositoryMock.Verify(x => x.AddAsync( + It.Is<{Entity}>(e => e.Name == "Test" && e.Price == 99.99m), + It.IsAny()), Times.Once); + } +} +``` + +### Validator Tests + +```csharp +public class Create{Entity}ValidatorTests +{ + private readonly Create{Entity}Validator _validator = new(); + + [Fact] + public void Validate_EmptyName_Fails() + { + var command = new Create{Entity}Command("", 99.99m); + var result = _validator.Validate(command); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.PropertyName == "Name"); + } + + [Fact] + public void Validate_NegativePrice_Fails() + { + var command = new Create{Entity}Command("Test", -1m); + var result = _validator.Validate(command); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.PropertyName == "Price"); + } + + [Theory] + [InlineData("Valid Name", 10)] + [InlineData("Another", 0.01)] + public void Validate_ValidCommand_Passes(string name, decimal price) + { + var command = new Create{Entity}Command(name, price); + var result = _validator.Validate(command); + + result.IsValid.Should().BeTrue(); + } +} +``` + +### Entity Tests + +```csharp +public class {Entity}Tests +{ + [Fact] + public void Create_ValidInput_Creates{Entity}WithEvent() + { + var entity = {Entity}.Create("Test", 99.99m, "tenant-1"); + + entity.Id.Should().NotBeEmpty(); + entity.Name.Should().Be("Test"); + entity.Price.Should().Be(99.99m); + entity.TenantId.Should().Be("tenant-1"); + entity.DomainEvents.Should().ContainSingle(e => e is {Entity}CreatedEvent); + } + + [Fact] + public void Create_EmptyName_ThrowsArgumentException() + { + var act = () => {Entity}.Create("", 99.99m, "tenant-1"); + + act.Should().Throw(); + } + + [Fact] + public void UpdateDetails_ValidInput_UpdatesAndRaisesEvent() + { + var entity = {Entity}.Create("Original", 50m, "tenant-1"); + entity.ClearDomainEvents(); + + entity.UpdateDetails("Updated", 75m, "New description"); + + entity.Name.Should().Be("Updated"); + entity.Price.Should().Be(75m); + entity.Description.Should().Be("New description"); + entity.DomainEvents.Should().ContainSingle(e => e is {Entity}UpdatedEvent); + } +} +``` + +## Running Tests + +```bash +# Run all tests +dotnet test src/FSH.Starter.slnx + +# Run specific test project +dotnet test src/Tests/Architecture.Tests + +# Run with coverage +dotnet test src/FSH.Starter.slnx --collect:"XPlat Code Coverage" + +# Run specific test +dotnet test --filter "FullyQualifiedName~Create{Entity}HandlerTests" +``` + +## Test Conventions + +| Convention | Example | +|------------|---------| +| Test class name | `{ClassUnderTest}Tests` | +| Test method name | `{Method}_{Scenario}_{ExpectedResult}` | +| Structure | Always Arrange-Act-Assert | +| Assertions | Multiple asserts OK if same concept | + +## Key Rules + +1. **Architecture tests are mandatory** - They enforce module boundaries +2. **Validators need tests** - Cover edge cases +3. **Handlers need tests** - Mock dependencies +4. **Entities need tests** - Test factory methods and domain logic +5. **Use FluentAssertions** - `.Should()` syntax +6. **Use Moq for mocking** - `Mock` pattern diff --git a/.agents/workflows/architecture-guard.md b/.agents/workflows/architecture-guard.md new file mode 100644 index 0000000000..091fc00159 --- /dev/null +++ b/.agents/workflows/architecture-guard.md @@ -0,0 +1,118 @@ +--- +description: Verify changes don't violate architecture rules. Run architecture tests, check module boundaries, verify BuildingBlocks aren't modified. Use before commits or PRs. +--- + +You are an architecture guardian for FullStackHero .NET Starter Kit. Your job is to verify architectural integrity. You are READ-ONLY — never modify files. + +## Verification Steps + +### 1. Check for BuildingBlocks Modifications + +```bash +git diff --name-only | grep -E "^src/BuildingBlocks/" +``` + +If any files listed: **STOP** - BuildingBlocks changes require explicit approval. + +### 2. Run Architecture Tests + +```bash +dotnet test src/Tests/Architecture.Tests --no-build +``` + +All tests must pass. + +### 3. Verify Build Has 0 Warnings + +```bash +dotnet build src/FSH.Starter.slnx 2>&1 | grep -E "warning|error" +``` + +Must show no warnings or errors. + +### 4. Check Module Boundaries + +Verify no cross-module internal dependencies: + +```bash +# Check if any module references another module's internal types +grep -r "using Modules\." src/Modules/ --include="*.cs" | grep -v "\.Contracts" +``` + +Should only show references to `.Contracts` namespaces. + +### 5. Verify Mediator Usage + +```bash +# Check for MediatR usage (should be empty) +grep -r "MediatR\|IRequest<\|IRequestHandler<" src/Modules/ --include="*.cs" +``` + +Must be empty - all should use Mediator interfaces. + +### 6. Check Validator Coverage + +For each command, verify a validator exists: + +```bash +# List commands +find src/Modules -name "*Command.cs" -type f + +# List validators +find src/Modules -name "*Validator.cs" -type f +``` + +Every command needs a corresponding validator. + +### 7. Check Endpoint Authorization + +```bash +# Find endpoints without authorization +grep -r "\.Map\(Get\|Post\|Put\|Delete\)" src/Modules/ --include="*.cs" -A 5 | \ +grep -v "RequirePermission\|AllowAnonymous" +``` + +Every endpoint must have explicit authorization. + +## Output Format + +``` +## Architecture Verification Report + +### BuildingBlocks +✅ No modifications | ⚠️ MODIFIED - Requires approval + +### Architecture Tests +✅ All passed | ❌ {count} failed + +### Build Warnings +✅ 0 warnings | ❌ {count} warnings + +### Module Boundaries +✅ Clean | ❌ Cross-module dependencies found + +### Mediator Usage +✅ Correct | ❌ MediatR interfaces detected + +### Validators +✅ All commands have validators | ❌ Missing: {list} + +### Authorization +✅ All endpoints authorized | ❌ Missing: {list} + +--- +**Overall:** ✅ PASS | ❌ FAIL - Fix issues before commit +``` + +## Quick Commands + +```bash +# Full verification +dotnet build src/FSH.Starter.slnx && dotnet test src/FSH.Starter.slnx + +# Architecture tests only +dotnet test src/Tests/Architecture.Tests + +# Check for common issues +git diff --name-only | xargs grep -l "IRequest<\|MediatR" +``` diff --git a/.agents/workflows/code-reviewer.md b/.agents/workflows/code-reviewer.md new file mode 100644 index 0000000000..e0e3cd522c --- /dev/null +++ b/.agents/workflows/code-reviewer.md @@ -0,0 +1,93 @@ +--- +description: Review code changes against FSH patterns and conventions. Run after any code modifications to catch violations before commit. +--- + +You are a code reviewer for the FullStackHero .NET Starter Kit. Your job is to review code changes and ensure they follow FSH patterns, outputting a structured report. + +## Review Process + +1. Run `git diff` to see recent changes +2. Identify which files were modified +3. Check each change against the rules below +4. Report violations with specific file:line references + +## Critical Rules to Check + +### Architecture +- [ ] Features are in `Modules/{Module}/Features/v1/{Name}/` structure +- [ ] DTOs are in Contracts project, not internal +- [ ] No cross-module dependencies (modules only use Contracts) +- [ ] BuildingBlocks not modified without explicit approval + +### Mediator (NOT MediatR!) +- [ ] Commands use `ICommand` not `IRequest` +- [ ] Queries use `IQuery` not `IRequest` +- [ ] Handlers use `ICommandHandler` or `IQueryHandler` +- [ ] Handler methods return `ValueTask` not `Task` +- [ ] Using `Mediator` namespace, not `MediatR` + +### Validation +- [ ] Every command has a matching `AbstractValidator` +- [ ] Validators use FluentValidation rules + +### Endpoints +- [ ] Has `.RequirePermission()` or `.AllowAnonymous()` +- [ ] Has `.WithName()` matching the command/query name +- [ ] Has `.WithSummary()` with description +- [ ] Returns TypedResults, not raw objects + +### Entities +- [ ] Implements required interfaces (IHasTenant, IAuditableEntity, ISoftDeletable) +- [ ] Has private constructor for EF Core +- [ ] Uses factory method for creation +- [ ] Properties have `private set` +- [ ] Domain events raised for state changes + +### Naming +- [ ] Commands: `{Action}{Entity}Command` +- [ ] Queries: `Get{Entity}Query` or `Get{Entities}Query` +- [ ] Handlers: `{CommandOrQuery}Handler` +- [ ] Validators: `{Command}Validator` +- [ ] DTOs: `{Entity}Dto`, `{Entity}Response` + +## Commands to Run + +```bash +# Review staged/uncommitted changes +git diff HEAD + +# Check for MediatR usage (must be empty) +grep -r "MediatR\|IRequest<\|IRequestHandler<" src/Modules/ --include="*.cs" + +# Check build +dotnet build src/FSH.Starter.slnx 2>&1 | grep -E "warning|error" +``` + +## Output Format + +``` +## Code Review Summary + +### ✅ Passed +- [List what's correct] + +### ❌ Violations Found +1. **{Rule}** - {file}:{line} + - Issue: {description} + - Fix: {how to fix} + +### ⚠️ Warnings +- [Optional suggestions] + +### Build Verification +Run: `dotnet build src/FSH.Starter.slnx` +Expected: 0 warnings +``` + +## After Review + +Always suggest running: +```bash +dotnet build src/FSH.Starter.slnx # Verify 0 warnings +dotnet test src/FSH.Starter.slnx # Run tests +``` diff --git a/.agents/workflows/feature-scaffolder.md b/.agents/workflows/feature-scaffolder.md new file mode 100644 index 0000000000..dcc50a2908 --- /dev/null +++ b/.agents/workflows/feature-scaffolder.md @@ -0,0 +1,107 @@ +--- +description: Generate complete feature slices (Command/Handler/Validator/Endpoint) from requirements. Use when creating new API endpoints or features. +--- + +You are a feature scaffolder for FullStackHero .NET Starter Kit. Your job is to generate complete vertical slice features. + +## Required Information + +Before generating, confirm: +1. **Module name** - Which module? (e.g., Identity, Catalog) +2. **Feature name** - What action? (e.g., CreateProduct, GetUser) +3. **Entity name** - What entity? (e.g., Product, User) +4. **Operation type** - Command (state change) or Query (read)? +5. **Properties** - What fields does the command/query need? + +## Generation Process + +### Step 1: Create Feature Folder + +``` +src/Modules/{Module}/Features/v1/{FeatureName}/ +``` + +### Step 2: Generate Files + +For **Commands** (POST/PUT/DELETE), create 4 files: +1. `{Action}{Entity}Command.cs` +2. `{Action}{Entity}Handler.cs` +3. `{Action}{Entity}Validator.cs` +4. `{Action}{Entity}Endpoint.cs` + +For **Queries** (GET), create 3 files: +1. `Get{Entity}Query.cs` or `Get{Entities}Query.cs` +2. `Get{Entity}Handler.cs` +3. `Get{Entity}Endpoint.cs` + +### Step 3: Add DTOs to Contracts + +Create response/DTO types in: +``` +src/Modules/{Module}/Modules.{Module}.Contracts/ +``` + +### Step 4: Wire Endpoint + +Show where to add endpoint mapping in the module's `MapEndpoints` method. + +## Template: Command + +```csharp +// {Action}{Entity}Command.cs +public sealed record {Action}{Entity}Command( + {Properties}) : ICommand<{Action}{Entity}Response>; + +// {Action}{Entity}Handler.cs +public sealed class {Action}{Entity}Handler( + IRepository<{Entity}> repository, + ICurrentUser currentUser) : ICommandHandler<{Action}{Entity}Command, {Action}{Entity}Response> +{ + public async ValueTask<{Action}{Entity}Response> Handle( + {Action}{Entity}Command command, + CancellationToken ct) + { + // Implementation + } +} + +// {Action}{Entity}Validator.cs +public sealed class {Action}{Entity}Validator : AbstractValidator<{Action}{Entity}Command> +{ + public {Action}{Entity}Validator() + { + // Validation rules + } +} + +// {Action}{Entity}Endpoint.cs +public static class {Action}{Entity}Endpoint +{ + public static RouteHandlerBuilder Map(this IEndpointRouteBuilder endpoints) => + endpoints.Map{HttpMethod}("/", async ( + {Action}{Entity}Command command, + IMediator mediator, + CancellationToken ct) => TypedResults.{Result}(await mediator.Send(command, ct))) + .WithName(nameof({Action}{Entity}Command)) + .WithSummary("{Summary}") + .RequirePermission({Module}Permissions.{Entities}.{Action}); +} +``` + +## Checklist Before Completion + +- [ ] All files use `Mediator` interfaces (NOT MediatR) +- [ ] Handler returns `ValueTask` +- [ ] Validator exists for commands +- [ ] Endpoint has `.RequirePermission()` and `.WithName()` and `.WithSummary()` +- [ ] DTOs in Contracts project +- [ ] Shown where to wire endpoint in module + +## Verification + +After generation, run: +```bash +dotnet build src/FSH.Starter.slnx +``` + +Must show 0 warnings. diff --git a/.agents/workflows/migration-helper.md b/.agents/workflows/migration-helper.md new file mode 100644 index 0000000000..f291a3ef94 --- /dev/null +++ b/.agents/workflows/migration-helper.md @@ -0,0 +1,126 @@ +--- +description: Handle EF Core migrations safely. Create, apply, and manage database migrations for the FSH multi-tenant setup. Use when adding entities or changing database schema. +--- + +You are a migration helper for FullStackHero .NET Starter Kit. Your job is to safely manage EF Core migrations. + +## Project Paths + +- **Migrations project:** `src/Host/FSH.Starter.Migrations.PostgreSQL` +- **Startup project:** `src/Host/FSH.Starter.Api` +- **DbContexts:** Each module has its own DbContext + +## Common Operations + +### Add Migration + +```bash +dotnet ef migrations add {MigrationName} \ + --project src/Host/FSH.Starter.Migrations.PostgreSQL \ + --startup-project src/Host/FSH.Starter.Api \ + --context {DbContextName} +``` + +**Context names:** +- `IdentityDbContext` - Identity module +- `MultitenancyDbContext` - Multitenancy module +- `AuditingDbContext` - Auditing module +- `{Module}DbContext` - Custom modules + +### Apply Migrations + +```bash +dotnet ef database update \ + --project src/Host/FSH.Starter.Migrations.PostgreSQL \ + --startup-project src/Host/FSH.Starter.Api \ + --context {DbContextName} +``` + +### List Migrations + +```bash +dotnet ef migrations list \ + --project src/Host/FSH.Starter.Migrations.PostgreSQL \ + --startup-project src/Host/FSH.Starter.Api \ + --context {DbContextName} +``` + +### Remove Last Migration + +```bash +dotnet ef migrations remove \ + --project src/Host/FSH.Starter.Migrations.PostgreSQL \ + --startup-project src/Host/FSH.Starter.Api \ + --context {DbContextName} +``` + +### Generate SQL Script + +```bash +dotnet ef migrations script \ + --project src/Host/FSH.Starter.Migrations.PostgreSQL \ + --startup-project src/Host/FSH.Starter.Api \ + --context {DbContextName} \ + --output migrations.sql +``` + +## Multi-Tenant Considerations + +FSH uses per-tenant databases. Migrations apply to: +1. **Host database** - Tenant registry, shared data +2. **Tenant databases** - Tenant-specific data + +The framework handles tenant database migrations automatically on startup via `UseHeroMultiTenantDatabases()`. + +## Migration Naming Conventions + +Use descriptive names: +- `Add{Entity}` - Adding new entity +- `Add{Property}To{Entity}` - Adding column +- `Remove{Property}From{Entity}` - Removing column +- `Create{Index}Index` - Adding index +- `Rename{Old}To{New}` - Renaming + +## Pre-Migration Checklist + +- [ ] Entity configuration exists (`IEntityTypeConfiguration`) +- [ ] Entity added to DbContext (`DbSet`) +- [ ] Build succeeds with 0 warnings +- [ ] Backup database if production + +## Post-Migration Checklist + +- [ ] Review generated migration file +- [ ] Check Up() and Down() methods are correct +- [ ] Test migration on development database +- [ ] Verify rollback works (Down method) + +## Troubleshooting + +### "No DbContext was found" +Specify context explicitly with `--context {Name}DbContext` + +### "Build failed" +Run `dotnet build src/FSH.Starter.slnx` first + +### "Pending migrations" +Apply pending migrations or remove them if not needed + +### "Migration already applied" +Check `__EFMigrationsHistory` table in database + +## Example: Adding a New Entity + +1. Create entity in `Domain/` folder +2. Create configuration (`IEntityTypeConfiguration`) +3. Add `DbSet` to DbContext +4. Build: `dotnet build src/FSH.Starter.slnx` +5. Add migration: + ```bash + dotnet ef migrations add Add{Entity} \ + --project src/Host/FSH.Starter.Migrations.PostgreSQL \ + --startup-project src/Host/FSH.Starter.Api \ + --context {Module}DbContext + ``` +6. Review migration file +7. Apply: `dotnet ef database update ...` diff --git a/.agents/workflows/module-creator.md b/.agents/workflows/module-creator.md new file mode 100644 index 0000000000..d91734be89 --- /dev/null +++ b/.agents/workflows/module-creator.md @@ -0,0 +1,150 @@ +--- +description: Create new modules (bounded contexts) with complete project structure, DbContext, permissions, and registration. Use when adding a new business domain. +--- + +You are a module creator for FullStackHero .NET Starter Kit. Your job is to scaffold complete new modules. + +## When to Create a New Module + +Ask these questions: +- Does it have its own domain entities? → Yes = new module +- Could it be deployed independently? → Yes = new module +- Is it just a feature in an existing domain? → No = use existing module + +## Required Information + +Before generating, confirm: +1. **Module name** - PascalCase (e.g., Catalog, Inventory, Billing) +2. **Initial entities** - What domain entities? +3. **Permissions** - What operations need permissions? + +## Generation Process + +### Step 1: Create Project Structure + +``` +src/Modules/{Name}/ +├── Modules.{Name}/ +│ ├── Modules.{Name}.csproj +│ ├── {Name}Module.cs +│ ├── {Name}PermissionConstants.cs +│ ├── {Name}DbContext.cs +│ ├── Domain/ +│ └── Features/v1/ +└── Modules.{Name}.Contracts/ + ├── Modules.{Name}.Contracts.csproj + └── DTOs/ +``` + +### Step 2: Generate Core Files + +**Modules.{Name}.csproj:** +```xml + + + net10.0 + + + + + + + + +``` + +**{Name}Module.cs:** +```csharp +public sealed class {Name}Module : IModule +{ + public void ConfigureServices(IHostApplicationBuilder builder) + { + // DbContext, repositories, services + builder.Services.AddDbContext<{Name}DbContext>((sp, options) => + { + var dbOptions = sp.GetRequiredService>().Value; + options.UseNpgsql(dbOptions.ConnectionString); + }); + builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); + builder.Services.AddScoped(typeof(IReadRepository<>), typeof(Repository<>)); + } + + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/v1/{name}"); + // Map feature endpoints + } +} +``` + +**{Name}PermissionConstants.cs:** +```csharp +public static class {Name}PermissionConstants +{ + public static class {Entities} + { + public const string View = "{Entities}.View"; + public const string Create = "{Entities}.Create"; + public const string Update = "{Entities}.Update"; + public const string Delete = "{Entities}.Delete"; + } +} +``` + +**{Name}DbContext.cs:** +```csharp +public sealed class {Name}DbContext : DbContext +{ + public {Name}DbContext(DbContextOptions<{Name}DbContext> options) : base(options) { } + + public DbSet<{Entity}> {Entities} => Set<{Entity}>(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("{name}"); + modelBuilder.ApplyConfigurationsFromAssembly(typeof({Name}DbContext).Assembly); + } +} +``` + +### Step 3: Create Contracts Project + +**Modules.{Name}.Contracts.csproj:** +```xml + + + net10.0 + + +``` + +### Step 4: Register Module + +Show changes needed in: +1. `src/Host/FSH.Starter.Api/Program.cs` - Add to moduleAssemblies +2. `src/Host/FSH.Starter.Api/FSH.Starter.Api.csproj` - Add ProjectReference +3. Solution file - Add both projects + +### Step 5: Add to Solution + +```bash +dotnet sln src/FSH.Starter.slnx add src/Modules/{Name}/Modules.{Name}/Modules.{Name}.csproj +dotnet sln src/FSH.Starter.slnx add src/Modules/{Name}/Modules.{Name}.Contracts/Modules.{Name}.Contracts.csproj +``` + +## Checklist + +- [ ] Both projects created (main + contracts) +- [ ] IModule implemented +- [ ] Permission constants defined +- [ ] DbContext created with schema +- [ ] Registered in Program.cs +- [ ] Added to solution +- [ ] Referenced from FSH.Starter.Api +- [ ] Build passes with 0 warnings + +## Verification + +```bash +dotnet build src/FSH.Starter.slnx # Must be 0 warnings +``` diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000000..ba55c4179f --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "nswag.consolecore": { + "version": "14.6.3", + "commands": [ + "nswag" + ], + "rollForward": false + }, + "dotnet-ef": { + "version": "10.0.2", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + }, + "fullstackhero.cli": { + "version": "10.0.0-rc.1", + "commands": [ + "fsh" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..5e690f421b --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,17 @@ +{ + "name": "FullStackHero .NET Starter Kit", + "image": "mcr.microsoft.com/dotnet/sdk:10.0", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + "postCreateCommand": "dotnet workload install aspire && dotnet restore src/FSH.Starter.slnx", + "forwardPorts": [5030, 7030, 15888], + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csdevkit", + "ms-dotnettools.csharp" + ] + } + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..9df01736b5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +**/bin +**/obj +**/.vs +**/node_modules +.git +*.md +src/Tests/ diff --git a/.github/workflows/blazor.yml b/.github/workflows/blazor.yml deleted file mode 100644 index 1939d2b8db..0000000000 --- a/.github/workflows/blazor.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Build / Publish Blazor WebAssembly Project - -on: - workflow_dispatch: - - push: - branches: - - main - paths: - - "src/apps/blazor/**" - - "src/Directory.Packages.props" - - "src/Dockerfile.Blazor" - - pull_request: - branches: - - main - paths: - - "src/apps/blazor/**" - - "src/Directory.Packages.props" - - "src/Dockerfile.Blazor" - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: setup dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9.x - - name: restore dependencies - run: dotnet restore ./src/apps/blazor/client/Client.csproj - - name: build - run: dotnet build ./src/apps/blazor/client/Client.csproj --no-restore - - name: test - run: dotnet test ./src/apps/blazor/client/Client.csproj --no-build --verbosity normal - - publish: - needs: build - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v4 - - name: docker login - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: build and publish to github container registry - working-directory: ./src/ - run: | - docker build -t ghcr.io/${{ github.repository_owner }}/blazor:latest -f Dockerfile.Blazor . - docker push ghcr.io/${{ github.repository_owner }}/blazor:latest diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml deleted file mode 100644 index 7a88fcb9b4..0000000000 --- a/.github/workflows/changelog.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Release Drafter - -on: - workflow_dispatch: - push: - branches: - - main - -permissions: - contents: read - -jobs: - update_release_draft: - permissions: - # write permission is required to create a github release - contents: write - # write permission is required for autolabeler - # otherwise, read permission is required at least - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: release-drafter/release-drafter@v6 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..765bf0e9e6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,304 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - main + - develop + tags: + - 'v*' + paths: + - 'src/**' + pull_request: + branches: + - main + - develop + paths: + - 'src/**' + workflow_dispatch: + inputs: + version: + description: 'Package version (e.g., 10.0.0-rc.1)' + required: false + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + packages: write + +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_NOLOGO: true + + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + dotnet-quality: 'preview' + + - name: Cache NuGet packages + uses: actions/cache@v5 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore dependencies + run: dotnet restore src/FSH.Starter.slnx + + - name: Build + run: dotnet build src/FSH.Starter.slnx -c Release --no-restore -warnaserror + + - name: Check for vulnerable packages + run: dotnet list src/FSH.Starter.slnx package --vulnerable --include-transitive 2>&1 | tee vulnerability-report.txt + continue-on-error: true + + - name: Upload build artifacts + uses: actions/upload-artifact@v7 + with: + name: build-output + path: | + src/**/bin/Release + src/**/obj/Release + retention-days: 1 + + test: + name: Test - ${{ matrix.test-project.name }} + runs-on: ubuntu-latest + needs: build + + strategy: + fail-fast: false + matrix: + test-project: + - name: Architecture.Tests + path: src/Tests/Architecture.Tests + - name: Auditing.Tests + path: src/Tests/Auditing.Tests + - name: Caching.Tests + path: src/Tests/Caching.Tests + - name: Generic.Tests + path: src/Tests/Generic.Tests + - name: Identity.Tests + path: src/Tests/Identity.Tests + - name: Multitenancy.Tests + path: src/Tests/Multitenancy.Tests + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + dotnet-quality: 'preview' + + - name: Cache NuGet packages + uses: actions/cache@v5 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Download build artifacts + uses: actions/download-artifact@v8 + with: + name: build-output + path: src + + - name: Run ${{ matrix.test-project.name }} + run: dotnet test ${{ matrix.test-project.path }} -c Release --no-build --verbosity normal --logger "trx;LogFileName=${{ matrix.test-project.name }}.trx" + + - name: Upload test results + uses: actions/upload-artifact@v7 + if: always() + with: + name: test-results-${{ matrix.test-project.name }} + path: '**/*.trx' + retention-days: 7 + + integration-test: + name: Integration Tests + runs-on: ubuntu-latest + needs: build + # Testcontainers requires Docker — ubuntu-latest has it pre-installed. + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + dotnet-quality: 'preview' + + - name: Cache NuGet packages + uses: actions/cache@v5 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props') }} + restore-keys: | + ${{ runner.os }}-nuget- + + # Integration tests use WebApplicationFactory + Testcontainers. + # They must build from source (can't use pre-built artifacts). + - name: Run Integration Tests + run: dotnet test src/Tests/Integration.Tests -c Release --verbosity normal --logger "trx;LogFileName=Integration.Tests.trx" + + - name: Upload test results + uses: actions/upload-artifact@v7 + if: always() + with: + name: test-results-Integration.Tests + path: '**/*.trx' + retention-days: 7 + + publish-dev-containers: + name: Publish Dev Containers + runs-on: ubuntu-latest + needs: [test, integration-test] + if: github.ref == 'refs/heads/develop' && github.event_name == 'push' + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + dotnet-quality: 'preview' + + - name: Cache NuGet packages + uses: actions/cache@v5 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Login to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish API container image + run: | + dotnet publish src/Host/FSH.Starter.Api/FSH.Starter.Api.csproj \ + -c Release -r linux-x64 \ + -p:PublishProfile=DefaultContainer \ + -p:ContainerRepository=ghcr.io/${{ github.repository_owner }}/fsh-api \ + -p:ContainerImageTags='"dev-${{ github.sha }};dev-latest"' + + + publish-release: + name: Publish Release (NuGet + Containers) + runs-on: ubuntu-latest + needs: [test, integration-test] + if: | + (github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch') || + startsWith(github.ref, 'refs/tags/v') + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + dotnet-quality: 'preview' + + - name: Cache NuGet packages + uses: actions/cache@v5 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Determine version + id: version + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ github.event.inputs.version }}" ]; then + VERSION="${{ github.event.inputs.version }}" + elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then + VERSION="${GITHUB_REF#refs/tags/v}" + else + echo "No version specified and not a tag push" + exit 1 + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Publishing version: $VERSION" + + - name: Restore and Build with version + run: | + dotnet restore src/FSH.Starter.slnx + dotnet build src/FSH.Starter.slnx -c Release --no-restore -p:Version=${{ steps.version.outputs.version }} + + - name: Pack BuildingBlocks + run: | + dotnet pack src/BuildingBlocks/Core/Core.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/BuildingBlocks/Shared/Shared.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/BuildingBlocks/Persistence/Persistence.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/BuildingBlocks/Caching/Caching.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/BuildingBlocks/Mailing/Mailing.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/BuildingBlocks/Jobs/Jobs.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/BuildingBlocks/Storage/Storage.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/BuildingBlocks/Eventing/Eventing.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/BuildingBlocks/Eventing.Abstractions/Eventing.Abstractions.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/BuildingBlocks/Web/Web.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + + - name: Pack Modules + run: | + dotnet pack src/Modules/Auditing/Modules.Auditing.Contracts/Modules.Auditing.Contracts.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/Modules/Auditing/Modules.Auditing/Modules.Auditing.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/Modules/Identity/Modules.Identity.Contracts/Modules.Identity.Contracts.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/Modules/Identity/Modules.Identity/Modules.Identity.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/Modules/Multitenancy/Modules.Multitenancy.Contracts/Modules.Multitenancy.Contracts.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/Modules/Multitenancy/Modules.Multitenancy/Modules.Multitenancy.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/Modules/Webhooks/Modules.Webhooks.Contracts/Modules.Webhooks.Contracts.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + dotnet pack src/Modules/Webhooks/Modules.Webhooks/Modules.Webhooks.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + + - name: Pack CLI Tool + run: dotnet pack src/Tools/CLI/FSH.CLI.csproj -c Release --no-build -o ./nupkgs -p:PackageVersion=${{ steps.version.outputs.version }} + + - name: Push to NuGet.org + run: dotnet nuget push "./nupkgs/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + + - name: Login to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push API container + run: | + dotnet publish src/Host/FSH.Starter.Api/FSH.Starter.Api.csproj \ + -c Release -r linux-x64 \ + -p:PublishProfile=DefaultContainer \ + -p:ContainerRepository=ghcr.io/${{ github.repository_owner }}/fsh-api \ + -p:ContainerImageTags='"${{ steps.version.outputs.version }};latest"' + diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..be7949966c --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,27 @@ +name: CodeQL Analysis +on: + pull_request: + branches: [main, develop] + paths: + - 'src/**' + schedule: + - cron: '0 6 * * 1' + +permissions: + security-events: write + contents: read + +jobs: + analyze: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: github/codeql-action/init@v3 + with: + languages: csharp + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + dotnet-quality: 'preview' + - run: dotnet build src/FSH.Starter.slnx -c Release + - uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml deleted file mode 100644 index 4ee62a6ff5..0000000000 --- a/.github/workflows/nuget.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Publish Package to NuGet.org -on: - push: - branches: - - main - paths: - - "FSH.StarterKit.nuspec" -jobs: - publish: - name: publish nuget - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - name: checkout code - - uses: nuget/setup-nuget@v2 - name: setup nuget - with: - nuget-version: "latest" - nuget-api-key: ${{ secrets.NUGET_API_KEY }} - - name: generate package - run: nuget pack FSH.StarterKit.nuspec -NoDefaultExcludes - - name: publish package - run: nuget push *.nupkg -Source 'https://api.nuget.org/v3/index.json' -SkipDuplicate diff --git a/.github/workflows/webapi.yml b/.github/workflows/webapi.yml deleted file mode 100644 index a84e28f03a..0000000000 --- a/.github/workflows/webapi.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Build / Publish .NET WebAPI Project - -on: - workflow_dispatch: - - push: - branches: - - main - paths: - - "src/api/**" - - "src/Directory.Packages.props" - - pull_request: - branches: - - main - paths: - - "src/api/**" - - "src/Directory.Packages.props" - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: setup dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9.x - - name: restore dependencies - run: dotnet restore ./src/api/server/Server.csproj - - name: build - run: dotnet build ./src/api/server/Server.csproj --no-restore - - name: test - run: dotnet test ./src/api/server/Server.csproj --no-build --verbosity normal - - publish: - needs: build - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v4 - - name: setup dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9.x - - name: docker login - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: publish to github container registry - working-directory: ./src/api/server/ - run: | - dotnet publish -c Release -p:ContainerRepository=ghcr.io/${{ github.repository_owner}}/webapi -p:RuntimeIdentifier=linux-x64 - docker push ghcr.io/${{ github.repository_owner}}/webapi --all-tags diff --git a/.gitignore b/.gitignore index 9995d856ac..f819c3f1a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,12 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +## Get latest from `dotnet new gitignore` + +*.terraform +terraform.tfstate +# dotenv files +.env # User-specific files *.rsuser @@ -31,16 +36,12 @@ bld/ [Oo]bj/ [Ll]og/ [Ll]ogs/ -[Ii]mages/ -[Dd]atabases/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ -.vscode/ - # Visual Studio 2017 auto generated files Generated\ Files/ @@ -61,7 +62,7 @@ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ -# .NET Core +# .NET project.lock.json project.fragment.lock.json artifacts/ @@ -97,6 +98,7 @@ StyleCopReport.xml *.tmp_proj *_wpftmp.csproj *.log +*.tlog *.vspscc *.vssscc .builds @@ -206,6 +208,9 @@ PublishScripts/ **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ +# except the Next.js workspace packages (monorepo, not NuGet restore artifacts). +!clients/**/packages/ +!clients/**/packages/** # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files @@ -300,6 +305,17 @@ node_modules/ # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -356,6 +372,9 @@ ASALocalRun/ # Local History for Visual Studio .localhistory/ +# Visual Studio History (VSHistory) files +.vshistory/ + # BeatPulse healthcheck temp database healthchecksdb @@ -368,6 +387,28 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea/ + ## ## Visual studio for Mac ## @@ -390,7 +431,7 @@ test-results/ *.dmg *.app -# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore # General .DS_Store .AppleDouble @@ -419,7 +460,7 @@ Network Trash Folder Temporary Items .apdisk -# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore # Windows thumbnail cache files Thumbs.db ehthumbs.db @@ -444,17 +485,32 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk -# JetBrains Rider -.idea/ -*.sln.iml - -## -## Visual Studio Code -## -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json - -**/Internal/Generated +# Vim temporary swap files +*.swp +/.bmad + +team/ +docs/node_modules/ +docs/dist/ +docs/.astro/ +spec-os/ +/PLAN.md +**/nul +**/wwwroot/uploads/* + +/.claude/settings.local.json +tmpclaude** + +# Auto Claude data directory +.auto-claude/ + +# Clients (Next.js / pnpm workspace) +clients/**/node_modules/ +clients/**/.next/ +clients/**/.turbo/ +clients/**/dist/ +clients/**/.env.local + +# Audit dead-letter queue — runtime fallback when the audit pipeline cannot +# reach its persistent sink. Local-only by design; never commit. +**/audit-dlq/ diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000000..a0868ce17d --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "shadcn": { + "type": "http", + "url": "https://mcp.shadcn.com" + } + } +} \ No newline at end of file diff --git a/.template.config/template.json b/.template.config/template.json index a5415e836e..6a1ab57f3e 100644 --- a/.template.config/template.json +++ b/.template.config/template.json @@ -3,7 +3,8 @@ "author": "Mukesh Murugan", "classifications": [ "WebAPI", - "Clean Architecture", + "Modular Monolith", + "Vertical Slice", "Boilerplate", "ASP.NET Core", "Starter Kit", @@ -12,14 +13,44 @@ ], "tags": { "language": "C#", - "type": "project" + "type": "solution" }, "identity": "FullStackHero.NET.StarterKit", "name": "FullStackHero .NET Starter Kit", - "description": "The best way to start a full-stack .NET 9 Web App.", + "description": "A production-ready modular .NET 10 framework — Vertical Slice Architecture, CQRS, Multitenancy, Identity, Aspire.", "shortName": "fsh", "sourceName": "FSH.Starter", "preferNameDirectory": true, + "symbols": { + "db": { + "type": "parameter", + "datatype": "choice", + "description": "Database provider for migrations and persistence.", + "defaultValue": "postgresql", + "choices": [ + { + "choice": "postgresql", + "description": "PostgreSQL (default)" + }, + { + "choice": "sqlserver", + "description": "SQL Server" + } + ] + }, + "aspire": { + "type": "parameter", + "datatype": "bool", + "description": "Include the .NET Aspire AppHost project for orchestration.", + "defaultValue": "true" + }, + "skipRestore": { + "type": "parameter", + "datatype": "bool", + "description": "Skip dotnet restore after project creation.", + "defaultValue": "false" + } + }, "sources": [ { "source": "./", @@ -30,16 +61,42 @@ ".vscode/**", ".vs/**", ".github/**", + ".agents/**", + ".claude/**", + ".devcontainer/**", + ".git/**", "templates/**/*", + "clients/**", + "demo/**", + "docs/**", + "nupkgs/**", + "scripts/**", + "src/Tools/**", "**/*.filelist", "**/*.user", "**/images", "**/*.lock.json", - "*.nuspec" + "*.nuspec", + "**/bin/**", + "**/obj/**", + ".mcp.json", + ".gitignore", + "CLAUDE.md", + "GEMINI.md", + "README.md", + "LICENSE" ], "rename": { "README-template.md": "README.md" - } + }, + "modifiers": [ + { + "condition": "(!aspire)", + "exclude": [ + "src/Host/FSH.Starter.AppHost/**" + ] + } + ] } ], "primaryOutputs": [ @@ -49,7 +106,8 @@ ], "postActions": [ { - "description": "restore webapi project dependencies", + "condition": "(!skipRestore)", + "description": "Restore NuGet packages", "manualInstructions": [ { "text": "Run 'dotnet restore'" @@ -59,4 +117,4 @@ "continueOnError": false } ] -} \ No newline at end of file +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..5d0cb11799 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch API", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/Host/FSH.Starter.Api/bin/Debug/net10.0/FSH.Starter.Api.dll", + "args": [], + "cwd": "${workspaceFolder}/src/Host/FSH.Starter.Api", + "stopAtEntry": false, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..dfdf130c95 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "omnisharp.dotnetPath": "/home/jarvis/.dotnet", + "omnisharp.sdkPath": "/home/jarvis/.dotnet/sdk/10.0.102", + "omnisharp.useModernNet": true, + "editor.formatOnSave": true, + "files.exclude": { + "**/bin": true, + "**/obj": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..949717bef7 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,76 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/FSH.Starter.slnx", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "test", + "command": "dotnet", + "type": "process", + "args": [ + "test", + "${workspaceFolder}/src/FSH.Starter.slnx" + ], + "problemMatcher": "$msCompile", + "group": "test" + }, + { + "label": "run (Aspire)", + "command": "dotnet", + "type": "process", + "args": [ + "run", + "--project", + "${workspaceFolder}/src/Host/FSH.Starter.AppHost" + ], + "problemMatcher": "$msCompile", + "group": "none" + }, + { + "label": "run (API only)", + "command": "dotnet", + "type": "process", + "args": [ + "run", + "--project", + "${workspaceFolder}/src/Host/FSH.Starter.Api" + ], + "problemMatcher": "$msCompile", + "group": "none" + }, + { + "label": "clean", + "command": "dotnet", + "type": "process", + "args": [ + "clean", + "${workspaceFolder}/src/FSH.Starter.slnx" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "restore", + "command": "dotnet", + "type": "process", + "args": [ + "restore", + "${workspaceFolder}/src/FSH.Starter.slnx" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..f8b5c9a0cb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,191 @@ +# FullStackHero .NET Starter Kit + +> A production-ready modular .NET framework for building enterprise applications. + +## Architecture + +**Modular Monolith + Vertical Slice Architecture (VSA)** + +- **BuildingBlocks** (`src/BuildingBlocks/`) — shared framework libraries (Core, Persistence, Web, Caching, Eventing, etc.) +- **Modules** (`src/Modules/`) — bounded contexts (Identity, Multitenancy, Auditing) +- **Host** (`src/Host/`) — composition-root host applications (API, AppHost) +- **Tests** (`src/Tests/`) — per-module test projects + architecture tests + +### Module Boundaries + +Modules communicate through **Contracts** projects only. A module MUST NOT reference another module's runtime project. + +``` +Modules.Identity/ ← runtime (internal) +Modules.Identity.Contracts/ ← public API (commands, queries, events, DTOs, service interfaces) +``` + +### Feature Folder Layout + +Each feature is a vertical slice inside `Features/v{version}/{Area}/{FeatureName}/`: + +``` +Features/v1/Users/RegisterUser/ +├── RegisterUserEndpoint.cs # Minimal API endpoint +├── RegisterUserCommandHandler.cs # CQRS handler +└── RegisterUserCommandValidator.cs # FluentValidation +``` + +Additional module folders: `Domain/`, `Data/`, `Services/`, `Events/`, `Authorization/`. + +## Tech Stack + +| Concern | Technology | +|---------|-----------| +| Framework | .NET 10 / C# latest | +| Solution format | `.slnx` (XML-based) | +| Package management | Central (`Directory.Packages.props`) | +| CQRS / Mediator | Mediator 3.0.1 (source generator) | +| Validation | FluentValidation 12.x | +| ORM | Entity Framework Core 10.x | +| Database | PostgreSQL (Npgsql) | +| Auth | JWT Bearer + ASP.NET Identity | +| Multitenancy | Finbuckle.MultiTenant 10.x (claim/header/query strategies) | +| Caching | Redis (StackExchange) | +| Jobs | Hangfire | +| Resilience | Microsoft.Extensions.Http.Resilience (Polly v8) | +| Feature Flags | Microsoft.FeatureManagement with tenant overrides | +| Idempotency | Idempotency-Key header with cache-based replay | +| Webhooks | Tenant-scoped subscriptions with HMAC signing | +| Real-time | Server-Sent Events (SSE) | +| Logging | Serilog + OpenTelemetry (OTLP) | +| API docs | OpenAPI + Scalar | +| API versioning | Asp.Versioning | +| Hosting | .NET Aspire (AppHost) | +| Testing | xUnit, Shouldly, NSubstitute, AutoFixture, NetArchTest | + +## Build & Run + +```bash +# Build +dotnet build src/FSH.Starter.slnx + +# Run API (from repo root) +dotnet run --project src/Host/FSH.Starter.Api + +# Run with Aspire +dotnet run --project src/Host/FSH.Starter.AppHost + +# Run tests +dotnet test src/FSH.Starter.slnx +``` + +## Key Conventions + +### Endpoints + +Static extension methods on `IEndpointRouteBuilder`. Return `RouteHandlerBuilder`. + +```csharp +public static class RegisterUserEndpoint +{ + internal static RouteHandlerBuilder MapRegisterUserEndpoint(this IEndpointRouteBuilder endpoints) + { + return endpoints.MapPost("/register", (RegisterUserCommand command, + IMediator mediator, CancellationToken cancellationToken) => + mediator.Send(command, cancellationToken)) + .WithName("RegisterUser") + .WithSummary("Register user") + .RequirePermission(IdentityPermissionConstants.Users.Create); + } +} +``` + +### CQRS + +- **Commands/Queries** → defined in `Modules.{Name}.Contracts` (implement `ICommand` / `IQuery`) +- **Handlers** → defined in `Modules.{Name}/Features/` (implement `ICommandHandler` / `IQueryHandler`) +- Handlers return `ValueTask` and use `.ConfigureAwait(false)` + +### Validation + +FluentValidation validators are auto-registered by `ModuleLoader`. Name them `{Command}Validator`. + +### Domain Events + +- Inherit from `DomainEvent` (abstract record with `EventId`, `OccurredOnUtc`, `CorrelationId`, `TenantId`) +- Entities implement `IHasDomainEvents` with `_domainEvents` list +- Integration events implement `IIntegrationEvent`, handlers implement `IIntegrationEventHandler` + +### Domain Entities + +- `BaseEntity` — `Id`, `CreatedAt`, `UpdatedAt`, `TenantId` +- `AggregateRoot` — extends `BaseEntity` with domain events +- `IHasTenant`, `IAuditableEntity`, `ISoftDeletable` — marker interfaces + +### Module Registration + +Each module implements `IModule` with `[FshModule(Order = n)]` attribute: + +```csharp +[FshModule(Order = 1)] +public class IdentityModule : IModule +{ + public void ConfigureServices(IHostApplicationBuilder builder) { ... } + public void MapEndpoints(IEndpointRouteBuilder endpoints) { ... } +} +``` + +Endpoints are grouped under versioned API paths: `api/v{version:apiVersion}/{module}`. + +### Exceptions + +Use framework exception types: `CustomException` (with `HttpStatusCode`), `NotFoundException`, `ForbiddenException`, `UnauthorizedException`. Global handler converts to `ProblemDetails` (RFC 9457). + +### Permissions + +Constants in `Shared/Identity/IdentityPermissionConstants.cs`. Applied via `.RequirePermission()` on endpoints. + +### Specifications + +Use `Specification` base class from `Persistence/Specifications/` for query composition. Default `AsNoTracking = true`. + +## Coding Style + +- **Namespace style**: File-scoped (`namespace X;`) +- **Indentation**: 4 spaces +- **Var usage**: Prefer explicit types; `var` only when type is apparent from RHS +- **Null checks**: `is null` / `is not null` (not `== null`) +- **Pattern matching**: Preferred over `is`/`as` casts +- **Switch expressions**: Preferred +- **Async**: `ValueTask` for handlers, `.ConfigureAwait(false)` on all awaits +- **Guard clauses**: `ArgumentNullException.ThrowIfNull(param)` at method entry +- **Properties**: Prefer auto-properties, `default!` for required non-nullable strings +- **Records**: Use for DTOs, events, and value objects + +## Testing Conventions + +- **Naming**: `MethodName_Should_ExpectedBehavior_When_Condition` +- **Pattern**: Arrange-Act-Assert with `#region` grouping (Happy Path, Exception, Edge Cases) +- **Assertions**: Shouldly (`result.ShouldBe(...)`, `result.ShouldNotBeNull()`) +- **Mocking**: NSubstitute (`Substitute.For()`) +- **Test data**: AutoFixture (`_fixture.Create()`) +- **Architecture tests**: NetArchTest enforces module boundary rules + +## Protected Directories + +**DO NOT modify BuildingBlocks** without explicit approval. These are shared framework libraries consumed by all modules. Changes here have wide blast radius. + +## Adding a New Feature + +1. Add command/query + response in `Modules.{Name}.Contracts/v1/{Area}/{Feature}/` +2. Add handler in `Modules.{Name}/Features/v1/{Area}/{Feature}/` +3. Add validator in the same feature folder +4. Add endpoint in the same feature folder +5. Wire endpoint in the module's `MapEndpoints()` method +6. Add tests in `Tests/{Name}.Tests/` + +## Adding a New Module + +1. Create `Modules.{Name}/` and `Modules.{Name}.Contracts/` projects under `src/Modules/{Name}/` +2. Implement `IModule` with `[FshModule(Order = n)]` +3. Add DbContext extending from framework base +4. Register in `Program.cs` module assemblies array +5. Add migration project if needed +6. Add test project in `src/Tests/` +7. Add architecture test rules diff --git a/FSH.StarterKit.nuspec b/FSH.StarterKit.nuspec deleted file mode 100644 index a96e4b69f1..0000000000 --- a/FSH.StarterKit.nuspec +++ /dev/null @@ -1,24 +0,0 @@ - - - - FullStackHero.NET.StarterKit - FullStackHero .NET Starter Kit - 2.0.4-rc - Mukesh Murugan - The best way to start a full-stack Multi-tenant .NET 9 Web App. - en-US - ./content/LICENSE - 2024 - ./content/README.md - https://fullstackhero.net/dotnet-starter-kit/general/getting-started/ - - - - - cleanarchitecture clean architecture WebAPI mukesh codewithmukesh fullstackhero solution csharp - ./content/icon.png - - - - - \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000000..6c09e02a0e --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,139 @@ +# FSH .NET Starter Kit — Gemini AI Assistant Guide + +> Modular Monolith · CQRS · DDD · Multi-Tenant · .NET 10 + +## Quick Start + +```bash +dotnet build src/FSH.Starter.slnx # Build (0 warnings required) +dotnet test src/FSH.Starter.slnx # Run tests +dotnet run --project src/Host/FSH.Starter.AppHost # Run with Aspire +``` + +## Project Layout + +``` +src/ +├── BuildingBlocks/ # Framework (11 packages) — ⚠️ Protected +├── Modules/ # Business features — Add code here +│ ├── Identity/ # Auth, users, roles, permissions +│ ├── Multitenancy/ # Tenant management (Finbuckle) +│ └── Auditing/ # Audit logging +├── Host/ # Composition-root host applications +└── Tests/ # Architecture + unit tests +``` + +## The Pattern + +Every feature = vertical slice: + +``` +Modules/{Module}/Features/v1/{Feature}/ +├── {Action}{Entity}Command.cs # ICommand +├── {Action}{Entity}Handler.cs # ICommandHandler +├── {Action}{Entity}Validator.cs # AbstractValidator +└── {Action}{Entity}Endpoint.cs # MapPost/Get/Put/Delete +``` + +## Critical Rules + +| ⚠️ Rule | Why | +|---------|-----| +| Use **Mediator** not MediatR | Different library, different interfaces | +| `ICommand` / `IQuery` | NOT `IRequest` | +| `ValueTask` return type | NOT `Task` | +| Every command needs validator | FluentValidation, no exceptions | +| `.RequirePermission()` on endpoints | Explicit authorization | +| Zero build warnings | CI blocks merges | + +## Available Skills + +Invoke with `/skill-name` in your prompt. + +| Skill | Purpose | +|-------|---------| +| `/add-feature` | Create complete CQRS feature (command/handler/validator/endpoint) | +| `/add-entity` | Add domain entity with base class inheritance | +| `/add-module` | Scaffold new bounded context module | +| `/query-patterns` | Implement paginated/filtered queries | +| `/testing-guide` | Write architecture + unit tests | +| `/mediator-reference` | Mediator vs MediatR interface reference | + +## Available Workflows + +Delegate complex tasks to specialized workflows. + +| Workflow | Expertise | +|----------|-----------| +| `/code-reviewer` | Review changes against FSH patterns + architecture rules | +| `/feature-scaffolder` | Generate complete feature slices from requirements | +| `/module-creator` | Create new modules with contracts, persistence, DI setup | +| `/architecture-guard` | Verify layering, dependencies, module boundaries | +| `/migration-helper` | Generate and apply EF Core migrations | + +## Example: Create Feature + +```csharp +// Command +public sealed record CreateProductCommand(string Name, decimal Price) + : ICommand; + +// Handler +public sealed class CreateProductHandler(IRepository repo) + : ICommandHandler +{ + public async ValueTask Handle(CreateProductCommand cmd, CancellationToken ct) + { + var product = Product.Create(cmd.Name, cmd.Price); + await repo.AddAsync(product, ct); + return product.Id; + } +} + +// Validator +public sealed class CreateProductValidator : AbstractValidator +{ + public CreateProductValidator() + { + RuleFor(x => x.Name).NotEmpty().MaximumLength(200); + RuleFor(x => x.Price).GreaterThan(0); + } +} + +// Endpoint +public static RouteHandlerBuilder Map(this IEndpointRouteBuilder endpoints) => + endpoints.MapPost("/", async (CreateProductCommand cmd, IMediator mediator, CancellationToken ct) => + TypedResults.Created($"/api/v1/products/{await mediator.Send(cmd, ct)}")) + .WithName(nameof(CreateProductCommand)) + .WithSummary("Create a new product") + .RequirePermission(CatalogPermissions.Products.Create); +``` + +## Architecture + +- **Pattern:** Modular Monolith (not microservices) +- **CQRS:** Mediator library (commands/queries) +- **DDD:** Rich domain models, aggregates, value objects +- **Multi-Tenancy:** Finbuckle.MultiTenant (shared DB, tenant isolation) +- **Modules:** 3 core (Identity, Multitenancy, Auditing) + your features +- **BuildingBlocks:** 11 packages (Core, Persistence, Caching, Jobs, Web, etc.) + +Details: See `.agents/rules/architecture.md` + +## Before Committing + +```bash +dotnet build src/FSH.Starter.slnx # Must pass with 0 warnings +dotnet test src/FSH.Starter.slnx # All tests must pass +``` + +## Documentation + +- **Architecture:** See `ARCHITECTURE_ANALYSIS.md` (19KB deep-dive) +- **Rules:** See `.agents/rules/*.md` (API conventions, testing, modules) +- **Skills:** See `.agents/skills/*/SKILL.md` (step-by-step guides) +- **Workflows:** See `.agents/workflows/*.md` (specialized assistants) + +--- + +**Philosophy:** This is a production-ready starter kit. Every pattern is battle-tested. Follow the conventions, and you'll ship faster. diff --git a/LICENSE b/LICENSE index fc25cd4f55..7e5256d336 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 fullstackhero +Copyright (c) 2021-2026 fullstackhero Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README-template.md b/README-template.md index e69de29bb2..223064904e 100644 --- a/README-template.md +++ b/README-template.md @@ -0,0 +1,63 @@ +# FSH.Starter + +Built with [FullStackHero .NET Starter Kit](https://github.com/fullstackhero/dotnet-starter-kit) — a production-ready modular .NET 10 framework. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- [Docker](https://www.docker.com/) (for PostgreSQL, Redis, and Aspire) +- .NET Aspire workload: `dotnet workload install aspire` + +## Quick Start + +```bash +# Start everything with Aspire (recommended) +dotnet run --project src/Host/FSH.Starter.AppHost + +# Or run the API standalone (requires external Postgres + Redis) +dotnet run --project src/Host/FSH.Starter.Api +``` + +The Aspire dashboard opens at `https://localhost:15888`. The API serves at `https://localhost:7030` with Scalar docs at `/scalar`. + +## Project Structure + +``` +src/ + BuildingBlocks/ # Shared framework libraries (do not modify unless necessary) + Modules/ # Bounded contexts (Identity, Multitenancy, Auditing, Webhooks) + Host/ + FSH.Starter.Api/ # API host + FSH.Starter.AppHost/ # .NET Aspire orchestrator +FSH.Starter.Migrations.PostgreSQL/ # EF Core migrations + Tests/ # Unit, integration, and architecture tests +``` + +## Adding Your First Feature + +1. Define command/query in `src/Modules/{Module}.Contracts/v1/{Area}/{Feature}/` +2. Add handler in `src/Modules/{Module}/Features/v1/{Area}/{Feature}/` +3. Add FluentValidation validator in the same folder +4. Add endpoint in the same folder +5. Wire the endpoint in the module's `MapEndpoints()` method + +## Removing Unwanted Modules + +To remove a module (e.g., Webhooks): + +1. Delete `src/Modules/Webhooks/` folders +2. Remove its references from `src/Host/FSH.Starter.Api/FSH.Starter.Api.csproj` +3. Remove its assembly from `Program.cs` (both `AddMediator` and `moduleAssemblies`) +4. Remove its migration folder from `src/Host/FSH.Starter.Migrations.PostgreSQL/` +5. Remove from `src/FSH.Starter.slnx` + +## Running Tests + +```bash +dotnet test src/FSH.Starter.slnx +``` + +## Learn More + +- [FullStackHero Documentation](https://fullstackhero.net) +- [GitHub Repository](https://github.com/fullstackhero/dotnet-starter-kit) diff --git a/README.md b/README.md index 7682ba1331..11529ac97c 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,119 @@ -# FullStackHero .NET 9 Starter Kit 🚀 +# FullStackHero .NET 10 Starter Kit -> With ASP.NET Core Web API & Blazor Client +[![NuGet](https://img.shields.io/nuget/v/FullStackHero.CLI?label=fsh%20cli)](https://www.nuget.org/packages/FullStackHero.CLI) +[![NuGet](https://img.shields.io/nuget/v/FullStackHero.Framework.Web?label=framework)](https://www.nuget.org/packages/FullStackHero.Framework.Web) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -FullStackHero .NET Starter Kit is a starting point for your next `.NET 9 Clean Architecture` Solution that incorporates the most essential packages and features your projects will ever need including out-of-the-box Multi-Tenancy support. This project can save well over 200+ hours of development time for your team. +An opinionated, production-first starter for building multi-tenant SaaS and enterprise APIs on .NET 10. You get ready-to-ship Identity, Multitenancy, Auditing, Webhooks, caching, mailing, jobs, storage, health, OpenAPI, and OpenTelemetry — wired through Minimal APIs, Mediator, and EF Core. -![FullStackHero .NET Starter Kit](./assets/fullstackhero-dotnet-starter-kit.png) +## Quick Start -# Important +You get the complete source code — BuildingBlocks, Modules, and Host — with full project references. No black-box NuGet packages; you own and can modify everything. -This project is currently work in progress. The NuGet package is not yet available for v2. For now, you can fork this repository to try it out. [Follow @iammukeshm on X](https://x.com/iammukeshm) for project related updates. +### Option 1: FSH CLI (recommended) -# Quick Start Guide - -As the project is still in beta, the NuGet packages are not yet available. You can try out the project by pulling the code directly from this repository. - -Prerequisites: - -- .NET 9 SDK installed. -- Visual Studio IDE. -- Docker Desktop. -- PostgreSQL instance running on your machine or docker container. - -Please follow the below instructions. - -1. Fork this repository to your local. -2. Open up the `./src/FSH.Starter.sln`. -3. This would up the FSH Starter solution which has 3 main components. - 1. Aspire Dashboard (set as the default project) - 2. Web API - 3. Blazor -4. Now we will have to set the connection string for the API. Navigate to `./src/api/server/appsettings.Development.json` and change the `ConnectionString` under `DatabaseOptions`. Save it. -5. Once that is done, run the application via Visual Studio, with Aspire as the default project. This will open up Aspire Dashboard at `https://localhost:7200/`. -6. API will be running at `https://localhost:7000/swagger/index.html`. -7. Blazor will be running at `https://localhost:7100/`. - -# 🔎 The Project - -# ✨ Technologies - -- .NET 9 -- Entity Framework Core 9 -- Blazor -- MediatR -- PostgreSQL -- Redis -- FluentValidation - -# 👨‍🚀 Architecture - -# 📬 Service Endpoints - -| Endpoint | Method | Description | -| -------- | ------ | ---------------- | -| `/token` | POST | Generates Token. | +```bash +dotnet tool install -g FullStackHero.CLI +fsh new MyApp +cd MyApp +dotnet run --project src/Host/MyApp.AppHost +``` -# 🧪 Running Locally +The interactive wizard lets you pick your database provider and whether to include Aspire. Run `fsh doctor` to verify your environment first. -# 🐳 Docker Support +### Option 2: dotnet new template -# ☁️ Deploying to AWS +```bash +dotnet new install FullStackHero.NET.StarterKit +dotnet new fsh -n MyApp +cd MyApp +dotnet run --project src/Host/MyApp.AppHost +``` -# 🤝 Contributing +### Option 3: Clone the repository -# 🍕 Community +```bash +git clone https://github.com/fullstackhero/dotnet-starter-kit.git MyApp +cd MyApp +dotnet restore src/FSH.Starter.slnx +dotnet run --project src/Host/FSH.Starter.AppHost +``` -Thanks to the community who contribute to this repository! [Submit your PR and join the elite list!](CONTRIBUTING.md) +### Option 4: GitHub Codespaces -[![FullStackHero .NET Starter Kit Contributors](https://contrib.rocks/image?repo=fullstackhero/dotnet-starter-kit "FullStackHero .NET Starter Kit Contributors")](https://github.com/fullstackhero/dotnet-starter-kit/graphs/contributors) +Click **"Use this template"** on GitHub, or open in Codespaces for a zero-install experience with .NET 10, Docker, and Aspire pre-configured. -# 📝 Notes +> Prerequisites: [.NET 10 SDK](https://dotnet.microsoft.com/download), [Docker](https://www.docker.com/) (for Postgres/Redis via Aspire) -## Add Migrations +## FSH CLI Commands -Navigate to `./api/server` and run the following EF CLI commands. +| Command | Description | +|---------|------------| +| `fsh new [name]` | Create a new project with interactive wizard | +| `fsh doctor` | Check your environment (SDK, Docker, Aspire, ports) | +| `fsh info` | Show CLI/template versions and available updates | +| `fsh update` | Update CLI tool and template to latest | ```bash -dotnet ef migrations add "Add Identity Schema" --project .././migrations/postgresql/ --context IdentityDbContext -o Identity -dotnet ef migrations add "Add Tenant Schema" --project .././migrations/postgresql/ --context TenantDbContext -o Tenant -dotnet ef migrations add "Add Todo Schema" --project .././migrations/postgresql/ --context TodoDbContext -o Todo -dotnet ef migrations add "Add Catalog Schema" --project .././migrations/postgresql/ --context CatalogDbContext -o Catalog -``` - -## What's Pending? +# Non-interactive with options +fsh new MyApp --db sqlserver --no-aspire --no-git -- Few Identity Endpoints -- Blazor Client -- File Storage Service -- NuGet Generation Pipeline -- Source Code Generation -- Searching / Sorting - -# ⚖️ LICENSE +# Dry run (preview without creating) +fsh new MyApp --dry-run +``` -MIT © [fullstackhero](LICENSE) +## Why teams pick this +- Modular vertical slices: drop `Modules.Identity`, `Modules.Multitenancy`, `Modules.Auditing`, `Modules.Webhooks` into any API and let the module loader wire endpoints. +- Battle-tested building blocks: persistence + specifications, distributed caching, mailing, jobs via Hangfire, storage abstractions, and web host primitives (auth, rate limiting, versioning, CORS, exception handling). +- Cloud-ready out of the box: Aspire AppHost spins up Postgres + Redis + the API host with OTLP tracing enabled. +- Multi-tenant from day one: Finbuckle-powered tenancy across Identity and your module DbContexts; helpers to migrate and seed tenant databases on startup. +- Observability baked in: OpenTelemetry traces/metrics/logs, structured logging, health checks, and security/exception auditing. + +## Stack highlights +- .NET 10, C# latest, Minimal APIs, Mediator for commands/queries, FluentValidation. +- EF Core 10 with domain events + specifications; Postgres by default, SQL Server ready. +- ASP.NET Identity with JWT issuance/refresh, roles/permissions, rate-limited auth endpoints. +- Hangfire for background jobs; Redis-backed distributed cache; pluggable storage. +- API versioning, rate limiting, CORS, security headers, OpenAPI (Swagger) + Scalar docs. + +## Repository map +- `src/BuildingBlocks` — Core abstractions (DDD primitives, exceptions), Persistence, Caching, Mailing, Jobs, Storage, Web host wiring. +- `src/Modules` — `Identity`, `Multitenancy`, `Auditing`, `Webhooks` runtime + contracts projects. +- `src/Host` — Composition-root host (`FSH.Starter.Api`), Aspire app host (`FSH.Starter.AppHost`), Postgres migrations. +- `src/Tools/CLI` — The `fsh` CLI tool source code. +- `src/Tests` — Architecture tests that enforce layering and module boundaries. +- `deploy` — Docker, Dokploy, and Terraform deployment scaffolding. + +## Run it now (Aspire) +Prereqs: .NET 10 SDK, Aspire workload, Docker running (for Postgres/Redis). + +1. Restore: `dotnet restore src/FSH.Starter.slnx` +2. Start everything: `dotnet run --project src/Host/FSH.Starter.AppHost` + - Aspire brings up Postgres + Redis containers, wires env vars, launches the API host, and enables OTLP export on https://localhost:4317. +3. Hit the API: `https://localhost:5285` (Swagger/Scalar and module endpoints under `/api/v1/...`). + +### Run the API only +- Set env vars or appsettings for `DatabaseOptions__Provider`, `DatabaseOptions__ConnectionString`, `DatabaseOptions__MigrationsAssembly`, `CachingOptions__Redis`, and JWT options. +- Run: `dotnet run --project src/Host/FSH.Starter.Api` +- The host applies migrations/seeding via `UseHeroMultiTenantDatabases()` and maps module endpoints via `UseHeroPlatform`. + +## Bring the framework into your API +- Reference the building block and module projects you need. +- In `Program.cs`: + - Register Mediator with assemblies containing your commands/queries and module handlers. + - Call `builder.AddHeroPlatform(...)` to enable auth, OpenAPI, caching, mailing, jobs, health, OTel, rate limiting. + - Call `builder.AddModules(moduleAssemblies)` and `app.UseHeroPlatform(p => p.MapModules = true);`. +- Configure connection strings, Redis, JWT, CORS, and OTel endpoints via configuration. Example wiring lives in `src/Host/FSH.Starter.Api/Program.cs`. + +## Included modules +- **Identity** — ASP.NET Identity + JWT issuance/refresh, user/role/permission management, profile image storage, login/refresh auditing, health checks. +- **Multitenancy** — Tenant provisioning, migrations, status/upgrade APIs, tenant-aware EF Core contexts, health checks. +- **Auditing** — Security/exception/activity auditing with queryable endpoints; plugs into global exception handling and Identity events. +- **Webhooks** — Tenant-scoped webhook subscriptions with HMAC-signed delivery, retry policies, and delivery logs. + +## Development notes +- Target framework: `net10.0`; nullable enabled; analyzers on. +- Tests: `dotnet test src/FSH.Starter.slnx` (includes architecture guardrails). +- Want the deeper story? Start with `docs/framework/architecture.md` and the developer cookbook in `docs/framework/developer-cookbook.md`. + +Built and maintained by Mukesh Murugan for teams that want to ship faster without sacrificing architecture discipline. diff --git a/assets/fullstackhero-dotnet-starter-kit.png b/assets/fullstackhero-dotnet-starter-kit.png deleted file mode 100644 index d5ac1f26ff..0000000000 Binary files a/assets/fullstackhero-dotnet-starter-kit.png and /dev/null differ diff --git a/clients/admin/.gitignore b/clients/admin/.gitignore new file mode 100644 index 0000000000..4e2fa96baf --- /dev/null +++ b/clients/admin/.gitignore @@ -0,0 +1,25 @@ +node_modules +dist +dist-ssr +*.local +*.tsbuildinfo +.DS_Store + +# Env +.env +.env.local +.env.*.local + +# Editor +.vscode/* +!.vscode/extensions.json +.idea + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* + +# Generated +src/api/schema.d.ts diff --git a/clients/admin/README.md b/clients/admin/README.md new file mode 100644 index 0000000000..97328a36ea --- /dev/null +++ b/clients/admin/README.md @@ -0,0 +1,101 @@ +# FullStackHero — Admin + +Operator console for the FullStackHero .NET Starter Kit. Built with React 19, Vite 7, TypeScript, TanStack Query, React Router and Tailwind 4 + shadcn/ui. + +This is a **standalone Vite app** — not part of a pnpm workspace — so it can be mounted into .NET Aspire as a plain `ExecutableResource` without monorepo friction. + +## Prerequisites + +- Node.js 20+ +- The API running locally (`dotnet run --project src/Host/FSH.Starter.Api`, defaults to `http://localhost:5030`) + +## Install & run + +Two options — pick whichever matches how you want to develop. + +### Option A — run everything through Aspire (recommended) + +The AppHost launches Postgres, Redis, MinIO, the API, **and** this Vite app together, with `VITE_API_BASE_URL` wired via service discovery. + +```bash +npm install --prefix clients/admin # one-time +dotnet run --project src/Host/FSH.Starter.AppHost +``` + +Aspire dashboard will expose `fsh-admin` on . + +### Option B — run the frontend standalone + +Useful when the API is already running elsewhere (container, remote). + +```bash +cd clients/admin +npm install +npm run dev # http://localhost:5173 +``` + +The dev server proxies `/api`, `/openapi`, and `/scalar` to `VITE_API_BASE_URL` (default `http://localhost:5030`), so browser requests stay same-origin. + +## Scripts + +| Script | Purpose | +|-------------------|--------------------------------------| +| `npm run dev` | Vite dev server on port 5173 | +| `npm run build` | `tsc -b` + `vite build` → `dist/` | +| `npm run preview` | Preview the production build | +| `npm run lint` | ESLint (flat config) | + +## Configuration + +Environment variables are read via `import.meta.env` and surfaced through `src/env.ts`: + +| Variable | Default | Purpose | +|---------------------------|--------------------------|-----------------------------------------------| +| `VITE_API_BASE_URL` | `http://localhost:5030` | API origin used by the dev proxy | +| `VITE_DEFAULT_TENANT` | `root` | Default tenant header for unauthenticated calls | + +Create `.env.local` to override locally. + +## Structure + +``` +src/ +├── api/ # Typed API client functions (per backend feature) +├── auth/ # Token store, JWT decode, auth context, protected route +├── components/ +│ ├── layout/ # Sidebar, Topbar, AppShell +│ └── ui/ # shadcn primitives (Button, Card, Input, Label, Table) +├── lib/ +│ ├── api-client.ts # fetch wrapper: auth header, tenant header, single-flight refresh +│ ├── query-client.ts # TanStack QueryClient +│ └── cn.ts # clsx + tailwind-merge +├── pages/ # Route-level components +├── styles/globals.css # Tailwind 4 CSS-first + shadcn CSS variables +├── App.tsx # Provider tree (QueryClient, Auth, Router) +├── main.tsx # React entry +└── routes.tsx # Route definitions +``` + +## Authentication flow + +1. `POST /api/v1/identity/token/issue` with `{ email, password }` plus `tenant` header. +2. Access + refresh tokens are stored in `localStorage` (keys prefixed `fsh.admin.`). +3. The API client attaches `Authorization: Bearer ` and `tenant: ` on every call. +4. On `401`, a single-flight refresh call hits `POST /api/v1/identity/token/refresh`, retries the original request, and logs the user out if the refresh fails. + +## Styling + +- Tailwind 4 CSS-first config lives in `src/styles/globals.css` (no `tailwind.config.ts`). +- Colors use shadcn/ui oklch CSS variables; dark mode is toggled via the `.dark` class on ``. +- shadcn components follow the **new-york** style; `components.json` is present for `npx shadcn add ...`. + +## Adding a new page + +1. Add the API function in `src/api/.ts`. +2. Add the page component in `src/pages//.tsx`. +3. Register it in `src/routes.tsx` as a child of the `AppShell` route. +4. Add a nav entry in `src/components/layout/sidebar.tsx`. + +## Production build + +`npm run build` emits a static bundle to `dist/`. Host it behind any static web server (nginx, Caddy, Azure Static Web Apps, CloudFront, …). Configure the reverse proxy to forward `/api/*` to the backend and serve `index.html` as the SPA fallback for unmatched routes. diff --git a/clients/admin/components.json b/clients/admin/components.json new file mode 100644 index 0000000000..408958d879 --- /dev/null +++ b/clients/admin/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/cn", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/clients/admin/eslint.config.js b/clients/admin/eslint.config.js new file mode 100644 index 0000000000..fdd39034f2 --- /dev/null +++ b/clients/admin/eslint.config.js @@ -0,0 +1,24 @@ +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { ignores: ['dist', 'node_modules', '*.config.js'] }, + { + extends: [...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + }, + }, +); diff --git a/clients/admin/index.html b/clients/admin/index.html new file mode 100644 index 0000000000..dd72e62969 --- /dev/null +++ b/clients/admin/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + FullStackHero — Admin + + +
+ + + diff --git a/clients/admin/package-lock.json b/clients/admin/package-lock.json new file mode 100644 index 0000000000..4520cb58cc --- /dev/null +++ b/clients/admin/package-lock.json @@ -0,0 +1,5174 @@ +{ + "name": "@fullstackhero/admin", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@fullstackhero/admin", + "version": "0.1.0", + "dependencies": { + "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-dropdown-menu": "^2.1.5", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@tanstack/react-query": "^5.66.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.475.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", + "react-router-dom": "^7.1.5", + "sonner": "^2.0.7", + "tailwind-merge": "^3.0.1", + "zod": "^3.24.2" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.6", + "@types/node": "^22.13.1", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.19.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.18", + "globals": "^15.14.0", + "openapi-typescript": "^7.6.1", + "tailwindcss": "^4.0.6", + "typescript": "^5.7.3", + "typescript-eslint": "^8.23.0", + "vite": "^7.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/ajv/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.12", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.12.tgz", + "integrity": "sha512-b32XWsz6enN6K4bx8xWsqUaXTJR/DnYT3lL1CzDYzIYKw243NNlz6fexmr71q/U4HrEcMoJGBvwAfcxOb8ymQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.1.1", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.99.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.99.2.tgz", + "integrity": "sha512-1HunU0bXVsR1ZJMZbcOPE6VtaBJxsW809RE9xPe4Gz7MlB0GWwQvuTPhMoEmQ/hIzFKJ/DWAuttIe7BOaWx0tA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.99.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.99.2.tgz", + "integrity": "sha512-vM91UEe45QUS9ED6OklsVL15i8qKcRqNwpWzPTVWvRPRSEgDudDgHpvyTjcdlwHcrKNa80T+xXYcchT2noPnZA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.99.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", + "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/type-utils": "8.58.2", + "@typescript-eslint/utils": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz", + "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", + "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.2", + "@typescript-eslint/types": "^8.58.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", + "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", + "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", + "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", + "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", + "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.2", + "@typescript-eslint/tsconfig-utils": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", + "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", + "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", + "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.340", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz", + "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.475.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.475.0.tgz", + "integrity": "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/openapi-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", + "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.6", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-hook-form": { + "version": "7.72.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.1.tgz", + "integrity": "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.1.tgz", + "integrity": "sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.1.tgz", + "integrity": "sha512-ZkrQuwwhGibjQLqH1eCdyiZyLWglPxzxdl5tgwgKEyCSGC76vmAjleGocRe3J/MLfzMUIKwaFJWpFVJhK3d2xA==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz", + "integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.58.2", + "@typescript-eslint/parser": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/clients/admin/package.json b/clients/admin/package.json new file mode 100644 index 0000000000..c7edb29812 --- /dev/null +++ b/clients/admin/package.json @@ -0,0 +1,49 @@ +{ + "name": "@fullstackhero/admin", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --port 5173", + "build": "tsc -b && vite build", + "preview": "vite preview --port 4173", + "lint": "eslint ." + }, + "dependencies": { + "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-dropdown-menu": "^2.1.5", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@tanstack/react-query": "^5.66.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.475.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", + "react-router-dom": "^7.1.5", + "sonner": "^2.0.7", + "tailwind-merge": "^3.0.1", + "zod": "^3.24.2" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.6", + "@types/node": "^22.13.1", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.19.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.18", + "globals": "^15.14.0", + "openapi-typescript": "^7.6.1", + "tailwindcss": "^4.0.6", + "typescript": "^5.7.3", + "typescript-eslint": "^8.23.0", + "vite": "^7.0.0" + }, + "engines": { + "node": ">=20" + } +} diff --git a/clients/admin/public/favicon.svg b/clients/admin/public/favicon.svg new file mode 100644 index 0000000000..9702aa091e --- /dev/null +++ b/clients/admin/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/clients/admin/src/App.tsx b/clients/admin/src/App.tsx new file mode 100644 index 0000000000..701a4eb399 --- /dev/null +++ b/clients/admin/src/App.tsx @@ -0,0 +1,17 @@ +import { RouterProvider } from "react-router-dom"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "sonner"; +import { queryClient } from "@/lib/query-client"; +import { AuthProvider } from "@/auth/auth-context"; +import { router } from "@/routes"; + +export function App() { + return ( + + + + + + + ); +} diff --git a/clients/admin/src/api/roles.ts b/clients/admin/src/api/roles.ts new file mode 100644 index 0000000000..7618ba237e --- /dev/null +++ b/clients/admin/src/api/roles.ts @@ -0,0 +1,12 @@ +import { apiFetch } from "@/lib/api-client"; + +export type RoleDto = { + id: string; + name: string; + description?: string | null; + permissions?: string[] | null; +}; + +export async function listRoles(): Promise { + return apiFetch(`/api/v1/identity/roles`); +} diff --git a/clients/admin/src/api/tenants.ts b/clients/admin/src/api/tenants.ts new file mode 100644 index 0000000000..95f04cda27 --- /dev/null +++ b/clients/admin/src/api/tenants.ts @@ -0,0 +1,100 @@ +import { apiFetch } from "@/lib/api-client"; +import type { PagedResponse } from "@/lib/api-types"; + +export type { PagedResponse } from "@/lib/api-types"; + +export type TenantDto = { + id: string; + name: string; + adminEmail: string; + isActive: boolean; + validUpto: string; + issuer?: string; +}; + +export type ListTenantsParams = { + pageNumber?: number; + pageSize?: number; + sort?: string; +}; + +export type CreateTenantInput = { + id: string; + name: string; + adminEmail: string; + issuer: string; + connectionString?: string | null; +}; + +export type CreateTenantResponse = { + id: string; + provisioningCorrelationId?: string; + status?: string; +}; + +export type TenantLifecycleResult = { + tenantId: string; + isActive: boolean; +}; + +export type TenantProvisioningStep = { + step: string; + status: string; + startedUtc?: string | null; + completedUtc?: string | null; + error?: string | null; +}; + +export type TenantProvisioningStatus = { + tenantId: string; + status: string; + correlationId: string; + currentStep?: string | null; + error?: string | null; + createdUtc: string; + startedUtc?: string | null; + completedUtc?: string | null; + steps: TenantProvisioningStep[]; +}; + +export async function listTenants(params: ListTenantsParams = {}): Promise> { + const query = new URLSearchParams(); + query.set("PageNumber", String(params.pageNumber ?? 1)); + query.set("PageSize", String(params.pageSize ?? 10)); + if (params.sort) query.set("Sort", params.sort); + return apiFetch>(`/api/v1/tenants/?${query.toString()}`); +} + +export async function getTenantStatus(id: string): Promise { + return apiFetch(`/api/v1/tenants/${encodeURIComponent(id)}/status`); +} + +export async function getTenantProvisioningStatus(id: string): Promise { + return apiFetch(`/api/v1/tenants/${encodeURIComponent(id)}/provisioning`); +} + +export async function createTenant(input: CreateTenantInput): Promise { + return apiFetch(`/api/v1/tenants/`, { + method: "POST", + body: JSON.stringify({ + id: input.id, + name: input.name, + adminEmail: input.adminEmail, + issuer: input.issuer, + connectionString: input.connectionString ?? null, + }), + }); +} + +export async function changeTenantActivation(id: string, isActive: boolean): Promise { + return apiFetch(`/api/v1/tenants/${encodeURIComponent(id)}/activation`, { + method: "POST", + body: JSON.stringify({ tenantId: id, isActive }), + }); +} + +export async function retryTenantProvisioning(id: string): Promise { + return apiFetch(`/api/v1/tenants/${encodeURIComponent(id)}/provisioning/retry`, { + method: "POST", + }); +} diff --git a/clients/admin/src/api/users.ts b/clients/admin/src/api/users.ts new file mode 100644 index 0000000000..f7ea9d097c --- /dev/null +++ b/clients/admin/src/api/users.ts @@ -0,0 +1,89 @@ +import { apiFetch } from "@/lib/api-client"; +import type { PagedResponse } from "@/lib/api-types"; + +export type UserDto = { + id: string; + userName?: string | null; + firstName?: string | null; + lastName?: string | null; + email?: string | null; + isActive: boolean; + emailConfirmed: boolean; + phoneNumber?: string | null; + imageUrl?: string | null; +}; + +export type UserRoleDto = { + roleId: string; + roleName: string; + description?: string | null; + enabled: boolean; +}; + +export type SearchUsersParams = { + pageNumber?: number; + pageSize?: number; + sort?: string; + search?: string; + isActive?: boolean; + emailConfirmed?: boolean; + roleId?: string; +}; + +export type RegisterUserInput = { + firstName: string; + lastName: string; + email: string; + userName: string; + password: string; + confirmPassword: string; + phoneNumber?: string; +}; + +export type RegisterUserResponse = { + userId: string; + message?: string; +}; + +const BASE = "/api/v1/identity/users"; + +export async function searchUsers(params: SearchUsersParams = {}): Promise> { + const q = new URLSearchParams(); + q.set("PageNumber", String(params.pageNumber ?? 1)); + q.set("PageSize", String(params.pageSize ?? 10)); + if (params.sort) q.set("Sort", params.sort); + if (params.search?.trim()) q.set("Search", params.search.trim()); + if (params.isActive !== undefined) q.set("IsActive", String(params.isActive)); + if (params.emailConfirmed !== undefined) q.set("EmailConfirmed", String(params.emailConfirmed)); + if (params.roleId) q.set("RoleId", params.roleId); + return apiFetch>(`${BASE}/search?${q.toString()}`); +} + +export async function getUser(id: string): Promise { + return apiFetch(`${BASE}/${encodeURIComponent(id)}`); +} + +export async function getUserRoles(id: string): Promise { + return apiFetch(`${BASE}/${encodeURIComponent(id)}/roles`); +} + +export async function registerUser(input: RegisterUserInput): Promise { + return apiFetch(`${BASE}/register`, { + method: "POST", + body: JSON.stringify(input), + }); +} + +export async function toggleUserStatus(id: string, activateUser: boolean): Promise { + await apiFetch(`${BASE}/${encodeURIComponent(id)}`, { + method: "PATCH", + body: JSON.stringify({ userId: id, activateUser }), + }); +} + +export async function assignUserRoles(id: string, roles: UserRoleDto[]): Promise { + return apiFetch(`${BASE}/${encodeURIComponent(id)}/roles`, { + method: "POST", + body: JSON.stringify({ userId: id, userRoles: roles }), + }); +} diff --git a/clients/admin/src/auth/api.ts b/clients/admin/src/auth/api.ts new file mode 100644 index 0000000000..bc0489dc0b --- /dev/null +++ b/clients/admin/src/auth/api.ts @@ -0,0 +1,23 @@ +import { apiFetch } from "@/lib/api-client"; + +export type TokenResponse = { + accessToken: string; + refreshToken: string; + accessTokenExpiresAt: string; + refreshTokenExpiresAt: string; +}; + +export function issueToken(input: { + email: string; + password: string; + tenant: string; +}) { + return apiFetch("/api/v1/identity/token/issue", { + method: "POST", + body: JSON.stringify({ email: input.email, password: input.password }), + // X-FSH-App marks this client as the platform-admin app. Used by the + // API to enforce the SuperAdmin / dashboard boundary. + headers: { tenant: input.tenant, "X-FSH-App": "admin" }, + skipAuth: true, + }); +} diff --git a/clients/admin/src/auth/auth-context.tsx b/clients/admin/src/auth/auth-context.tsx new file mode 100644 index 0000000000..3f32f4d22b --- /dev/null +++ b/clients/admin/src/auth/auth-context.tsx @@ -0,0 +1,77 @@ +import { createContext, useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { tokenStore } from "@/auth/token-store"; +import { decodeJwt, type JwtClaims } from "@/auth/jwt"; +import { issueToken } from "@/auth/api"; + +export type AuthUser = { + id: string; + email?: string; + name?: string; + tenant?: string; + permissions: string[]; +}; + +export type AuthContextValue = { + user: AuthUser | null; + isAuthenticated: boolean; + login: (input: { email: string; password: string; tenant: string }) => Promise; + logout: () => void; +}; + +export const AuthContext = createContext(null); + +function claimsToUser(claims: JwtClaims | null): AuthUser | null { + if (!claims?.sub) return null; + const permissions = Array.isArray(claims.permissions) + ? claims.permissions + : typeof claims.permissions === "string" + ? [claims.permissions] + : []; + return { + id: claims.sub, + email: claims.email, + name: claims.name, + tenant: claims.tenant, + permissions, + }; +} + +export function AuthProvider({ children }: { children: ReactNode }) { + const queryClient = useQueryClient(); + const [user, setUser] = useState(() => + claimsToUser(decodeJwt(tokenStore.getAccessToken())), + ); + + useEffect(() => { + return tokenStore.subscribe(() => { + setUser(claimsToUser(decodeJwt(tokenStore.getAccessToken()))); + }); + }, []); + + const login = useCallback( + async (input: { email: string; password: string; tenant: string }) => { + tokenStore.setTenant(input.tenant); + const tokens = await issueToken(input); + tokenStore.setTokens(tokens.accessToken, tokens.refreshToken); + }, + [], + ); + + const logout = useCallback(() => { + tokenStore.clear(); + queryClient.clear(); + }, [queryClient]); + + const value = useMemo( + () => ({ + user, + isAuthenticated: user !== null, + login, + logout, + }), + [user, login, logout], + ); + + return {children}; +} diff --git a/clients/admin/src/auth/jwt.ts b/clients/admin/src/auth/jwt.ts new file mode 100644 index 0000000000..f6d81770e0 --- /dev/null +++ b/clients/admin/src/auth/jwt.ts @@ -0,0 +1,23 @@ +export type JwtClaims = { + sub?: string; + email?: string; + name?: string; + tenant?: string; + permissions?: string[] | string; + exp?: number; + [key: string]: unknown; +}; + +export function decodeJwt(token: string | null | undefined): JwtClaims | null { + if (!token) return null; + const parts = token.split("."); + if (parts.length !== 3) return null; + try { + const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/"); + const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4); + const json = atob(padded); + return JSON.parse(json) as JwtClaims; + } catch { + return null; + } +} diff --git a/clients/admin/src/auth/protected-route.tsx b/clients/admin/src/auth/protected-route.tsx new file mode 100644 index 0000000000..5c589fe8fd --- /dev/null +++ b/clients/admin/src/auth/protected-route.tsx @@ -0,0 +1,31 @@ +import { Navigate, Outlet, useLocation } from "react-router-dom"; +import { useAuth } from "@/auth/use-auth"; +import { ForbiddenView } from "@/components/forbidden-view"; + +type ProtectedRouteProps = { + /** + * Optional list of permission strings. When provided, the current user must hold EVERY + * listed permission. Missing any one renders a 403 view instead of navigating to login. + * Omit or pass [] to keep the route auth-only (any signed-in user). + */ + permissions?: string[]; +}; + +export function ProtectedRoute({ permissions = [] }: ProtectedRouteProps) { + const { isAuthenticated, user } = useAuth(); + const location = useLocation(); + + if (!isAuthenticated) { + return ; + } + + if (permissions.length > 0) { + const granted = user?.permissions ?? []; + const missing = permissions.filter((p) => !granted.includes(p)); + if (missing.length > 0) { + return ; + } + } + + return ; +} diff --git a/clients/admin/src/auth/token-store.ts b/clients/admin/src/auth/token-store.ts new file mode 100644 index 0000000000..c6c3275e0b --- /dev/null +++ b/clients/admin/src/auth/token-store.ts @@ -0,0 +1,41 @@ +const ACCESS_KEY = "fsh.admin.accessToken"; +const REFRESH_KEY = "fsh.admin.refreshToken"; +const TENANT_KEY = "fsh.admin.tenant"; + +type Listener = () => void; + +const listeners = new Set(); + +function emit() { + for (const listener of listeners) listener(); +} + +export const tokenStore = { + getAccessToken: () => localStorage.getItem(ACCESS_KEY), + getRefreshToken: () => localStorage.getItem(REFRESH_KEY), + getTenant: () => localStorage.getItem(TENANT_KEY), + + setTokens(accessToken: string, refreshToken: string) { + localStorage.setItem(ACCESS_KEY, accessToken); + localStorage.setItem(REFRESH_KEY, refreshToken); + emit(); + }, + + setTenant(tenant: string) { + localStorage.setItem(TENANT_KEY, tenant); + emit(); + }, + + clear() { + localStorage.removeItem(ACCESS_KEY); + localStorage.removeItem(REFRESH_KEY); + emit(); + }, + + subscribe(listener: Listener) { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, +}; diff --git a/clients/admin/src/auth/use-auth.ts b/clients/admin/src/auth/use-auth.ts new file mode 100644 index 0000000000..e66d0827d8 --- /dev/null +++ b/clients/admin/src/auth/use-auth.ts @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import { AuthContext } from "@/auth/auth-context"; + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) { + throw new Error("useAuth must be used within AuthProvider"); + } + return ctx; +} diff --git a/clients/admin/src/components/forbidden-view.tsx b/clients/admin/src/components/forbidden-view.tsx new file mode 100644 index 0000000000..3326d0975c --- /dev/null +++ b/clients/admin/src/components/forbidden-view.tsx @@ -0,0 +1,33 @@ +import { Link } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +type ForbiddenViewProps = { + /** Permission strings the caller required that the user doesn't hold. Shown for operator clarity. */ + missing?: string[]; +}; + +export function ForbiddenView({ missing }: ForbiddenViewProps) { + return ( +
+ + + Access denied + + Your account doesn't have permission to view this area. + + + + {missing && missing.length > 0 && ( +
+ Missing: {missing.join(", ")} +
+ )} + +
+
+
+ ); +} diff --git a/clients/admin/src/components/layout/app-shell.tsx b/clients/admin/src/components/layout/app-shell.tsx new file mode 100644 index 0000000000..dae51b709a --- /dev/null +++ b/clients/admin/src/components/layout/app-shell.tsx @@ -0,0 +1,17 @@ +import { Outlet } from "react-router-dom"; +import { Sidebar } from "@/components/layout/sidebar"; +import { Topbar } from "@/components/layout/topbar"; + +export function AppShell() { + return ( +
+ +
+ +
+ +
+
+
+ ); +} diff --git a/clients/admin/src/components/layout/sidebar.tsx b/clients/admin/src/components/layout/sidebar.tsx new file mode 100644 index 0000000000..20e48f0fbe --- /dev/null +++ b/clients/admin/src/components/layout/sidebar.tsx @@ -0,0 +1,48 @@ +import { NavLink } from "react-router-dom"; +import { LayoutDashboard, Building2, UsersRound, Receipt, Gauge } from "lucide-react"; +import { cn } from "@/lib/cn"; + +type NavItem = { + to: string; + label: string; + icon: React.ComponentType<{ className?: string }>; +}; + +const items: NavItem[] = [ + { to: "/", label: "Dashboard", icon: LayoutDashboard }, + { to: "/tenants", label: "Tenants", icon: Building2 }, + { to: "/users", label: "Users", icon: UsersRound }, + { to: "/billing", label: "Billing", icon: Receipt }, + { to: "/quota", label: "Quota", icon: Gauge }, +]; + +export function Sidebar() { + return ( + + ); +} diff --git a/clients/admin/src/components/layout/topbar.tsx b/clients/admin/src/components/layout/topbar.tsx new file mode 100644 index 0000000000..5a315120dc --- /dev/null +++ b/clients/admin/src/components/layout/topbar.tsx @@ -0,0 +1,28 @@ +import { LogOut } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useAuth } from "@/auth/use-auth"; + +export function Topbar() { + const { user, logout } = useAuth(); + return ( +
+
+ + Tenant: {user?.tenant ?? "—"} + +
+
+
+
{user?.name ?? user?.email ?? "Unknown"}
+ {user?.email && user.name && ( +
{user.email}
+ )} +
+ +
+
+ ); +} diff --git a/clients/admin/src/components/monogram.tsx b/clients/admin/src/components/monogram.tsx new file mode 100644 index 0000000000..acb42f89dd --- /dev/null +++ b/clients/admin/src/components/monogram.tsx @@ -0,0 +1,57 @@ +import { cn } from "@/lib/cn"; + +const TONES = ["mono-tone-0", "mono-tone-1", "mono-tone-2", "mono-tone-3"] as const; + +function toneFor(seed: string): string { + let h = 2166136261; + for (let i = 0; i < seed.length; i++) { + h ^= seed.charCodeAt(i); + h = (h * 16777619) >>> 0; + } + return TONES[h % TONES.length]; +} + +function initialsFor(first?: string | null, last?: string | null, fallback?: string | null): string { + const f = (first ?? "").trim(); + const l = (last ?? "").trim(); + if (f && l) return (f[0] + l[0]).toUpperCase(); + if (f) return f.slice(0, 2).toUpperCase(); + if (l) return l.slice(0, 2).toUpperCase(); + const fb = (fallback ?? "??").trim(); + return (fb.slice(0, 2) || "??").toUpperCase(); +} + +type Size = "sm" | "md" | "lg"; + +const SIZE_CLASS: Record = { + sm: "h-8 w-8 text-[0.6875rem]", + md: "h-10 w-10 text-xs", + lg: "h-20 w-20 text-2xl", +}; + +export type MonogramProps = { + seed: string; + firstName?: string | null; + lastName?: string | null; + fallback?: string | null; + size?: Size; + className?: string; +}; + +export function Monogram({ seed, firstName, lastName, fallback, size = "md", className }: MonogramProps) { + const tone = toneFor(seed || fallback || "x"); + const initials = initialsFor(firstName, lastName, fallback); + return ( +
+ {initials} +
+ ); +} diff --git a/clients/admin/src/components/route-error.tsx b/clients/admin/src/components/route-error.tsx new file mode 100644 index 0000000000..8a18ea721a --- /dev/null +++ b/clients/admin/src/components/route-error.tsx @@ -0,0 +1,50 @@ +import { isRouteErrorResponse, useNavigate, useRouteError } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; + +/** + * Route-level error element. React Router v7 passes any error thrown during rendering, + * loading, or an action to the nearest . Without one, a render + * error white-screens the whole app — this replaces that with a recoverable view. + */ +export function RouteError() { + const error = useRouteError(); + const navigate = useNavigate(); + + const { title, detail } = describe(error); + + return ( +
+ + + Something went wrong + {title} + + + {detail && ( +
+              {detail}
+            
+ )} +
+ + + + +
+
+ ); +} + +function describe(error: unknown): { title: string; detail?: string } { + if (isRouteErrorResponse(error)) { + return { + title: `${error.status} ${error.statusText}`, + detail: typeof error.data === "string" ? error.data : JSON.stringify(error.data, null, 2), + }; + } + if (error instanceof Error) { + return { title: error.message, detail: error.stack }; + } + return { title: "Unexpected error", detail: String(error) }; +} diff --git a/clients/admin/src/components/section-rule.tsx b/clients/admin/src/components/section-rule.tsx new file mode 100644 index 0000000000..9bf123f28b --- /dev/null +++ b/clients/admin/src/components/section-rule.tsx @@ -0,0 +1,35 @@ +import type { ReactNode } from "react"; +import { cn } from "@/lib/cn"; + +export type SectionCrumb = { + label: string; + muted?: boolean; +}; + +export type SectionRuleProps = { + crumbs: SectionCrumb[]; + trailing?: ReactNode; + className?: string; +}; + +/** + * Editorial top-rule used on every page within a module section. + * Renders a hairline above breadcrumb-style mono-caps labels separated by "\\". + */ +export function SectionRule({ crumbs, trailing, className }: SectionRuleProps) { + return ( +
+
+ {crumbs.map((c, i) => ( + + {i > 0 && \\} + + {c.label} + + + ))} +
+ {trailing &&
{trailing}
} +
+ ); +} diff --git a/clients/admin/src/components/ui/button.tsx b/clients/admin/src/components/ui/button.tsx new file mode 100644 index 0000000000..6715ed8ea5 --- /dev/null +++ b/clients/admin/src/components/ui/button.tsx @@ -0,0 +1,52 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/cn"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-[var(--color-primary)] text-[var(--color-primary-foreground)] hover:opacity-90", + destructive: "bg-[var(--color-destructive)] text-[var(--color-destructive-foreground)] hover:opacity-90", + outline: "border border-[var(--color-border)] bg-[var(--color-background)] hover:bg-[var(--color-accent)] hover:text-[var(--color-accent-foreground)]", + secondary: "bg-[var(--color-secondary)] text-[var(--color-secondary-foreground)] hover:opacity-90", + ghost: "hover:bg-[var(--color-accent)] hover:text-[var(--color-accent-foreground)]", + link: "text-[var(--color-primary)] underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-6", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +export const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { buttonVariants }; diff --git a/clients/admin/src/components/ui/card.tsx b/clients/admin/src/components/ui/card.tsx new file mode 100644 index 0000000000..495927dbdf --- /dev/null +++ b/clients/admin/src/components/ui/card.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { cn } from "@/lib/cn"; + +export const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +Card.displayName = "Card"; + +export const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardHeader.displayName = "CardHeader"; + +export const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardTitle.displayName = "CardTitle"; + +export const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardDescription.displayName = "CardDescription"; + +export const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardContent.displayName = "CardContent"; + +export const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardFooter.displayName = "CardFooter"; diff --git a/clients/admin/src/components/ui/input.tsx b/clients/admin/src/components/ui/input.tsx new file mode 100644 index 0000000000..c15c2694ee --- /dev/null +++ b/clients/admin/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; +import { cn } from "@/lib/cn"; + +export type InputProps = React.InputHTMLAttributes; + +export const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; diff --git a/clients/admin/src/components/ui/label.tsx b/clients/admin/src/components/ui/label.tsx new file mode 100644 index 0000000000..a9fb7a1bc1 --- /dev/null +++ b/clients/admin/src/components/ui/label.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cn } from "@/lib/cn"; + +export const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; diff --git a/clients/admin/src/components/ui/table.tsx b/clients/admin/src/components/ui/table.tsx new file mode 100644 index 0000000000..eae22cef8f --- /dev/null +++ b/clients/admin/src/components/ui/table.tsx @@ -0,0 +1,60 @@ +import * as React from "react"; +import { cn } from "@/lib/cn"; + +export const Table = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ + + ), +); +Table.displayName = "Table"; + +export const TableHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +); +TableHeader.displayName = "TableHeader"; + +export const TableBody = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +); +TableBody.displayName = "TableBody"; + +export const TableRow = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +); +TableRow.displayName = "TableRow"; + +export const TableHead = React.forwardRef>( + ({ className, ...props }, ref) => ( + ; +} + +function Row({ + row, + striped, + onClick, +}: { + row: AuditSummaryDto; + striped: boolean; + onClick: () => void; +}) { + const ts = fmtIsoDense(row.occurredAtUtc); + const Icon = eventTypeIcon(row.eventType); + const tone = severityTone(row.severity); + const toneColor = severityColorVar(row.severity); + const tags = decodeTags(row.tags); + + return ( + + {/* Severity tone bar — 2px on the leftmost edge */} + + + + + + + + + + ); +} + +// ──────────────────────────────────────────────────────────────────────── +// Pagination footer (custom — list/Pagination doesn't expose page jumps) +// ──────────────────────────────────────────────────────────────────────── + +function PaginationFooter({ + page, + pageSize, + totalCount, + totalPages, + shown, + fetching, + onPage, +}: { + page: number; + pageSize: number; + totalCount: number; + totalPages: number; + shown: number; + fetching: boolean; + onPage: (p: number) => void; +}) { + const start = (page - 1) * pageSize + 1; + const end = start + shown - 1; + return ( +
+ + {start.toLocaleString()}–{end.toLocaleString()} of{" "} + + {totalCount.toLocaleString()} + + {" · "} + page {page} / {totalPages} + +
+ + +
+
+ ); +} + +// ──────────────────────────────────────────────────────────────────────── +// Detail drawer — right-side panel with the full payload, metadata grid, +// and "jump to correlated/traced events" actions. Re-uses the existing +// Dialog primitive but overrides positioning so it slides in from the +// right instead of opening centered. +// ──────────────────────────────────────────────────────────────────────── + +function AuditDetailDrawer({ + auditId, + onClose, + onJumpAudit, + onJumpCorrelation, + onJumpTrace, +}: { + auditId: string | null; + onClose: () => void; + onJumpAudit: (id: string) => void; + onJumpCorrelation: (id: string) => void; + onJumpTrace: (id: string) => void; +}) { + const open = auditId !== null; + + const detail = useQuery({ + queryKey: ["audit", "detail", auditId], + queryFn: ({ signal }) => getAuditById(auditId!, signal), + enabled: open, + staleTime: 60_000, + }); + + return ( + !v && onClose()}> + + + + Audit detail + + Full payload, identifiers, and related actions for the selected audit event. + + +
+ + +
+ {detail.isLoading ? ( + + ) : detail.isError ? ( + + ) : detail.data ? ( + + ) : null} +
+
+ + + + +
+
+
+ ); +} + +function DrawerHeader({ detail, loading }: { detail?: AuditDetailDto; loading: boolean }) { + if (loading || !detail) { + return ( +
+ + + +
+ ); + } + + const Icon = eventTypeIcon(detail.eventType); + const tone = severityTone(detail.severity); + const toneColor = severityColorVar(detail.severity); + const ts = fmtIsoDense(detail.occurredAtUtc); + const tags = decodeTags(detail.tags); + + return ( +
+
+
+
+ + + + + {AUDIT_SEVERITY_LABELS[detail.severity]} + + + {AUDIT_EVENT_TYPE_LABELS[detail.eventType]} + +
+
+

+ {detail.source ?? "Audit event"} +

+
+
+ {ts.date} {ts.time} UTC + · + {fmtRelative(detail.occurredAtUtc)} +
+ {tags.length > 0 && ( +
+ {tags.map((t) => ( + + + {t} + + ))} +
+ )} +
+
+ ); +} + +function DrawerBody({ + detail, + onJumpAudit, + onJumpCorrelation, + onJumpTrace, +}: { + detail: AuditDetailDto; + onJumpAudit: (id: string) => void; + onJumpCorrelation: (id: string) => void; + onJumpTrace: (id: string) => void; +}) { + return ( +
+ {/* Identity grid */} +
+ Identity +
+ + + + +
+
+ + {/* Trace grid + jump links */} +
+ Trace +
+ + + + +
+
+ {detail.correlationId && ( + + )} + {detail.traceId && ( + + )} +
+
+ + {/* Related events — every audit sharing this correlation ID, + rendered as a vertical timeline. Click another row to swap + the drawer to that audit without closing. */} + {detail.correlationId && ( + + )} + + {/* Payload */} +
+
+ Payload + +
+
+          {JSON.stringify(detail.payload, null, 2)}
+        
+
+ + {/* Reception window */} +
+ Pipeline +
+ + + + +
+
+
+ ); +} + +function DrawerSkeleton() { + return ( +
+ {[0, 1, 2].map((i) => ( +
+ +
+ {[0, 1, 2, 3].map((j) => ( + + ))} +
+
+ ))} + +
+ ); +} + +function DrawerError({ message }: { message?: string }) { + return ( +
+ +
Could not load audit
+

+ {message ?? "The server returned an error fetching this audit. The record may have been purged by the retention job."} +

+
+ ); +} + +function SectionLabel({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function DefRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) { + return ( +
+
+ {label} +
+
{value}
+
+ ); +} + +function RelatedEventsSection({ + currentId, + correlationId, + currentOccurredAtUtc, + onJumpAudit, +}: { + currentId: string; + correlationId: string; + currentOccurredAtUtc: string; + onJumpAudit: (id: string) => void; +}) { + const related = useQuery({ + queryKey: ["audit", "by-correlation", correlationId], + queryFn: ({ signal }) => getAuditsByCorrelation(correlationId, {}, signal), + enabled: !!correlationId, + staleTime: 30_000, + }); + + // Newest → oldest, capped at 12 to keep the timeline bounded for + // chatty correlations. Memoised against the underlying response so a + // re-render doesn't re-sort. + const sorted = useMemo(() => { + const items = related.data ?? []; + return [...items] + .sort((a, b) => Date.parse(b.occurredAtUtc) - Date.parse(a.occurredAtUtc)) + .slice(0, 12); + }, [related.data]); + const others = sorted.filter((r) => r.id !== currentId); + const currentMs = Date.parse(currentOccurredAtUtc); + + return ( +
+
+ Related events + {!related.isLoading && ( + + {sorted.length} on this correlation + + )} +
+ + {related.isLoading ? ( +
+ {[0, 1, 2].map((i) => ( + + ))} +
+ ) : others.length === 0 ? ( +

+ No other events share this correlation. The full lifecycle of this + request is contained in the payload above. +

+ ) : ( +
    + {/* Vertical rail — fades top + bottom so it reads as a slice + of a longer timeline rather than a hard-bounded list. */} + + {sorted.map((row) => { + const isCurrent = row.id === currentId; + const tone = severityColorVar(row.severity); + const RowIcon = eventTypeIcon(row.eventType); + const deltaSec = Math.round((Date.parse(row.occurredAtUtc) - currentMs) / 1000); + const deltaLabel = + isCurrent + ? "this event" + : deltaSec === 0 + ? "0s" + : deltaSec > 0 + ? `+${deltaSec}s` + : `${deltaSec}s`; + return ( +
  1. + {/* Node dot */} + + +
  2. + ); + })} +
+ )} +
+ ); +} + +function CopyButton({ value }: { value: string }) { + const [copied, setCopied] = useState(false); + return ( + + ); +} diff --git a/clients/dashboard/src/pages/catalog/brands.tsx b/clients/dashboard/src/pages/catalog/brands.tsx new file mode 100644 index 0000000000..825042a89d --- /dev/null +++ b/clients/dashboard/src/pages/catalog/brands.tsx @@ -0,0 +1,941 @@ +import { + useCallback, + useEffect, + useMemo, + useState, + type FormEvent, +} from "react"; +import { + keepPreviousData, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { + Check, + Image as ImageIcon, + ImageOff, + Pencil, + Search, + Sparkles, + Trash2, + X, +} from "lucide-react"; +import { toast } from "sonner"; +import { + createBrand, + deleteBrand, + searchBrands, + updateBrand, + type BrandDto, + type CreateBrandInput, + type UpdateBrandInput, +} from "@/api/catalog"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogBody, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + DensityToggle, + EmptyState, + ErrorBand, + Field, + ListHero, + Pagination, + SortChips, + Stat, + StatStrip, + usePersistedDensity, + type Density, + type SortDir, + type SortOption, +} from "@/components/list"; +import { useAuth } from "@/auth/use-auth"; +import { cn } from "@/lib/cn"; +import { + describe, + formatDate, + formatDateMono, + formatRelative, + pad2, + slugify, +} from "@/lib/list-helpers"; + +const PAGE_SIZE = 20; +const DENSITY_KEY = "fsh.dashboard.catalog.brands.density"; + +type EditorState = + | { mode: "closed" } + | { mode: "create" } + | { mode: "edit"; brand: BrandDto } + | { mode: "delete"; brand: BrandDto }; + +type SortKey = "name" | "slug" | "createdAtUtc"; + +const SORT_OPTIONS: SortOption[] = [ + { key: "name", label: "Name" }, + { key: "slug", label: "Slug" }, + { key: "createdAtUtc", label: "Created" }, +]; + +// ─────────────────────────────────────────────────────────────────────── +// Page +// ─────────────────────────────────────────────────────────────────────── + +export function BrandsPage() { + const { user } = useAuth(); + const [search, setSearch] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); + const [pageNumber, setPageNumber] = useState(1); + const [editor, setEditor] = useState({ mode: "closed" }); + + const [sortKey, setSortKey] = useState("createdAtUtc"); + const [sortDir, setSortDir] = useState("desc"); + + const [density, setDensity] = usePersistedDensity(DENSITY_KEY); + + useEffect(() => { + const t = setTimeout(() => { + setDebouncedSearch(search.trim()); + setPageNumber(1); + }, 250); + return () => clearTimeout(t); + }, [search]); + + const query = useQuery({ + queryKey: [ + "catalog", + "brands", + { search: debouncedSearch, pageNumber, pageSize: PAGE_SIZE, sortKey, sortDir }, + ], + queryFn: () => + searchBrands({ + search: debouncedSearch || undefined, + pageNumber, + pageSize: PAGE_SIZE, + sortBy: sortKey, + sortDir, + }), + placeholderData: keepPreviousData, + }); + + const data = query.data; + const items = data?.items ?? []; + // Server-side sort drives the order. Items are already sorted on arrival. + const sortedItems = items; + + const featured = useMemo(() => { + if (debouncedSearch) return []; + return [...items] + .sort( + (a, b) => + new Date(b.createdAtUtc).getTime() - + new Date(a.createdAtUtc).getTime(), + ) + .slice(0, 6); + }, [items, debouncedSearch]); + + const stats = useMemo(() => { + if (!data) return null; + const total = data.totalCount; + const withLogos = items.filter((b) => b.logoUrl).length; + const pct = items.length === 0 ? 0 : Math.round((withLogos / items.length) * 100); + const latest = + items.length === 0 + ? null + : items.reduce( + (best, b) => + new Date(b.createdAtUtc) > new Date(best.createdAtUtc) ? b : best, + items[0], + ); + return { total, withLogosPct: pct, latest }; + }, [data, items]); + + const onSort = useCallback( + (key: SortKey) => { + if (sortKey === key) { + setSortDir((d) => (d === "asc" ? "desc" : "asc")); + } else { + setSortKey(key); + setSortDir(key === "createdAtUtc" ? "desc" : "asc"); + } + }, + [sortKey], + ); + + return ( +
+ void query.refetch()} + ctaLabel="New brand" + onCreate={() => setEditor({ mode: "create" })} + /> + + {stats && data && data.totalCount > 0 && ( + + + + + + )} + + {!debouncedSearch && featured.length > 1 && ( + setEditor({ mode: "edit", brand })} + /> + )} + + {query.isError && } + +
+
+ + +
+ +
+ {query.isLoading && items.length === 0 ? ( +
    + {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+ ) : sortedItems.length === 0 ? ( + (() => { + const filtered = debouncedSearch.length > 0; + return ( + + ) : ( + + ) + } + primaryAction={{ + label: filtered ? "Add a new brand" : "Add the first brand", + onClick: () => setEditor({ mode: "create" }), + icon: , + }} + secondaryAction={ + filtered + ? { + label: "Clear search", + onClick: () => setSearch(""), + icon: , + } + : undefined + } + /> + ); + })() + ) : ( +
    + {sortedItems.map((brand, i) => ( + setEditor({ mode: "edit", brand })} + onDelete={() => setEditor({ mode: "delete", brand })} + /> + ))} +
+ )} +
+
+ + {data && data.totalCount > 0 && ( + setPageNumber((p) => Math.max(1, p - 1))} + onNext={() => setPageNumber((p) => p + 1)} + hasPrev={data.hasPrevious} + hasNext={data.hasNext} + /> + )} + + setEditor({ mode: "closed" })} + /> + setEditor({ mode: "closed" })} + /> +
+ ); +} + +// ─────────────────────────────────────────────────────────────────────── +// Featured rail (Brand-specific — not lifted into shared primitives) +// ─────────────────────────────────────────────────────────────────────── + +function FeaturedRail({ + items, + onPick, +}: { + items: BrandDto[]; + onPick: (brand: BrandDto) => void; +}) { + return ( +
+
+
+

+ Recently added +

+ + {pad2(items.length)} on file + +
+
+ +
+
    + {items.map((brand) => ( +
  • + onPick(brand)} /> +
  • + ))} +
+
+
+ ); +} + +function FeaturedCard({ brand, onClick }: { brand: BrandDto; onClick: () => void }) { + const initial = brand.name.trim().charAt(0).toUpperCase() || "·"; + return ( + + ); +} + +// ─────────────────────────────────────────────────────────────────────── +// Row +// ─────────────────────────────────────────────────────────────────────── + +function Row({ + brand, + density, + delayMs, + onEdit, + onDelete, +}: { + brand: BrandDto; + density: Density; + delayMs: number; + onEdit: () => void; + onDelete: () => void; +}) { + const padY = density === "compact" ? "py-3" : "py-4"; + + return ( +
  • + + + + +
    +
    +
    + + {brand.name} + + + {brand.slug} + +
    +
    + {brand.description ?? "No description on file."} +
    +
    + +
    +
    + {formatDateMono(brand.createdAtUtc)} +
    +
    + {formatRelative(brand.createdAtUtc)} +
    +
    +
    + +
    + + + + + + +
    +
  • + ); +} + +function RowAction({ + label, + onClick, + tone = "default", + children, +}: { + label: string; + onClick: () => void; + tone?: "default" | "danger"; + children: React.ReactNode; +}) { + return ( + + ); +} + +function Swatch({ + logoUrl, + initial, + size, +}: { + logoUrl: string | null | undefined; + initial: string; + size: number; +}) { + const style = { width: size, height: size }; + if (logoUrl) { + return ( + + { + const target = e.currentTarget; + target.style.display = "none"; + target.parentElement + ?.querySelector("[data-fallback]") + ?.style.removeProperty("display"); + }} + /> + + {initial} + + + ); + } + + return ( + + + = 48 ? "text-[18px]" : "text-[14px]", + )} + > + {initial} + + + ); +} + +function SkeletonRow({ delayMs, density }: { delayMs: number; density: Density }) { + const padY = density === "compact" ? "py-3" : "py-4"; + return ( +
  • + +
    +
    + + +
    +
    + + +
    +
    + +
  • + ); +} + + +// ─────────────────────────────────────────────────────────────────────── +// Editor dialog +// ─────────────────────────────────────────────────────────────────────── + +function BrandEditorDialog({ + state, + onClose, +}: { + state: EditorState; + onClose: () => void; +}) { + const isOpen = state.mode === "create" || state.mode === "edit"; + const brand = state.mode === "edit" ? state.brand : undefined; + const queryClient = useQueryClient(); + + const initial = useMemo( + () => ({ + name: brand?.name ?? "", + description: brand?.description ?? "", + logoUrl: brand?.logoUrl ?? "", + }), + [brand?.id, brand?.name, brand?.description, brand?.logoUrl], + ); + + const [name, setName] = useState(initial.name); + const [description, setDescription] = useState(initial.description); + const [logoUrl, setLogoUrl] = useState(initial.logoUrl); + + useEffect(() => { + if (isOpen) { + setName(initial.name); + setDescription(initial.description); + setLogoUrl(initial.logoUrl); + } + }, [isOpen, initial.name, initial.description, initial.logoUrl]); + + const slugPreview = useMemo(() => slugify(name) || "—", [name]); + + const createMutation = useMutation({ + mutationFn: (input: CreateBrandInput) => createBrand(input), + onSuccess: () => { + toast.success("Brand created"); + queryClient.invalidateQueries({ queryKey: ["catalog", "brands"] }); + onClose(); + }, + onError: (err) => toast.error("Create failed", { description: describe(err) }), + }); + + const updateMutation = useMutation({ + mutationFn: (input: UpdateBrandInput) => updateBrand(input), + onSuccess: () => { + toast.success("Brand updated"); + queryClient.invalidateQueries({ queryKey: ["catalog", "brands"] }); + onClose(); + }, + onError: (err) => toast.error("Update failed", { description: describe(err) }), + }); + + const isPending = createMutation.isPending || updateMutation.isPending; + const trimmedName = name.trim(); + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + if (!trimmedName) return; + const payload = { + name: trimmedName, + description: description.trim() || null, + logoUrl: logoUrl.trim() || null, + }; + if (state.mode === "edit" && brand) { + updateMutation.mutate({ brandId: brand.id, ...payload }); + } else { + createMutation.mutate(payload); + } + }; + + return ( + (!o ? onClose() : undefined)}> + +
    + + + {brand ? "Edit entry" : "New entry"} + + {brand ? "Edit brand" : "Add a brand"} + + {brand + ? `Update details for ${brand.name}. The slug is re-derived from the name.` + : "Add a brand to your catalog. The slug is generated automatically from the name."} + + + + + + + + setName(e.target.value)} + placeholder="Acme Goods" + autoFocus + required + maxLength={128} + /> + + + +
    + + → + + + {slugPreview} + + + ready + +
    +
    + + +
    + ), +); +TableHead.displayName = "TableHead"; + +export const TableCell = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +); +TableCell.displayName = "TableCell"; diff --git a/clients/admin/src/env.ts b/clients/admin/src/env.ts new file mode 100644 index 0000000000..d133f08038 --- /dev/null +++ b/clients/admin/src/env.ts @@ -0,0 +1,8 @@ +// Dev builds proxy `/api` → VITE_API_BASE_URL via vite.config.ts, so the app can use relative URLs. +// Production builds should set VITE_API_BASE_URL to the fully-qualified API origin. +const apiBase = (import.meta.env.VITE_API_BASE_URL ?? "").replace(/\/$/, ""); + +export const env = { + apiBase, + defaultTenant: import.meta.env.VITE_DEFAULT_TENANT ?? "root", +}; diff --git a/clients/admin/src/lib/api-client.ts b/clients/admin/src/lib/api-client.ts new file mode 100644 index 0000000000..c85d65e026 --- /dev/null +++ b/clients/admin/src/lib/api-client.ts @@ -0,0 +1,125 @@ +import { env } from "@/env"; +import { tokenStore } from "@/auth/token-store"; + +export type ApiError = { + status: number; + title?: string; + detail?: string; + errors?: Record; +}; + +export class ApiRequestError extends Error { + readonly status: number; + readonly problem?: ApiError; + + constructor(status: number, message: string, problem?: ApiError) { + super(message); + this.status = status; + this.problem = problem; + } +} + +type RequestInitEx = RequestInit & { skipAuth?: boolean }; + +let refreshPromise: Promise | null = null; + +async function refreshAccessToken() { + const refreshToken = tokenStore.getRefreshToken(); + if (!refreshToken) { + throw new ApiRequestError(401, "No refresh token"); + } + + const response = await fetch(`${env.apiBase}/api/v1/identity/token/refresh`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ refreshToken }), + }); + + if (!response.ok) { + tokenStore.clear(); + throw new ApiRequestError(response.status, "Refresh failed"); + } + + const tokens = (await response.json()) as { + accessToken: string; + refreshToken: string; + }; + tokenStore.setTokens(tokens.accessToken, tokens.refreshToken); +} + +async function parseError(response: Response): Promise { + const contentType = response.headers.get("content-type") ?? ""; + if (!contentType.includes("json")) { + return undefined; + } + try { + return (await response.json()) as ApiError; + } catch { + return undefined; + } +} + +export async function apiFetch( + path: string, + init: RequestInitEx = {}, +): Promise { + const { skipAuth, headers, ...rest } = init; + + const mergedHeaders = new Headers(headers); + if (!mergedHeaders.has("Content-Type") && rest.body && typeof rest.body === "string") { + mergedHeaders.set("Content-Type", "application/json"); + } + + if (!skipAuth) { + const accessToken = tokenStore.getAccessToken(); + if (accessToken) { + mergedHeaders.set("Authorization", `Bearer ${accessToken}`); + } + } + + const tenant = tokenStore.getTenant() ?? env.defaultTenant; + if (tenant && !mergedHeaders.has("tenant")) { + mergedHeaders.set("tenant", tenant); + } + + const url = path.startsWith("http") ? path : `${env.apiBase}${path}`; + let response = await fetch(url, { ...rest, headers: mergedHeaders }); + + if (response.status === 401 && !skipAuth && tokenStore.getRefreshToken()) { + refreshPromise ??= refreshAccessToken().finally(() => { + refreshPromise = null; + }); + + try { + await refreshPromise; + } catch (e) { + throw e instanceof ApiRequestError + ? e + : new ApiRequestError(401, "Session expired"); + } + + const retryHeaders = new Headers(mergedHeaders); + retryHeaders.set("Authorization", `Bearer ${tokenStore.getAccessToken() ?? ""}`); + response = await fetch(url, { ...rest, headers: retryHeaders }); + } + + if (!response.ok) { + const problem = await parseError(response); + throw new ApiRequestError( + response.status, + problem?.title ?? problem?.detail ?? response.statusText, + problem, + ); + } + + if (response.status === 204) { + return undefined as T; + } + + const contentType = response.headers.get("content-type") ?? ""; + if (!contentType.includes("json")) { + return undefined as T; + } + + return (await response.json()) as T; +} diff --git a/clients/admin/src/lib/api-types.ts b/clients/admin/src/lib/api-types.ts new file mode 100644 index 0000000000..c6ad0b1048 --- /dev/null +++ b/clients/admin/src/lib/api-types.ts @@ -0,0 +1,9 @@ +export type PagedResponse = { + items: T[]; + pageNumber: number; + pageSize: number; + totalCount: number; + totalPages: number; + hasNext: boolean; + hasPrevious: boolean; +}; diff --git a/clients/admin/src/lib/cn.ts b/clients/admin/src/lib/cn.ts new file mode 100644 index 0000000000..a5ef193506 --- /dev/null +++ b/clients/admin/src/lib/cn.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/clients/admin/src/lib/query-client.ts b/clients/admin/src/lib/query-client.ts new file mode 100644 index 0000000000..3089edb4b4 --- /dev/null +++ b/clients/admin/src/lib/query-client.ts @@ -0,0 +1,17 @@ +import { QueryClient } from "@tanstack/react-query"; +import { ApiRequestError } from "@/lib/api-client"; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: (failureCount, error) => { + if (error instanceof ApiRequestError && (error.status === 401 || error.status === 403)) { + return false; + } + return failureCount < 2; + }, + staleTime: 30_000, + refetchOnWindowFocus: false, + }, + }, +}); diff --git a/clients/admin/src/main.tsx b/clients/admin/src/main.tsx new file mode 100644 index 0000000000..95ccfed62e --- /dev/null +++ b/clients/admin/src/main.tsx @@ -0,0 +1,15 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "@/App"; +import "@/styles/globals.css"; + +const rootElement = document.getElementById("root"); +if (!rootElement) { + throw new Error("Root element '#root' not found"); +} + +createRoot(rootElement).render( + + + , +); diff --git a/clients/admin/src/pages/dashboard.tsx b/clients/admin/src/pages/dashboard.tsx new file mode 100644 index 0000000000..401ffaaaa3 --- /dev/null +++ b/clients/admin/src/pages/dashboard.tsx @@ -0,0 +1,45 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { useAuth } from "@/auth/use-auth"; + +export function DashboardPage() { + const { user } = useAuth(); + return ( +
    +
    +

    Dashboard

    +

    + Welcome{user?.name ? `, ${user.name}` : ""}. +

    +
    +
    + + + Tenants + Manage workspace tenants + + + Head to the Tenants page to view and manage tenant accounts. + + + + + Billing + Plans and subscriptions + + + Coming soon. + + + + + Quota + Usage limits per tenant + + + Coming soon. + + +
    +
    + ); +} diff --git a/clients/admin/src/pages/login.tsx b/clients/admin/src/pages/login.tsx new file mode 100644 index 0000000000..fe352c4be5 --- /dev/null +++ b/clients/admin/src/pages/login.tsx @@ -0,0 +1,252 @@ +import { useState, type FormEvent } from "react"; +import { Navigate, useLocation, useNavigate } from "react-router-dom"; +import { ClipboardCheck, Copy, FlaskConical, ShieldAlert } from "lucide-react"; +import { useAuth } from "@/auth/use-auth"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { ApiRequestError } from "@/lib/api-client"; +import { cn } from "@/lib/cn"; +import { env } from "@/env"; + +type LocationState = { from?: { pathname: string } }; + +const DEMO_PASSWORD = "Password123!"; +const DEMO_SUPERADMIN = { + tenant: "root", + email: "superadmin@root.com", + label: "SuperAdmin", + persona: "Platform operator · cross-tenant control", +} as const; + +export function LoginPage() { + const { isAuthenticated, login } = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + const from = (location.state as LocationState | null)?.from?.pathname ?? "/"; + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [tenant, setTenant] = useState(env.defaultTenant); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(false); + + if (isAuthenticated) { + return ; + } + + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(null); + setSubmitting(true); + try { + await login({ email, password, tenant }); + navigate(from, { replace: true }); + } catch (err) { + const message = + err instanceof ApiRequestError + ? err.problem?.detail ?? err.problem?.title ?? err.message + : err instanceof Error + ? err.message + : "Login failed"; + setError(message); + } finally { + setSubmitting(false); + } + }; + + const onPickDemo = () => { + setError(null); + setEmail(DEMO_SUPERADMIN.email); + setPassword(DEMO_PASSWORD); + setTenant(DEMO_SUPERADMIN.tenant); + }; + + const onCopyPassword = async () => { + try { + await navigator.clipboard.writeText(DEMO_PASSWORD); + setCopied(true); + window.setTimeout(() => setCopied(false), 1400); + } catch { + // ignore + } + }; + + return ( +
    +
    + + + Sign in + FullStackHero Admin · platform console + +
    + +
    + + setTenant(e.target.value)} + required + autoComplete="organization" + /> +
    +
    + + setEmail(e.target.value)} + required + autoComplete="email" + /> +
    +
    + + setPassword(e.target.value)} + required + autoComplete="current-password" + /> +
    + {error && ( +
    + {error} +
    + )} +
    + + + +
    +
    + + {import.meta.env.DEV && ( + + )} +
    +
    + ); +} + +function DevDemoCallout({ + active, + copied, + onPick, + onCopy, +}: { + active: boolean; + copied: boolean; + onPick: () => void; + onCopy: () => void; +}) { + return ( +
    +
    + + + +
    +
    + + DEV · Demo account + +
    + + +
    + + + password + + {DEMO_PASSWORD} + + + +
    +
    +
    +
    + ); +} diff --git a/clients/admin/src/pages/not-found.tsx b/clients/admin/src/pages/not-found.tsx new file mode 100644 index 0000000000..f2ec882873 --- /dev/null +++ b/clients/admin/src/pages/not-found.tsx @@ -0,0 +1,14 @@ +import { Link } from "react-router-dom"; +import { Button } from "@/components/ui/button"; + +export function NotFoundPage() { + return ( +
    +
    404
    +

    The page you're looking for doesn't exist.

    + +
    + ); +} diff --git a/clients/admin/src/pages/tenants/create.tsx b/clients/admin/src/pages/tenants/create.tsx new file mode 100644 index 0000000000..4475995876 --- /dev/null +++ b/clients/admin/src/pages/tenants/create.tsx @@ -0,0 +1,151 @@ +import { useState, type FormEvent } from "react"; +import { useNavigate } from "react-router-dom"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { createTenant } from "@/api/tenants"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { ApiRequestError } from "@/lib/api-client"; + +const TENANT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/; + +export function CreateTenantPage() { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const [id, setId] = useState(""); + const [name, setName] = useState(""); + const [adminEmail, setAdminEmail] = useState(""); + const [issuer, setIssuer] = useState(""); + const [connectionString, setConnectionString] = useState(""); + + const mutation = useMutation({ + mutationFn: createTenant, + onSuccess: (result) => { + toast.success(`Tenant ${result.id} created`, { + description: "Provisioning runs in the background. Track progress on the detail page.", + }); + queryClient.invalidateQueries({ queryKey: ["tenants"] }); + navigate(`/tenants/${result.id}`); + }, + onError: (err) => { + const detail = + err instanceof ApiRequestError + ? err.problem?.detail ?? err.problem?.title ?? err.message + : err instanceof Error + ? err.message + : "Failed to create tenant"; + toast.error("Create failed", { description: detail }); + }, + }); + + const idInvalid = id.length > 0 && !TENANT_ID_PATTERN.test(id); + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + mutation.mutate({ + id: id.trim(), + name: name.trim(), + adminEmail: adminEmail.trim(), + issuer: issuer.trim(), + connectionString: connectionString.trim() ? connectionString.trim() : null, + }); + }; + + return ( +
    +
    +

    New tenant

    +

    + Provision a new tenant and its seed admin user. +

    +
    + + +
    + + Tenant details + + The tenant identifier is used as a subdomain-like slug and must be URL-safe. + + + + + + + + + + + + + +
    +
    +
    + ); +} + +type FieldProps = { + id: string; + label: string; + value: string; + onChange: (value: string) => void; + type?: string; + required?: boolean; + hint?: string; + error?: string; + placeholder?: string; + autoComplete?: string; +}; + +function Field({ id, label, value, onChange, type, required, hint, error, placeholder, autoComplete }: FieldProps) { + return ( +
    + + onChange(e.target.value)} + required={required} + placeholder={placeholder} + autoComplete={autoComplete} + aria-invalid={error ? true : undefined} + /> + {hint &&

    {hint}

    } + {error &&

    {error}

    } +
    + ); +} diff --git a/clients/admin/src/pages/tenants/detail.tsx b/clients/admin/src/pages/tenants/detail.tsx new file mode 100644 index 0000000000..49260f7ccd --- /dev/null +++ b/clients/admin/src/pages/tenants/detail.tsx @@ -0,0 +1,196 @@ +import { useNavigate, useParams } from "react-router-dom"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { ArrowLeft, RefreshCw } from "lucide-react"; +import { toast } from "sonner"; +import { + changeTenantActivation, + getTenantProvisioningStatus, + getTenantStatus, + retryTenantProvisioning, + type TenantProvisioningStep, +} from "@/api/tenants"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { ApiRequestError } from "@/lib/api-client"; + +export function TenantDetailPage() { + const { id = "" } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const tenantQuery = useQuery({ + queryKey: ["tenant", id], + queryFn: () => getTenantStatus(id), + enabled: !!id, + }); + + const provisioningQuery = useQuery({ + queryKey: ["tenant", id, "provisioning"], + queryFn: () => getTenantProvisioningStatus(id), + enabled: !!id, + // Poll while provisioning is still in flight; back off once terminal. + refetchInterval: (query) => { + const status = query.state.data?.status; + if (status === "Completed" || status === "Failed") return false; + return 2000; + }, + }); + + const activationMutation = useMutation({ + mutationFn: (isActive: boolean) => changeTenantActivation(id, isActive), + onSuccess: (result) => { + toast.success(result.isActive ? "Tenant activated" : "Tenant deactivated"); + queryClient.invalidateQueries({ queryKey: ["tenant", id] }); + queryClient.invalidateQueries({ queryKey: ["tenants"] }); + }, + onError: (err) => toast.error("Activation change failed", { description: describe(err) }), + }); + + const retryMutation = useMutation({ + mutationFn: () => retryTenantProvisioning(id), + onSuccess: () => { + toast.success("Provisioning re-queued"); + queryClient.invalidateQueries({ queryKey: ["tenant", id, "provisioning"] }); + }, + onError: (err) => toast.error("Retry failed", { description: describe(err) }), + }); + + const tenant = tenantQuery.data; + const provisioning = provisioningQuery.data; + + return ( +
    +
    +
    + +

    + {tenant?.name ?? id} +

    +

    {id}

    +
    + {tenant && ( + + )} +
    + +
    + + + Details + Identity and lifecycle metadata. + + + {tenantQuery.isLoading && Loading…} + {tenantQuery.isError && ( + {describe(tenantQuery.error)} + )} + {tenant && ( + <> + + + + + + )} + + + + + +
    + Provisioning + + {provisioning ? ( + + ) : ( + "Loading provisioning status…" + )} + +
    + {provisioning?.status === "Failed" && ( + + )} +
    + + {provisioningQuery.isError && ( + {describe(provisioningQuery.error)} + )} + {provisioning?.steps.map((step) => )} + {provisioning?.error && ( +
    +                {provisioning.error}
    +              
    + )} +
    +
    +
    +
    + ); +} + +function Detail({ label, value }: { label: string; value: string }) { + return ( +
    + {label} + {value} +
    + ); +} + +function StepRow({ step }: { step: TenantProvisioningStep }) { + return ( +
    + {step.step} + +
    + ); +} + +function StatusBadge({ status }: { status: string }) { + const colors = + status === "Completed" + ? "bg-emerald-500/15 text-emerald-500" + : status === "Failed" + ? "bg-red-500/15 text-red-500" + : status === "Running" + ? "bg-blue-500/15 text-blue-500" + : "bg-[var(--color-muted)] text-[var(--color-muted-foreground)]"; + return {status}; +} + +function ProvisioningSummary({ provisioning }: { provisioning: { status: string; currentStep?: string | null } }) { + if (provisioning.status === "Completed") return Completed.; + if (provisioning.status === "Failed") return Failed at {provisioning.currentStep ?? "unknown step"}.; + return {provisioning.status}{provisioning.currentStep ? ` — ${provisioning.currentStep}` : ""}…; +} + +function formatDate(value: string | undefined): string { + if (!value) return "—"; + const d = new Date(value); + return Number.isNaN(d.getTime()) ? value : d.toLocaleString(); +} + +function describe(err: unknown): string { + if (err instanceof ApiRequestError) return err.problem?.detail ?? err.problem?.title ?? err.message; + if (err instanceof Error) return err.message; + return String(err); +} diff --git a/clients/admin/src/pages/tenants/list.tsx b/clients/admin/src/pages/tenants/list.tsx new file mode 100644 index 0000000000..46d167ccdd --- /dev/null +++ b/clients/admin/src/pages/tenants/list.tsx @@ -0,0 +1,154 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useQuery, keepPreviousData } from "@tanstack/react-query"; +import { ChevronLeft, ChevronRight, Plus } from "lucide-react"; +import { listTenants, type TenantDto } from "@/api/tenants"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { ApiRequestError } from "@/lib/api-client"; + +const PAGE_SIZE = 10; + +function formatDate(value: string): string { + const date = new Date(value); + return Number.isNaN(date.getTime()) ? value : date.toLocaleDateString(); +} + +function StatusBadge({ active }: { active: boolean }) { + return ( + + {active ? "Active" : "Inactive"} + + ); +} + +export function TenantsListPage() { + const [pageNumber, setPageNumber] = useState(1); + const navigate = useNavigate(); + + const query = useQuery({ + queryKey: ["tenants", { pageNumber, pageSize: PAGE_SIZE }], + queryFn: () => listTenants({ pageNumber, pageSize: PAGE_SIZE }), + placeholderData: keepPreviousData, + }); + + const data = query.data; + const items: TenantDto[] = data?.items ?? []; + + return ( +
    +
    +
    +

    Tenants

    +

    + Manage tenants registered on this instance. +

    +
    + +
    + + + + All tenants + + {data ? `${data.totalCount} total` : "Loading…"} + + + + {query.isError && ( +
    + {query.error instanceof ApiRequestError + ? query.error.problem?.detail ?? query.error.message + : "Failed to load tenants."} +
    + )} + + + + Name + Identifier + Admin email + Status + Valid until + + + + {query.isLoading && items.length === 0 ? ( + + + Loading… + + + ) : items.length === 0 ? ( + + + No tenants found. + + + ) : ( + items.map((tenant) => ( + navigate(`/tenants/${tenant.id}`)} + > + {tenant.name} + + {tenant.id} + + {tenant.adminEmail} + + + + {formatDate(tenant.validUpto)} + + )) + )} + +
    +
    +
    + +
    +
    + {data ? `Page ${data.pageNumber} of ${Math.max(data.totalPages, 1)}` : ""} +
    +
    + + +
    +
    +
    + ); +} diff --git a/clients/admin/src/pages/users/create.tsx b/clients/admin/src/pages/users/create.tsx new file mode 100644 index 0000000000..fd772bca1d --- /dev/null +++ b/clients/admin/src/pages/users/create.tsx @@ -0,0 +1,271 @@ +import { useState, type FormEvent } from "react"; +import { useNavigate } from "react-router-dom"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { ArrowLeft } from "lucide-react"; +import { toast } from "sonner"; +import { registerUser, type RegisterUserInput } from "@/api/users"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { SectionRule } from "@/components/section-rule"; +import { ApiRequestError } from "@/lib/api-client"; +import { cn } from "@/lib/cn"; + +const USERNAME_RE = /^[a-zA-Z][a-zA-Z0-9._-]{2,31}$/; + +export function CreateUserPage() { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const [form, setForm] = useState({ + firstName: "", + lastName: "", + email: "", + userName: "", + password: "", + confirmPassword: "", + phoneNumber: "", + }); + + const usernameInvalid = form.userName.length > 0 && !USERNAME_RE.test(form.userName); + const passwordsMismatch = + form.confirmPassword.length > 0 && form.password !== form.confirmPassword; + + const mutation = useMutation({ + mutationFn: (input: RegisterUserInput) => registerUser(input), + onSuccess: (result) => { + toast.success("User created", { + description: result.message ?? "Confirmation email queued.", + }); + queryClient.invalidateQueries({ queryKey: ["users"] }); + if (result.userId) { + navigate(`/users/${result.userId}`); + } else { + navigate("/users"); + } + }, + onError: (err) => { + const detail = + err instanceof ApiRequestError + ? err.problem?.detail ?? err.problem?.title ?? err.message + : err instanceof Error + ? err.message + : "Failed to create user"; + toast.error("Create failed", { description: detail }); + }, + }); + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + if (usernameInvalid || passwordsMismatch) return; + mutation.mutate({ + firstName: form.firstName.trim(), + lastName: form.lastName.trim(), + email: form.email.trim(), + userName: form.userName.trim(), + password: form.password, + confirmPassword: form.confirmPassword, + phoneNumber: form.phoneNumber?.trim() || undefined, + }); + }; + + const set = (k: K) => (v: RegisterUserInput[K]) => + setForm((f) => ({ ...f, [k]: v })); + + return ( +
    + + +
    + +

    + New account +

    +

    + The new user is created in the current tenant and emailed a confirmation link. + Roles can be assigned from the detail page after creation. +

    +
    + +
    + + +
    +
    + + +
    + + + + + + +
    + + + +
    + + +
    + +
    +
    + + +
    +
    +
    +
    + ); +} + +type FieldProps = { + id: string; + label: string; + value: string; + onChange: (value: string) => void; + type?: string; + required?: boolean; + hint?: string; + error?: string; + placeholder?: string; + autoComplete?: string; + mono?: boolean; +}; + +function Field({ id, label, value, onChange, type, required, hint, error, placeholder, autoComplete, mono }: FieldProps) { + return ( +
    + + onChange(e.target.value)} + required={required} + placeholder={placeholder} + autoComplete={autoComplete} + aria-invalid={error ? true : undefined} + className={cn(mono && "font-mono")} + /> + {hint &&

    {hint}

    } + {error &&

    {error}

    } +
    + ); +} diff --git a/clients/admin/src/pages/users/detail.tsx b/clients/admin/src/pages/users/detail.tsx new file mode 100644 index 0000000000..e62a9f386b --- /dev/null +++ b/clients/admin/src/pages/users/detail.tsx @@ -0,0 +1,369 @@ +import { useEffect, useMemo, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { ArrowLeft, Check, Mail, ShieldCheck } from "lucide-react"; +import { toast } from "sonner"; +import { + assignUserRoles, + getUser, + getUserRoles, + toggleUserStatus, + type UserDto, + type UserRoleDto, +} from "@/api/users"; +import { Button } from "@/components/ui/button"; +import { Monogram } from "@/components/monogram"; +import { SectionRule } from "@/components/section-rule"; +import { ApiRequestError } from "@/lib/api-client"; +import { cn } from "@/lib/cn"; + +export function UserDetailPage() { + const { id = "" } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const userQuery = useQuery({ + queryKey: ["user", id], + queryFn: () => getUser(id), + enabled: !!id, + }); + + const rolesQuery = useQuery({ + queryKey: ["user", id, "roles"], + queryFn: () => getUserRoles(id), + enabled: !!id, + }); + + const toggleMutation = useMutation({ + mutationFn: (activate: boolean) => toggleUserStatus(id, activate), + onSuccess: (_, activate) => { + toast.success(activate ? "User activated" : "User deactivated"); + queryClient.invalidateQueries({ queryKey: ["user", id] }); + queryClient.invalidateQueries({ queryKey: ["users"] }); + }, + onError: (err) => toast.error("Status change failed", { description: describe(err) }), + }); + + const user = userQuery.data; + const roles = rolesQuery.data; + + return ( +
    + + +
    + + + {userQuery.isError && ( + + )} + + {user ? ( +
    +
    + +
    +

    + {[user.firstName, user.lastName].filter(Boolean).join(" ").trim() || + user.userName || + user.email || + "Unnamed"} +

    +
    + {user.userName && @{user.userName}} + {user.email && {user.email}} +
    +
    + }> + {user.isActive ? "Active" : "Disabled"} + + }> + {user.emailConfirmed ? "Email confirmed" : "Email pending"} + +
    +
    +
    + + +
    + ) : userQuery.isLoading ? ( +
    + Loading account… +
    + ) : null} +
    + + {/* Dossier */} + {user && ( +
    + + { + queryClient.invalidateQueries({ queryKey: ["user", id, "roles"] }); + queryClient.invalidateQueries({ queryKey: ["users"] }); + }} + /> +
    + )} +
    + ); +} + +function IdentitySpine({ user }: { user: UserDto }) { + return ( +
    +
    +

    + \\ Identity +

    +
    +
    + + {user.id ?? "—"} + + + {user.userName ?? "—"} + + + {user.email ?? "—"} + + + {user.phoneNumber ?? "—"} + + {user.isActive ? "Active" : "Disabled"} + {user.emailConfirmed ? "Confirmed" : "Pending"} +
    +
    + ); +} + +function Row({ label, children, mono }: { label: string; children: React.ReactNode; mono?: boolean }) { + return ( +
    +
    + {label} +
    +
    {children}
    +
    + ); +} + +function RolesEditor({ + userId, + roles, + loading, + error, + onSaved, +}: { + userId: string; + roles: UserRoleDto[]; + loading: boolean; + error: unknown; + onSaved: () => void; +}) { + const queryClient = useQueryClient(); + const [draft, setDraft] = useState>({}); + + // Sync draft → server state whenever the source-of-truth roles change. + useEffect(() => { + setDraft(Object.fromEntries(roles.map((r) => [r.roleId, r.enabled]))); + }, [roles]); + + const original = useMemo( + () => Object.fromEntries(roles.map((r) => [r.roleId, r.enabled])), + [roles], + ); + const dirtyCount = useMemo( + () => + Object.keys(draft).reduce( + (acc, k) => acc + (Boolean(draft[k]) === Boolean(original[k]) ? 0 : 1), + 0, + ), + [draft, original], + ); + + const mutation = useMutation({ + mutationFn: (next: UserRoleDto[]) => assignUserRoles(userId, next), + onSuccess: () => { + toast.success("Roles updated"); + queryClient.invalidateQueries({ queryKey: ["user", userId, "roles"] }); + onSaved(); + }, + onError: (err) => toast.error("Role update failed", { description: describe(err) }), + }); + + const onSave = () => { + const next = roles.map((r) => ({ ...r, enabled: !!draft[r.roleId] })); + mutation.mutate(next); + }; + + const onDiscard = () => setDraft(original); + + return ( +
    +
    +

    + \\ Roles +

    + {!loading && roles.length > 0 && ( +

    + {dirtyCount === 0 + ? "No pending changes" + : `${dirtyCount} ${dirtyCount === 1 ? "change" : "changes"} pending`} +

    + )} +
    + + {error ? ( +

    {describe(error)}

    + ) : loading ? ( +

    + Loading… +

    + ) : roles.length === 0 ? ( +

    + No roles defined for this tenant. +

    + ) : ( + <> +

    + Click any role to toggle. Changes are batched — review and save when ready. +

    + +
    + {roles.map((r) => ( + + setDraft((d) => ({ ...d, [r.roleId]: !d[r.roleId] })) + } + /> + ))} +
    + +
    + + +
    + + )} +
    + ); +} + +function RoleChip({ + role, + enabled, + changed, + onToggle, +}: { + role: UserRoleDto; + enabled: boolean; + changed: boolean; + onToggle: () => void; +}) { + return ( + + ); +} + +function Pill({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) { + return ( + + {icon} + {children} + + ); +} + +function Dot({ active }: { active: boolean }) { + return ( + + ); +} + +function ErrorPanel({ error }: { error: unknown }) { + return ( +
    + {describe(error)} +
    + ); +} + +function shortId(id: string): string { + if (id.length <= 12) return id; + return `${id.slice(0, 4)}…${id.slice(-4)}`; +} + +function describe(err: unknown): string { + if (err instanceof ApiRequestError) return err.problem?.detail ?? err.problem?.title ?? err.message; + if (err instanceof Error) return err.message; + return String(err); +} diff --git a/clients/admin/src/pages/users/list.tsx b/clients/admin/src/pages/users/list.tsx new file mode 100644 index 0000000000..ab105281eb --- /dev/null +++ b/clients/admin/src/pages/users/list.tsx @@ -0,0 +1,329 @@ +import { useEffect, useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useQuery, keepPreviousData } from "@tanstack/react-query"; +import { ChevronLeft, ChevronRight, Plus, Search } from "lucide-react"; +import { searchUsers, type UserDto } from "@/api/users"; +import { listRoles } from "@/api/roles"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Monogram } from "@/components/monogram"; +import { SectionRule } from "@/components/section-rule"; +import { ApiRequestError } from "@/lib/api-client"; +import { cn } from "@/lib/cn"; + +const PAGE_SIZE = 12; + +type Tri = "any" | "yes" | "no"; + +function triToBool(v: Tri): boolean | undefined { + if (v === "yes") return true; + if (v === "no") return false; + return undefined; +} + +export function UsersListPage() { + const navigate = useNavigate(); + + const [pageNumber, setPageNumber] = useState(1); + const [searchInput, setSearchInput] = useState(""); + const [searchTerm, setSearchTerm] = useState(""); + const [activeFilter, setActiveFilter] = useState("any"); + const [confirmedFilter, setConfirmedFilter] = useState("any"); + const [roleId, setRoleId] = useState(""); + + // Debounce the search input → searchTerm + useEffect(() => { + const t = setTimeout(() => { + setSearchTerm(searchInput); + setPageNumber(1); + }, 250); + return () => clearTimeout(t); + }, [searchInput]); + + // Reset page when filters change + useEffect(() => { + setPageNumber(1); + }, [activeFilter, confirmedFilter, roleId]); + + const rolesQuery = useQuery({ + queryKey: ["roles"], + queryFn: listRoles, + staleTime: 5 * 60_000, + }); + + const usersQuery = useQuery({ + queryKey: [ + "users", + { pageNumber, pageSize: PAGE_SIZE, searchTerm, activeFilter, confirmedFilter, roleId }, + ], + queryFn: () => + searchUsers({ + pageNumber, + pageSize: PAGE_SIZE, + search: searchTerm || undefined, + isActive: triToBool(activeFilter), + emailConfirmed: triToBool(confirmedFilter), + roleId: roleId || undefined, + }), + placeholderData: keepPreviousData, + }); + + const data = usersQuery.data; + const items: UserDto[] = data?.items ?? []; + const baseIndex = ((data?.pageNumber ?? 1) - 1) * (data?.pageSize ?? PAGE_SIZE); + + const pageBadge = useMemo(() => { + if (!data) return "—"; + const p = String(data.pageNumber).padStart(2, "0"); + const t = String(Math.max(data.totalPages, 1)).padStart(2, "0"); + return `Page ${p} of ${t}`; + }, [data]); + + return ( +
    + + +
    +
    +

    + Directory +

    +

    + {data + ? `${data.totalCount} ${data.totalCount === 1 ? "account" : "accounts"} on this tenant.` + : "Loading the roster…"} +

    +
    + +
    + + {/* Filter row */} +
    +
    + + setSearchInput(e.target.value)} + placeholder="Search name, username, email…" + className="pl-9 font-mono text-xs placeholder:font-mono placeholder:text-xs" + /> +
    + + + + + +
    + + {/* Roster */} +
    + {usersQuery.isError && ( +
    + {usersQuery.error instanceof ApiRequestError + ? usersQuery.error.problem?.detail ?? usersQuery.error.message + : "Failed to load users."} +
    + )} + + {usersQuery.isLoading && items.length === 0 && ( +
    + Loading… +
    + )} + + {!usersQuery.isLoading && items.length === 0 && ( +
    +

    No matches.

    +

    + Adjust filters or invite a new user. +

    +
    + )} + +
      + {items.map((user, i) => ( + user.id && navigate(`/users/${user.id}`)} + /> + ))} +
    +
    + + {/* Pagination */} + {data && data.totalPages > 1 && ( +
    + + {pageBadge} + +
    + + +
    +
    + )} +
    + ); +} + +function UserRow({ + user, + index, + onClick, +}: { + user: UserDto; + index: number; + onClick: () => void; +}) { + const fullName = [user.firstName, user.lastName].filter(Boolean).join(" ").trim(); + const display = fullName || user.userName || user.email || "Unnamed"; + const num = String(index).padStart(3, "0"); + + return ( +
  • + +
  • + ); +} + +function StatusDot({ active }: { active: boolean }) { + return ( + + + {active ? "Active" : "Disabled"} + + ); +} + +type SegmentedProps = { + label: string; + value: T; + onChange: (next: T) => void; + options: ReadonlyArray<{ value: T; label: string }>; +}; + +function Segmented({ label, value, onChange, options }: SegmentedProps) { + return ( +
    + {label} +
    + {options.map((o) => { + const selected = o.value === value; + return ( + + ); + })} +
    +
    + ); +} diff --git a/clients/admin/src/routes.tsx b/clients/admin/src/routes.tsx new file mode 100644 index 0000000000..6dd315dfd4 --- /dev/null +++ b/clients/admin/src/routes.tsx @@ -0,0 +1,39 @@ +import { createBrowserRouter, Navigate } from "react-router-dom"; +import { AppShell } from "@/components/layout/app-shell"; +import { ProtectedRoute } from "@/auth/protected-route"; +import { RouteError } from "@/components/route-error"; +import { LoginPage } from "@/pages/login"; +import { DashboardPage } from "@/pages/dashboard"; +import { TenantsListPage } from "@/pages/tenants/list"; +import { CreateTenantPage } from "@/pages/tenants/create"; +import { TenantDetailPage } from "@/pages/tenants/detail"; +import { UsersListPage } from "@/pages/users/list"; +import { CreateUserPage } from "@/pages/users/create"; +import { UserDetailPage } from "@/pages/users/detail"; +import { NotFoundPage } from "@/pages/not-found"; + +export const router = createBrowserRouter([ + { path: "/login", element: , errorElement: }, + { + element: , + errorElement: , + children: [ + { + element: , + errorElement: , + children: [ + { index: true, element: }, + { path: "tenants", element: }, + { path: "tenants/new", element: }, + { path: "tenants/:id", element: }, + { path: "users", element: }, + { path: "users/new", element: }, + { path: "users/:id", element: }, + { path: "billing", element: }, + { path: "quota", element: }, + ], + }, + ], + }, + { path: "*", element: }, +]); diff --git a/clients/admin/src/styles/globals.css b/clients/admin/src/styles/globals.css new file mode 100644 index 0000000000..84baa44a19 --- /dev/null +++ b/clients/admin/src/styles/globals.css @@ -0,0 +1,158 @@ +@import "tailwindcss"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --radius: 0.625rem; +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.985 0 0); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.556 0 0); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --font-display: "Fraunces", Georgia, "Times New Roman", serif; + --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} + +@layer base { + * { + border-color: var(--color-border); + } + + html, body { + background-color: var(--color-background); + color: var(--color-foreground); + } + + /* Editorial display utility — Fraunces with subtle optical sizing baked in. */ + .font-display { + font-family: var(--font-display); + font-optical-sizing: auto; + font-variation-settings: "opsz" 96, "SOFT" 50; + letter-spacing: -0.018em; + } + .font-mono { + font-family: var(--font-mono); + } +} + +/* ------------------------------------------------------------------------- */ +/* Editorial section rule used across the admin module surfaces. */ +/* "\\ USERS" hairline header lives at the top of every page in a section. */ +/* ------------------------------------------------------------------------- */ +.section-rule { + position: relative; + display: flex; + align-items: baseline; + gap: 0.625rem; + padding-top: 0.625rem; + border-top: 1px solid var(--color-foreground); +} +.section-rule__crumb { + font-family: var(--font-mono); + font-size: 0.6875rem; + font-weight: 500; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--color-foreground); +} +.section-rule__crumb--muted { + color: var(--color-muted-foreground); + font-weight: 400; +} + +/* ------------------------------------------------------------------------- */ +/* Deterministic monogram tones — chroma=0 to honour the existing palette. */ +/* ------------------------------------------------------------------------- */ +.mono-tone-0 { background: oklch(0.94 0 0); color: oklch(0.20 0 0); } +.mono-tone-1 { background: oklch(0.86 0 0); color: oklch(0.18 0 0); } +.mono-tone-2 { background: oklch(0.32 0 0); color: oklch(0.96 0 0); } +.mono-tone-3 { background: oklch(0.18 0 0); color: oklch(0.95 0 0); } + +.dark .mono-tone-0 { background: oklch(0.32 0 0); color: oklch(0.93 0 0); } +.dark .mono-tone-1 { background: oklch(0.42 0 0); color: oklch(0.97 0 0); } +.dark .mono-tone-2 { background: oklch(0.86 0 0); color: oklch(0.18 0 0); } +.dark .mono-tone-3 { background: oklch(0.96 0 0); color: oklch(0.18 0 0); } + +.mono-grid { + position: relative; + isolation: isolate; +} +.mono-grid::after { + content: ""; + position: absolute; + inset: 0; + background-image: + linear-gradient(to right, rgb(0 0 0 / 0.06) 1px, transparent 1px), + linear-gradient(to bottom, rgb(0 0 0 / 0.06) 1px, transparent 1px); + background-size: 4px 4px; + mix-blend-mode: multiply; + pointer-events: none; +} +.dark .mono-grid::after { + background-image: + linear-gradient(to right, rgb(255 255 255 / 0.05) 1px, transparent 1px), + linear-gradient(to bottom, rgb(255 255 255 / 0.05) 1px, transparent 1px); + mix-blend-mode: screen; +} diff --git a/clients/admin/tsconfig.app.json b/clients/admin/tsconfig.app.json new file mode 100644 index 0000000000..5265631a82 --- /dev/null +++ b/clients/admin/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "erasableSyntaxOnly": true, + "noEmit": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/clients/admin/tsconfig.json b/clients/admin/tsconfig.json new file mode 100644 index 0000000000..1ffef600d9 --- /dev/null +++ b/clients/admin/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/clients/admin/tsconfig.node.json b/clients/admin/tsconfig.node.json new file mode 100644 index 0000000000..111470864a --- /dev/null +++ b/clients/admin/tsconfig.node.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["vite.config.ts"] +} diff --git a/clients/admin/vite.config.ts b/clients/admin/vite.config.ts new file mode 100644 index 0000000000..4e6b131901 --- /dev/null +++ b/clients/admin/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig, loadEnv } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import path from "node:path"; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ""); + const apiBase = env.VITE_API_BASE_URL ?? "http://localhost:5030"; + + return { + plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + server: { + port: 5173, + strictPort: true, + proxy: { + "/api": { target: apiBase, changeOrigin: true, secure: false }, + "/openapi": { target: apiBase, changeOrigin: true, secure: false }, + "/scalar": { target: apiBase, changeOrigin: true, secure: false }, + }, + }, + }; +}); diff --git a/clients/dashboard/.gitignore b/clients/dashboard/.gitignore new file mode 100644 index 0000000000..2e582de6d8 --- /dev/null +++ b/clients/dashboard/.gitignore @@ -0,0 +1,22 @@ +node_modules +dist +dist-ssr +*.local +*.tsbuildinfo +.DS_Store + +# Env +.env +.env.local +.env.*.local + +# Editor +.vscode/* +!.vscode/extensions.json +.idea + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* diff --git a/clients/dashboard/README.md b/clients/dashboard/README.md new file mode 100644 index 0000000000..b836a88ef7 --- /dev/null +++ b/clients/dashboard/README.md @@ -0,0 +1,101 @@ +# FullStackHero — Dashboard + +Tenant-facing dashboard for the FullStackHero .NET Starter Kit. Shows realtime telemetry over Server-Sent Events, current-period usage vs. plan limits (Recharts), and billing history. + +Built with React 19, Vite 7, TypeScript, TanStack Query, React Router, Tailwind 4 + shadcn/ui, and Recharts. Standalone — not part of a pnpm workspace — so it plugs into .NET Aspire as a plain `ExecutableResource`. + +## Prerequisites + +- Node.js 20+ +- The API running (locally or remote) + +## Install & run + +Two options — pick whichever matches how you want to develop. + +### Option A — run everything through Aspire (recommended) + +The AppHost launches Postgres, Redis, MinIO, the API, the admin app, **and** this dashboard together, with `VITE_API_BASE_URL` wired via service discovery. + +```bash +npm install --prefix clients/dashboard # one-time +dotnet run --project src/Host/FSH.Starter.AppHost +``` + +Aspire dashboard exposes `fsh-dashboard` on . + +### Option B — run the frontend standalone + +Useful when the API is already running elsewhere. + +```bash +cd clients/dashboard +npm install +npm run dev # http://localhost:5174 +``` + +The dev server proxies `/api`, `/openapi`, and `/scalar` to `VITE_API_BASE_URL` (default `http://localhost:5030`). + +## Scripts + +| Script | Purpose | +|-------------------|--------------------------------------| +| `npm run dev` | Vite dev server on port 5174 | +| `npm run build` | `tsc -b` + `vite build` → `dist/` | +| `npm run preview` | Preview the production build | +| `npm run lint` | ESLint (flat config) | + +## Configuration + +| Variable | Default | Purpose | +|-----------------------|--------------------------|-----------------------------------------------| +| `VITE_API_BASE_URL` | `http://localhost:5030` | API origin used by the dev proxy | +| `VITE_DEFAULT_TENANT` | `root` | Default tenant header for unauthenticated calls | + +## Architecture + +``` +src/ +├── api/ # Typed API clients (billing, usage, subscription) +├── auth/ # JWT-backed auth (own localStorage prefix: fsh.dashboard.*) +├── components/ +│ ├── layout/ # Sidebar, Topbar, AppShell +│ ├── sse/ # SseStatusBadge, LiveFeed +│ └── ui/ # shadcn primitives +├── lib/ # api-client, query-client, cn +├── pages/ # Overview, Activity, Invoices, Login, NotFound +├── sse/ +│ ├── sse-api.ts # POST /api/v1/sse/token +│ └── sse-context.tsx # SSE connection manager (fetch-based streaming) +├── styles/globals.css # Tailwind 4 CSS-first + shadcn variables +├── App.tsx, main.tsx, routes.tsx +``` + +### Server-Sent Events + +EventSource can't send an `Authorization` header, so the flow is: + +1. `POST /api/v1/sse/token` (authenticated) — returns a short-lived opaque token. +2. `GET /api/v1/sse/stream?token=` (anonymous, token-gated) — holds a long-lived `text/event-stream` response. + +This app uses **fetch streaming** (not the native `EventSource` API) so it can: + +- Mint a fresh single-use token on every (re)connect without relying on the browser's auto-reconnect, which would replay the already-consumed token and get 401'd. +- Apply the tenant header to the stream request. +- Use an explicit exponential backoff (1s → 30s). + +The `SseProvider` in `src/sse/sse-context.tsx` is mounted inside `AppShell`, so the stream is active only when authenticated. A bounded ring buffer (200 events) is exposed via `useSse()` and consumed by `LiveFeed` and `SseStatusBadge`. + +### What the overview shows + +- **Usage this period** — current-month `UsageSnapshots` rendered as a bar chart of used vs. plan limit. Overage bars turn red. +- **Subscription** — plan key, status, and validity window from `GET /billing/subscriptions/me`. +- **Live activity** — rolling feed of SSE events as the backend publishes them. + +## Authentication flow + +Identical to the admin app: JWT in `localStorage`, `Authorization: Bearer` + `tenant` headers, single-flight refresh on 401 via `POST /api/v1/identity/token/refresh`. Keys are namespaced `fsh.dashboard.*` so both apps can run side-by-side without clobbering each other's session. + +## Production build + +`npm run build` emits `dist/`. Deploy behind any static host; forward `/api/*` to the backend and serve `index.html` as the SPA fallback. diff --git a/clients/dashboard/components.json b/clients/dashboard/components.json new file mode 100644 index 0000000000..408958d879 --- /dev/null +++ b/clients/dashboard/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/cn", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/clients/dashboard/eslint.config.js b/clients/dashboard/eslint.config.js new file mode 100644 index 0000000000..fdd39034f2 --- /dev/null +++ b/clients/dashboard/eslint.config.js @@ -0,0 +1,24 @@ +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { ignores: ['dist', 'node_modules', '*.config.js'] }, + { + extends: [...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + }, + }, +); diff --git a/clients/dashboard/index.html b/clients/dashboard/index.html new file mode 100644 index 0000000000..38afba3f62 --- /dev/null +++ b/clients/dashboard/index.html @@ -0,0 +1,85 @@ + + + + + + + FullStackHero — Dashboard + + + + + + + + + + +
    + + + diff --git a/clients/dashboard/package-lock.json b/clients/dashboard/package-lock.json new file mode 100644 index 0000000000..89629a7ab9 --- /dev/null +++ b/clients/dashboard/package-lock.json @@ -0,0 +1,5685 @@ +{ + "name": "@fullstackhero/dashboard", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@fullstackhero/dashboard", + "version": "0.1.0", + "dependencies": { + "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@tanstack/react-query": "^5.66.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "lucide-react": "^0.475.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", + "react-router-dom": "^7.1.5", + "recharts": "^2.15.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.0.1", + "zod": "^3.24.2" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.6", + "@types/lodash": "^4.17.24", + "@types/node": "^22.13.1", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.19.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.18", + "globals": "^15.14.0", + "tailwindcss": "^4.0.6", + "typescript": "^5.7.3", + "typescript-eslint": "^8.23.0", + "vite": "^7.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.99.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.99.2.tgz", + "integrity": "sha512-1HunU0bXVsR1ZJMZbcOPE6VtaBJxsW809RE9xPe4Gz7MlB0GWwQvuTPhMoEmQ/hIzFKJ/DWAuttIe7BOaWx0tA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.99.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.99.2.tgz", + "integrity": "sha512-vM91UEe45QUS9ED6OklsVL15i8qKcRqNwpWzPTVWvRPRSEgDudDgHpvyTjcdlwHcrKNa80T+xXYcchT2noPnZA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.99.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", + "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/type-utils": "8.58.2", + "@typescript-eslint/utils": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz", + "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", + "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.2", + "@typescript-eslint/types": "^8.58.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", + "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", + "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", + "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", + "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", + "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.2", + "@typescript-eslint/tsconfig-utils": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", + "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", + "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", + "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.340", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz", + "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.475.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.475.0.tgz", + "integrity": "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-hook-form": { + "version": "7.72.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.1.tgz", + "integrity": "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.1.tgz", + "integrity": "sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.1.tgz", + "integrity": "sha512-ZkrQuwwhGibjQLqH1eCdyiZyLWglPxzxdl5tgwgKEyCSGC76vmAjleGocRe3J/MLfzMUIKwaFJWpFVJhK3d2xA==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz", + "integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.58.2", + "@typescript-eslint/parser": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/clients/dashboard/package.json b/clients/dashboard/package.json new file mode 100644 index 0000000000..963df2886d --- /dev/null +++ b/clients/dashboard/package.json @@ -0,0 +1,52 @@ +{ + "name": "@fullstackhero/dashboard", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --port 5174", + "build": "tsc -b && vite build", + "preview": "vite preview --port 4174", + "lint": "eslint ." + }, + "dependencies": { + "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@tanstack/react-query": "^5.66.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "lucide-react": "^0.475.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", + "react-router-dom": "^7.1.5", + "recharts": "^2.15.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.0.1", + "zod": "^3.24.2" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.6", + "@types/lodash": "^4.17.24", + "@types/node": "^22.13.1", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.19.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.18", + "globals": "^15.14.0", + "tailwindcss": "^4.0.6", + "typescript": "^5.7.3", + "typescript-eslint": "^8.23.0", + "vite": "^7.0.0" + }, + "engines": { + "node": ">=20" + } +} diff --git a/clients/dashboard/public/favicon.svg b/clients/dashboard/public/favicon.svg new file mode 100644 index 0000000000..ce7b3b6425 --- /dev/null +++ b/clients/dashboard/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/clients/dashboard/src/App.tsx b/clients/dashboard/src/App.tsx new file mode 100644 index 0000000000..41ec97e2db --- /dev/null +++ b/clients/dashboard/src/App.tsx @@ -0,0 +1,55 @@ +import { RouterProvider } from "react-router-dom"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "sonner"; +import { queryClient } from "@/lib/query-client"; +import { AuthProvider } from "@/auth/auth-context"; +import { ThemeProvider } from "@/components/theme/theme-provider"; +import { CommandPaletteProvider } from "@/components/command-palette/command-palette"; +import { router } from "@/routes"; + +export function App() { + return ( + + + + + + {/* + FSH-native toaster — "tone rail" treatment. + + `richColors` is intentionally off; the colour story lives in a + 3px tone-coloured left rail (border-left, never repainted) and + a small lowercase mono pill that lines up inline with the + title (`ok` / `err` / `warn` / `info` / `note`). The default + sonner icon plate is hidden — type is announced typographically. + All chrome lives in globals.css under the `.fsh-toast` family; + the `closeButton` flag below makes sonner render the hairline X + we re-style there. Layout is flexbox so it survives sonner's + internal DOM nesting (`data-content` wrapper) without grid- + placement issues. + */} + + + + + + ); +} diff --git a/clients/dashboard/src/api/audits.ts b/clients/dashboard/src/api/audits.ts new file mode 100644 index 0000000000..1cd593a487 --- /dev/null +++ b/clients/dashboard/src/api/audits.ts @@ -0,0 +1,208 @@ +import { apiFetch } from "@/lib/api-client"; + +// ──────────────────────────────────────────────────────────────────────── +// Enum mirrors of the API contracts. Keep in lockstep with +// src/Modules/Auditing/Modules.Auditing.Contracts/AuditEnums.cs. +// ──────────────────────────────────────────────────────────────────────── + +export const AuditEventType = { + None: 0, + EntityChange: 1, + Security: 2, + Activity: 3, + Exception: 4, +} as const; +export type AuditEventType = (typeof AuditEventType)[keyof typeof AuditEventType]; + +export const AUDIT_EVENT_TYPE_LABELS: Record = { + 0: "Unknown", + 1: "Entity", + 2: "Security", + 3: "Activity", + 4: "Exception", +}; + +export const AuditSeverity = { + None: 0, + Trace: 1, + Debug: 2, + Information: 3, + Warning: 4, + Error: 5, + Critical: 6, +} as const; +export type AuditSeverity = (typeof AuditSeverity)[keyof typeof AuditSeverity]; + +export const AUDIT_SEVERITY_LABELS: Record = { + 0: "—", + 1: "Trace", + 2: "Debug", + 3: "Info", + 4: "Warn", + 5: "Error", + 6: "Critical", +}; + +/** Bitwise flags — keep in sync with AuditTag enum on the backend. */ +export const AuditTag = { + None: 0, + PiiMasked: 1 << 0, + OutOfQuota: 1 << 1, + Sampled: 1 << 2, + RetainedLong: 1 << 3, + HealthCheck: 1 << 4, + Authentication: 1 << 5, + Authorization: 1 << 6, +} as const; +export type AuditTag = (typeof AuditTag)[keyof typeof AuditTag]; + +export const AUDIT_TAG_LABELS: Array<{ flag: number; name: string }> = [ + { flag: AuditTag.PiiMasked, name: "PII masked" }, + { flag: AuditTag.OutOfQuota, name: "Quota exceeded" }, + { flag: AuditTag.Sampled, name: "Sampled" }, + { flag: AuditTag.RetainedLong, name: "Retained" }, + { flag: AuditTag.HealthCheck, name: "Health check" }, + { flag: AuditTag.Authentication, name: "Auth" }, + { flag: AuditTag.Authorization, name: "Authz" }, +]; + +export function decodeTags(mask: number): string[] { + return AUDIT_TAG_LABELS.filter((t) => (mask & t.flag) !== 0).map((t) => t.name); +} + +// ──────────────────────────────────────────────────────────────────────── +// DTOs +// ──────────────────────────────────────────────────────────────────────── + +export type AuditSummaryDto = { + id: string; + occurredAtUtc: string; + eventType: number; + severity: number; + tenantId?: string | null; + userId?: string | null; + userName?: string | null; + traceId?: string | null; + correlationId?: string | null; + requestId?: string | null; + source?: string | null; + tags: number; +}; + +export type AuditDetailDto = AuditSummaryDto & { + receivedAtUtc: string; + spanId?: string | null; + /** Server returns a JsonElement — comes through as already-parsed JSON. */ + payload: unknown; +}; + +export type AuditSummaryAggregateDto = { + /** keys are stringified AuditEventType integers */ + eventsByType: Record; + eventsBySeverity: Record; + eventsBySource: Record; + eventsByTenant: Record; +}; + +export type PagedResponse = { + items: T[]; + pageNumber: number; + pageSize: number; + totalCount: number; + totalPages: number; +}; + +// ──────────────────────────────────────────────────────────────────────── +// Query shape — matches GetAuditsQuery on the backend. +// ──────────────────────────────────────────────────────────────────────── + +export type ListAuditsQuery = { + pageNumber?: number; + pageSize?: number; + fromUtc?: string; + toUtc?: string; + tenantId?: string; + userId?: string; + eventType?: AuditEventType; + severity?: AuditSeverity; + /** Bitmask of AuditTag values. */ + tags?: number; + source?: string; + correlationId?: string; + traceId?: string; + search?: string; + sort?: string; +}; + +function toQueryString(query: Record): string { + const params = new URLSearchParams(); + for (const [key, value] of Object.entries(query)) { + if (value === undefined || value === null || value === "") continue; + // FluentValidation on the backend uses PascalCase parameter names by + // default for [AsParameters]; ASP.NET Core's binder is case-insensitive, + // so we send what's natural for JS callers. + params.append(toPascal(key), String(value)); + } + return params.toString(); +} + +function toPascal(s: string): string { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +// ──────────────────────────────────────────────────────────────────────── +// Fetchers +// ──────────────────────────────────────────────────────────────────────── + +export async function listAudits( + query: ListAuditsQuery, + signal?: AbortSignal, +): Promise> { + const qs = toQueryString(query); + return apiFetch>( + `/api/v1/audits${qs ? `?${qs}` : ""}`, + { signal }, + ); +} + +export async function getAuditById( + id: string, + signal?: AbortSignal, +): Promise { + return apiFetch(`/api/v1/audits/${id}`, { signal }); +} + +export async function getAuditSummary( + query: { fromUtc?: string; toUtc?: string; tenantId?: string }, + signal?: AbortSignal, +): Promise { + const qs = toQueryString(query); + return apiFetch( + `/api/v1/audits/summary${qs ? `?${qs}` : ""}`, + { signal }, + ); +} + +export async function getAuditsByCorrelation( + correlationId: string, + query: { fromUtc?: string; toUtc?: string } = {}, + signal?: AbortSignal, +): Promise { + const qs = toQueryString(query); + return apiFetch( + `/api/v1/audits/by-correlation/${encodeURIComponent(correlationId)}${qs ? `?${qs}` : ""}`, + { signal }, + ); +} + +export async function getAuditsByTrace( + traceId: string, + query: { fromUtc?: string; toUtc?: string } = {}, + signal?: AbortSignal, +): Promise { + const qs = toQueryString(query); + return apiFetch( + `/api/v1/audits/by-trace/${encodeURIComponent(traceId)}${qs ? `?${qs}` : ""}`, + { signal }, + ); +} diff --git a/clients/dashboard/src/api/billing.ts b/clients/dashboard/src/api/billing.ts new file mode 100644 index 0000000000..fc01b0ca29 --- /dev/null +++ b/clients/dashboard/src/api/billing.ts @@ -0,0 +1,68 @@ +import { apiFetch } from "@/lib/api-client"; + +export type QuotaResource = + | "ApiCalls" + | "StorageBytes" + | "Users" + | "WebhookDeliveries" + | (string & {}); + +export type UsageSnapshotDto = { + id: string; + tenantId: string; + periodYear: number; + periodMonth: number; + resource: QuotaResource; + usedUnits: number; + limitUnits: number; + overage: number; + capturedAtUtc: string; +}; + +export type InvoiceStatus = "Draft" | "Issued" | "Paid" | "Void" | (string & {}); + +export type InvoiceDto = { + id: string; + tenantId: string; + invoiceNumber: string; + periodYear: number; + periodMonth: number; + currency: string; + subtotalAmount: number; + status: InvoiceStatus; + createdAtUtc: string; + issuedAtUtc?: string | null; + dueAtUtc?: string | null; + paidAtUtc?: string | null; + voidedAtUtc?: string | null; + notes?: string | null; + lineItems: unknown[]; +}; + +export type SubscriptionStatus = "Active" | "Canceled" | "Expired" | (string & {}); + +export type SubscriptionDto = { + id: string; + tenantId: string; + planId: string; + planKey: string; + startUtc: string; + endUtc?: string | null; + status: SubscriptionStatus; +}; + +export function getUsageSnapshots(params?: { periodYear?: number; periodMonth?: number }) { + const query = new URLSearchParams(); + if (params?.periodYear) query.set("periodYear", String(params.periodYear)); + if (params?.periodMonth) query.set("periodMonth", String(params.periodMonth)); + const suffix = query.toString() ? `?${query.toString()}` : ""; + return apiFetch(`/api/v1/billing/usage${suffix}`); +} + +export function getMyInvoices() { + return apiFetch("/api/v1/billing/invoices/me"); +} + +export function getMySubscription() { + return apiFetch("/api/v1/billing/subscriptions/me"); +} diff --git a/clients/dashboard/src/api/catalog.ts b/clients/dashboard/src/api/catalog.ts new file mode 100644 index 0000000000..f4178f1d1c --- /dev/null +++ b/clients/dashboard/src/api/catalog.ts @@ -0,0 +1,404 @@ +import { apiFetch } from "@/lib/api-client"; + +export type PagedResponse = { + items: T[]; + pageNumber: number; + pageSize: number; + totalCount: number; + totalPages: number; + hasNext: boolean; + hasPrevious: boolean; +}; + +export type BrandDto = { + id: string; + name: string; + slug: string; + description?: string | null; + logoUrl?: string | null; + createdAtUtc: string; + updatedAtUtc?: string | null; + deletedOnUtc?: string | null; + deletedBy?: string | null; +}; + +export type SearchBrandsParams = { + search?: string; + pageNumber?: number; + pageSize?: number; + sortBy?: string; + sortDir?: "asc" | "desc"; +}; + +export type CreateBrandInput = { + name: string; + description?: string | null; + logoUrl?: string | null; +}; + +export type UpdateBrandInput = { + brandId: string; + name: string; + description?: string | null; + logoUrl?: string | null; +}; + +export function searchBrands(params: SearchBrandsParams = {}): Promise> { + const query = new URLSearchParams(); + if (params.search) query.set("search", params.search); + query.set("pageNumber", String(params.pageNumber ?? 1)); + query.set("pageSize", String(params.pageSize ?? 20)); + if (params.sortBy) query.set("sortBy", params.sortBy); + if (params.sortDir) query.set("sortDir", params.sortDir); + return apiFetch>(`/api/v1/catalog/brands?${query.toString()}`); +} + +export function getBrandById(id: string): Promise { + return apiFetch(`/api/v1/catalog/brands/${encodeURIComponent(id)}`); +} + +export async function createBrand(input: CreateBrandInput): Promise { + return apiFetch("/api/v1/catalog/brands", { + method: "POST", + body: JSON.stringify({ + name: input.name, + description: input.description ?? null, + logoUrl: input.logoUrl ?? null, + }), + }); +} + +export async function updateBrand(input: UpdateBrandInput): Promise { + return apiFetch(`/api/v1/catalog/brands/${encodeURIComponent(input.brandId)}`, { + method: "PUT", + body: JSON.stringify({ + brandId: input.brandId, + name: input.name, + description: input.description ?? null, + logoUrl: input.logoUrl ?? null, + }), + }); +} + +export async function deleteBrand(id: string): Promise { + await apiFetch(`/api/v1/catalog/brands/${encodeURIComponent(id)}`, { + method: "DELETE", + }); +} + +// ─── Categories ──────────────────────────────────────────────────────── + +export type CategoryDto = { + id: string; + name: string; + slug: string; + description?: string | null; + parentCategoryId?: string | null; + createdAtUtc: string; + updatedAtUtc?: string | null; + deletedOnUtc?: string | null; + deletedBy?: string | null; +}; + +export type CategoryTreeNodeDto = { + id: string; + name: string; + slug: string; + description?: string | null; + children: CategoryTreeNodeDto[]; +}; + +export type SearchCategoriesParams = { + search?: string; + parentCategoryId?: string | null; + pageNumber?: number; + pageSize?: number; + sortBy?: string; + sortDir?: "asc" | "desc"; +}; + +export type CreateCategoryInput = { + name: string; + description?: string | null; + parentCategoryId?: string | null; +}; + +export type UpdateCategoryInput = { + categoryId: string; + name: string; + description?: string | null; + parentCategoryId?: string | null; +}; + +export function searchCategories( + params: SearchCategoriesParams = {}, +): Promise> { + const query = new URLSearchParams(); + if (params.search) query.set("search", params.search); + if (params.parentCategoryId) query.set("parentCategoryId", params.parentCategoryId); + query.set("pageNumber", String(params.pageNumber ?? 1)); + query.set("pageSize", String(params.pageSize ?? 50)); + if (params.sortBy) query.set("sortBy", params.sortBy); + if (params.sortDir) query.set("sortDir", params.sortDir); + return apiFetch>( + `/api/v1/catalog/categories?${query.toString()}`, + ); +} + +export function getCategoryTree(): Promise { + return apiFetch("/api/v1/catalog/categories/tree"); +} + +export function getCategoryById(id: string): Promise { + return apiFetch(`/api/v1/catalog/categories/${encodeURIComponent(id)}`); +} + +export async function createCategory(input: CreateCategoryInput): Promise { + return apiFetch("/api/v1/catalog/categories", { + method: "POST", + body: JSON.stringify({ + name: input.name, + description: input.description ?? null, + parentCategoryId: input.parentCategoryId ?? null, + }), + }); +} + +export async function updateCategory(input: UpdateCategoryInput): Promise { + return apiFetch( + `/api/v1/catalog/categories/${encodeURIComponent(input.categoryId)}`, + { + method: "PUT", + body: JSON.stringify({ + categoryId: input.categoryId, + name: input.name, + description: input.description ?? null, + parentCategoryId: input.parentCategoryId ?? null, + }), + }, + ); +} + +export async function deleteCategory(id: string): Promise { + await apiFetch(`/api/v1/catalog/categories/${encodeURIComponent(id)}`, { + method: "DELETE", + }); +} + +// ─── Products ────────────────────────────────────────────────────────── + +export type MoneyDto = { + amount: number; + currency: string; +}; + +export type ProductDto = { + id: string; + sku: string; + name: string; + slug: string; + description?: string | null; + brandId: string; + categoryId: string; + price: MoneyDto; + stock: number; + isActive: boolean; + imageUrl?: string | null; + createdAtUtc: string; + updatedAtUtc?: string | null; + deletedOnUtc?: string | null; + deletedBy?: string | null; +}; + +export type SearchProductsParams = { + search?: string; + brandId?: string | null; + categoryId?: string | null; + isActive?: boolean | null; + pageNumber?: number; + pageSize?: number; + sortBy?: string; + sortDir?: "asc" | "desc"; +}; + +export type CreateProductInput = { + sku: string; + name: string; + description?: string | null; + brandId: string; + categoryId: string; + priceAmount: number; + priceCurrency: string; + stock: number; + imageUrl?: string | null; +}; + +export type UpdateProductInput = { + productId: string; + name: string; + description?: string | null; + brandId: string; + categoryId: string; + imageUrl?: string | null; + isActive: boolean; +}; + +export type ChangeProductPriceInput = { + productId: string; + amount: number; + currency: string; +}; + +export type AdjustProductStockInput = { + productId: string; + delta: number; +}; + +export function searchProducts( + params: SearchProductsParams = {}, +): Promise> { + const query = new URLSearchParams(); + if (params.search) query.set("search", params.search); + if (params.brandId) query.set("brandId", params.brandId); + if (params.categoryId) query.set("categoryId", params.categoryId); + if (params.isActive !== undefined && params.isActive !== null) + query.set("isActive", String(params.isActive)); + query.set("pageNumber", String(params.pageNumber ?? 1)); + query.set("pageSize", String(params.pageSize ?? 20)); + if (params.sortBy) query.set("sortBy", params.sortBy); + if (params.sortDir) query.set("sortDir", params.sortDir); + return apiFetch>( + `/api/v1/catalog/products?${query.toString()}`, + ); +} + +export function getProductById(id: string): Promise { + return apiFetch(`/api/v1/catalog/products/${encodeURIComponent(id)}`); +} + +export async function createProduct(input: CreateProductInput): Promise { + return apiFetch("/api/v1/catalog/products", { + method: "POST", + body: JSON.stringify({ + sku: input.sku, + name: input.name, + description: input.description ?? null, + brandId: input.brandId, + categoryId: input.categoryId, + priceAmount: input.priceAmount, + priceCurrency: input.priceCurrency, + stock: input.stock, + imageUrl: input.imageUrl ?? null, + }), + }); +} + +export async function updateProduct(input: UpdateProductInput): Promise { + return apiFetch( + `/api/v1/catalog/products/${encodeURIComponent(input.productId)}`, + { + method: "PUT", + body: JSON.stringify({ + productId: input.productId, + name: input.name, + description: input.description ?? null, + brandId: input.brandId, + categoryId: input.categoryId, + imageUrl: input.imageUrl ?? null, + isActive: input.isActive, + }), + }, + ); +} + +export async function changeProductPrice(input: ChangeProductPriceInput): Promise { + return apiFetch( + `/api/v1/catalog/products/${encodeURIComponent(input.productId)}/price`, + { + method: "PATCH", + body: JSON.stringify({ + productId: input.productId, + amount: input.amount, + currency: input.currency, + }), + }, + ); +} + +export async function adjustProductStock( + input: AdjustProductStockInput, +): Promise<{ stock: number }> { + return apiFetch<{ stock: number }>( + `/api/v1/catalog/products/${encodeURIComponent(input.productId)}/stock`, + { + method: "PATCH", + body: JSON.stringify({ + productId: input.productId, + delta: input.delta, + }), + }, + ); +} + +export async function deleteProduct(id: string): Promise { + await apiFetch(`/api/v1/catalog/products/${encodeURIComponent(id)}`, { + method: "DELETE", + }); +} + +// ─── Trash + Restore ────────────────────────────────────────────────── + +export function listTrashedBrands( + pageNumber = 1, + pageSize = 20, +): Promise> { + const q = new URLSearchParams({ + pageNumber: String(pageNumber), + pageSize: String(pageSize), + }); + return apiFetch>(`/api/v1/catalog/brands/trash?${q.toString()}`); +} + +export function restoreBrand(id: string): Promise { + return apiFetch(`/api/v1/catalog/brands/${encodeURIComponent(id)}/restore`, { + method: "POST", + }); +} + +export function listTrashedCategories( + pageNumber = 1, + pageSize = 20, +): Promise> { + const q = new URLSearchParams({ + pageNumber: String(pageNumber), + pageSize: String(pageSize), + }); + return apiFetch>( + `/api/v1/catalog/categories/trash?${q.toString()}`, + ); +} + +export function restoreCategory(id: string): Promise { + return apiFetch(`/api/v1/catalog/categories/${encodeURIComponent(id)}/restore`, { + method: "POST", + }); +} + +export function listTrashedProducts( + pageNumber = 1, + pageSize = 20, +): Promise> { + const q = new URLSearchParams({ + pageNumber: String(pageNumber), + pageSize: String(pageSize), + }); + return apiFetch>( + `/api/v1/catalog/products/trash?${q.toString()}`, + ); +} + +export function restoreProduct(id: string): Promise { + return apiFetch(`/api/v1/catalog/products/${encodeURIComponent(id)}/restore`, { + method: "POST", + }); +} diff --git a/clients/dashboard/src/api/health.ts b/clients/dashboard/src/api/health.ts new file mode 100644 index 0000000000..0ff41b074c --- /dev/null +++ b/clients/dashboard/src/api/health.ts @@ -0,0 +1,54 @@ +import { env } from "@/env"; + +export type HealthStatus = "Healthy" | "Degraded" | "Unhealthy"; + +export type HealthEntry = { + name: string; + status: HealthStatus; + description?: string | null; + durationMs: number; + details?: Record | null; +}; + +export type HealthResult = { + status: HealthStatus; + results: HealthEntry[]; +}; + +export type HealthSnapshot = HealthResult & { + /** Wall-clock time the response landed in the browser. */ + fetchedAt: string; + /** Round-trip from request start to response parse, in ms. */ + roundTripMs: number; + /** HTTP status. 200 = healthy, 503 = degraded/unhealthy (still has payload). */ + httpStatus: number; +}; + +/** + * Fetch the readiness report. Anonymous endpoint — bypasses apiFetch's + * auth/refresh dance. We accept both 200 and 503 because the API now + * returns the same JSON shape for both (so we can show *which* check + * failed). Network errors propagate as exceptions. + */ +export async function getReadiness(signal?: AbortSignal): Promise { + const t0 = performance.now(); + const url = `${env.apiBase}/health/ready`; + const response = await fetch(url, { + method: "GET", + headers: { Accept: "application/json" }, + signal, + }); + const roundTripMs = performance.now() - t0; + + if (response.status !== 200 && response.status !== 503) { + throw new Error(`Health endpoint returned ${response.status}`); + } + + const body = (await response.json()) as HealthResult; + return { + ...body, + fetchedAt: new Date().toISOString(), + roundTripMs, + httpStatus: response.status, + }; +} diff --git a/clients/dashboard/src/api/identity.ts b/clients/dashboard/src/api/identity.ts new file mode 100644 index 0000000000..a028593a24 --- /dev/null +++ b/clients/dashboard/src/api/identity.ts @@ -0,0 +1,333 @@ +import { apiFetch } from "@/lib/api-client"; +import type { PagedResponse } from "@/api/catalog"; + +// ----------------------------- +// Types +// ----------------------------- + +export type UserDto = { + id?: string; + userName?: string; + firstName?: string; + lastName?: string; + email?: string; + isActive: boolean; + emailConfirmed: boolean; + phoneNumber?: string; + imageUrl?: string; +}; + +export type UserRoleDto = { + roleId?: string; + roleName?: string; + description?: string; + enabled: boolean; +}; + +export type RoleDto = { + id: string; + name: string; + description?: string | null; + permissions?: string[] | null; +}; + +export type SearchUsersParams = { + pageNumber?: number; + pageSize?: number; + sort?: string; + search?: string; + isActive?: boolean | null; + emailConfirmed?: boolean | null; + roleId?: string | null; +}; + +export type RegisterUserInput = { + firstName: string; + lastName: string; + email: string; + userName: string; + password: string; + confirmPassword: string; + phoneNumber?: string; +}; + +export type RegisterUserResponse = { + userId: string; + message?: string; +}; + +export type UpsertRoleInput = { + id: string; + name: string; + description?: string; +}; + +// ----------------------------- +// Users +// ----------------------------- + +export async function searchUsers(params: SearchUsersParams = {}): Promise> { + const query = new URLSearchParams(); + query.set("PageNumber", String(params.pageNumber ?? 1)); + query.set("PageSize", String(params.pageSize ?? 20)); + if (params.sort) query.set("Sort", params.sort); + if (params.search) query.set("Search", params.search); + if (params.isActive !== null && params.isActive !== undefined) { + query.set("IsActive", String(params.isActive)); + } + if (params.emailConfirmed !== null && params.emailConfirmed !== undefined) { + query.set("EmailConfirmed", String(params.emailConfirmed)); + } + if (params.roleId) query.set("RoleId", params.roleId); + return apiFetch>(`/api/v1/identity/users/search?${query.toString()}`); +} + +export async function getUserById(id: string): Promise { + return apiFetch(`/api/v1/identity/users/${encodeURIComponent(id)}`); +} + +export async function getUserRoles(id: string): Promise { + return apiFetch(`/api/v1/identity/users/${encodeURIComponent(id)}/roles`); +} + +export async function assignUserRoles(userId: string, userRoles: UserRoleDto[]): Promise { + return apiFetch(`/api/v1/identity/users/${encodeURIComponent(userId)}/roles`, { + method: "POST", + body: JSON.stringify({ userId, userRoles }), + }); +} + +export async function toggleUserStatus(userId: string, activate: boolean): Promise { + await apiFetch(`/api/v1/identity/users/${encodeURIComponent(userId)}`, { + method: "PATCH", + body: JSON.stringify({ userId, activateUser: activate }), + }); +} + +export async function deleteUser(userId: string): Promise { + await apiFetch(`/api/v1/identity/users/${encodeURIComponent(userId)}`, { + method: "DELETE", + }); +} + +export async function registerUser(input: RegisterUserInput): Promise { + return apiFetch(`/api/v1/identity/register`, { + method: "POST", + body: JSON.stringify(input), + }); +} + +// ----------------------------- +// Roles +// ----------------------------- + +export async function listRoles(): Promise { + const result = await apiFetch(`/api/v1/identity/roles`); + if (Array.isArray(result)) return result; + return result.items ?? []; +} + +export async function getRoleById(id: string): Promise { + return apiFetch(`/api/v1/identity/roles/${encodeURIComponent(id)}`); +} + +export async function getRoleWithPermissions(id: string): Promise { + return apiFetch(`/api/v1/identity/${encodeURIComponent(id)}/permissions`); +} + +export async function upsertRole(input: UpsertRoleInput): Promise { + return apiFetch(`/api/v1/identity/roles`, { + method: "POST", + body: JSON.stringify(input), + }); +} + +export async function updateRolePermissions(roleId: string, permissions: string[]): Promise { + return apiFetch(`/api/v1/identity/${encodeURIComponent(roleId)}/permissions`, { + method: "PUT", + body: JSON.stringify({ roleId, permissions }), + }); +} + +export async function deleteRole(id: string): Promise { + await apiFetch(`/api/v1/identity/roles/${encodeURIComponent(id)}`, { + method: "DELETE", + }); +} + +// ----------------------------- +// User sessions (admin) +// ----------------------------- + +export type AdminUserSessionDto = { + id: string; + userId?: string | null; + userName?: string | null; + userEmail?: string | null; + ipAddress?: string | null; + deviceType?: string | null; + browser?: string | null; + browserVersion?: string | null; + operatingSystem?: string | null; + osVersion?: string | null; + createdAt: string; + lastActivityAt: string; + expiresAt: string; + isActive: boolean; + isCurrentSession: boolean; +}; + +export async function getUserSessionsAdmin(userId: string): Promise { + return apiFetch( + `/api/v1/identity/users/${encodeURIComponent(userId)}/sessions`, + ); +} + +export async function adminRevokeUserSession(userId: string, sessionId: string): Promise { + await apiFetch( + `/api/v1/identity/users/${encodeURIComponent(userId)}/sessions/${encodeURIComponent(sessionId)}`, + { method: "DELETE" }, + ); +} + +export async function adminRevokeAllUserSessions( + userId: string, +): Promise<{ revokedCount: number }> { + return apiFetch<{ revokedCount: number }>( + `/api/v1/identity/users/${encodeURIComponent(userId)}/sessions/revoke-all`, + { method: "POST", body: JSON.stringify({}) }, + ); +} + +// ----------------------------- +// Groups +// ----------------------------- + +export type GroupDto = { + id: string; + name: string; + description?: string | null; + isDefault: boolean; + isSystemGroup: boolean; + memberCount: number; + roleIds?: string[] | null; + roleNames?: string[] | null; + createdAt: string; +}; + +export type GroupMemberDto = { + userId: string; + userName?: string | null; + email?: string | null; + firstName?: string | null; + lastName?: string | null; + addedAt: string; + addedBy?: string | null; +}; + +export type CreateGroupInput = { + name: string; + description?: string; + isDefault: boolean; + roleIds?: string[]; +}; + +export type UpdateGroupInput = { + name: string; + description?: string; + isDefault: boolean; + roleIds?: string[]; +}; + +export async function listGroups(search?: string): Promise { + const q = search ? `?search=${encodeURIComponent(search)}` : ""; + const result = await apiFetch( + `/api/v1/identity/groups${q}`, + ); + if (Array.isArray(result)) return result; + return result.items ?? []; +} + +export async function getGroupById(id: string): Promise { + return apiFetch(`/api/v1/identity/groups/${encodeURIComponent(id)}`); +} + +export async function createGroup(input: CreateGroupInput): Promise { + return apiFetch(`/api/v1/identity/groups`, { + method: "POST", + body: JSON.stringify(input), + }); +} + +export async function updateGroup(id: string, input: UpdateGroupInput): Promise { + return apiFetch(`/api/v1/identity/groups/${encodeURIComponent(id)}`, { + method: "PUT", + body: JSON.stringify(input), + }); +} + +export async function deleteGroup(id: string): Promise { + await apiFetch(`/api/v1/identity/groups/${encodeURIComponent(id)}`, { + method: "DELETE", + }); +} + +export async function getGroupMembers(groupId: string): Promise { + return apiFetch( + `/api/v1/identity/groups/${encodeURIComponent(groupId)}/members`, + ); +} + +export async function addUsersToGroup( + groupId: string, + userIds: string[], +): Promise<{ addedCount: number; alreadyMemberUserIds: string[] }> { + return apiFetch<{ addedCount: number; alreadyMemberUserIds: string[] }>( + `/api/v1/identity/groups/${encodeURIComponent(groupId)}/members`, + { method: "POST", body: JSON.stringify({ userIds }) }, + ); +} + +export async function removeUserFromGroup(groupId: string, userId: string): Promise { + await apiFetch( + `/api/v1/identity/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}`, + { method: "DELETE" }, + ); +} + +// ----------------------------- +// Impersonation +// ----------------------------- + +export type ImpersonationResponse = { + accessToken: string; + accessTokenExpiresAt: string; + actorUserId: string; + actorTenantId: string; + impersonatedUserId: string; + impersonatedTenantId: string; +}; + +export type EndImpersonationResponse = { + accessToken: string; + refreshToken: string; + refreshTokenExpiresAt: string; + accessTokenExpiresAt: string; +}; + +export async function startImpersonation(input: { + targetUserId: string; + targetTenantId: string; + reason?: string; +}): Promise { + return apiFetch(`/api/v1/identity/impersonation/start`, { + method: "POST", + body: JSON.stringify(input), + }); +} + +export async function endImpersonation(): Promise { + return apiFetch(`/api/v1/identity/impersonation/end`, { + method: "POST", + }); +} diff --git a/clients/dashboard/src/api/permissions-catalog.ts b/clients/dashboard/src/api/permissions-catalog.ts new file mode 100644 index 0000000000..e04ec0b565 --- /dev/null +++ b/clients/dashboard/src/api/permissions-catalog.ts @@ -0,0 +1,81 @@ +// Mirror of FSH.Modules.Identity.Contracts.Authorization.IdentityPermissions.All +// Keep in sync when permissions are added/removed server-side. + +export type PermissionDescriptor = { + name: string; // Permissions.{Resource}.{Action} + description: string; + resource: string; + action: string; + isBasic?: boolean; + isRoot?: boolean; +}; + +const def = ( + description: string, + action: string, + resource: string, + flags: { isBasic?: boolean; isRoot?: boolean } = {}, +): PermissionDescriptor => ({ + name: `Permissions.${resource}.${action}`, + description, + resource, + action, + isBasic: flags.isBasic, + isRoot: flags.isRoot, +}); + +export const IDENTITY_PERMISSIONS: PermissionDescriptor[] = [ + // Users + def("View Users", "View", "Users", { isBasic: true }), + def("Search Users", "Search", "Users"), + def("Create Users", "Create", "Users"), + def("Update Users", "Update", "Users"), + def("Delete Users", "Delete", "Users"), + def("Export Users", "Export", "Users"), + def("Manage User Roles", "ManageRoles", "Users"), + def("Impersonate User", "Impersonate", "Users"), + + // UserRoles + def("View User Roles", "View", "UserRoles", { isBasic: true }), + def("Update User Roles", "Update", "UserRoles"), + + // Roles + def("View Roles", "View", "Roles", { isBasic: true }), + def("Create Roles", "Create", "Roles"), + def("Update Roles", "Update", "Roles"), + def("Delete Roles", "Delete", "Roles"), + + // RoleClaims + def("View Role Claims", "View", "RoleClaims", { isBasic: true }), + def("Update Role Claims", "Update", "RoleClaims"), + + // Sessions + def("View My Sessions", "View", "Sessions", { isBasic: true }), + def("Revoke My Sessions", "Revoke", "Sessions", { isBasic: true }), + def("View All Sessions", "ViewAll", "Sessions"), + def("Revoke Any Session", "RevokeAll", "Sessions"), + + // Groups + def("View Groups", "View", "Groups", { isBasic: true }), + def("Create Groups", "Create", "Groups"), + def("Update Groups", "Update", "Groups"), + def("Delete Groups", "Delete", "Groups"), + def("Manage Group Members", "ManageMembers", "Groups"), +]; + +export type PermissionGroup = { + resource: string; + permissions: PermissionDescriptor[]; +}; + +export function groupPermissions(perms: PermissionDescriptor[]): PermissionGroup[] { + const map = new Map(); + for (const p of perms) { + const list = map.get(p.resource) ?? []; + list.push(p); + map.set(p.resource, list); + } + return Array.from(map.entries()).map(([resource, permissions]) => ({ resource, permissions })); +} + +export const PERMISSION_GROUPS: PermissionGroup[] = groupPermissions(IDENTITY_PERMISSIONS); diff --git a/clients/dashboard/src/api/sessions.ts b/clients/dashboard/src/api/sessions.ts new file mode 100644 index 0000000000..bdf08313d6 --- /dev/null +++ b/clients/dashboard/src/api/sessions.ts @@ -0,0 +1,78 @@ +import { apiFetch } from "@/lib/api-client"; +import type { PagedResponse } from "@/api/catalog"; + +export type UserSessionDto = { + id: string; + userId?: string | null; + userName?: string | null; + userEmail?: string | null; + ipAddress?: string | null; + deviceType?: string | null; + browser?: string | null; + browserVersion?: string | null; + operatingSystem?: string | null; + osVersion?: string | null; + createdAt: string; + lastActivityAt: string; + expiresAt: string; + isActive: boolean; + isCurrentSession: boolean; +}; + +const IDENTITY = "/api/v1/identity"; +const BASE = `${IDENTITY}/sessions`; + +// ─── Self-service ──────────────────────────────────────────────────── + +export function getMySessions() { + return apiFetch(`${BASE}/me`); +} + +export function revokeSession(sessionId: string) { + return apiFetch(`${BASE}/${sessionId}`, { method: "DELETE" }); +} + +export function revokeAllOtherSessions() { + return apiFetch<{ revokedCount: number }>(`${BASE}/revoke-all`, { + method: "POST", + body: JSON.stringify({}), + }); +} + +// ─── Admin / tenant-wide ───────────────────────────────────────────── + +export type TenantSessionsParams = { + includeInactive?: boolean; + search?: string; + pageNumber?: number; + pageSize?: number; +}; + +export function getTenantSessions( + params: TenantSessionsParams = {}, +): Promise> { + const q = new URLSearchParams(); + if (params.includeInactive) q.set("includeInactive", "true"); + if (params.search) q.set("search", params.search); + q.set("pageNumber", String(params.pageNumber ?? 1)); + q.set("pageSize", String(params.pageSize ?? 50)); + return apiFetch>(`${BASE}?${q.toString()}`); +} + +/** + * Admin: revoke a session belonging to a specific user. Maps to + * DELETE /users/{userId}/sessions/{sessionId}. + */ +export function adminRevokeUserSessionById(userId: string, sessionId: string) { + return apiFetch( + `${IDENTITY}/users/${encodeURIComponent(userId)}/sessions/${encodeURIComponent(sessionId)}`, + { method: "DELETE" }, + ); +} + +export function adminRevokeAllUserSessions(userId: string) { + return apiFetch<{ revokedCount: number }>( + `${IDENTITY}/users/${encodeURIComponent(userId)}/sessions/revoke-all`, + { method: "POST", body: JSON.stringify({}) }, + ); +} diff --git a/clients/dashboard/src/api/tickets.ts b/clients/dashboard/src/api/tickets.ts new file mode 100644 index 0000000000..113f2301cc --- /dev/null +++ b/clients/dashboard/src/api/tickets.ts @@ -0,0 +1,160 @@ +import { apiFetch } from "@/lib/api-client"; +import type { PagedResponse } from "@/api/catalog"; + +// ─── Enums (string-serialized via JsonStringEnumConverter) ─────────────── + +export type TicketStatus = "Open" | "InProgress" | "Resolved" | "Closed"; + +export type TicketPriority = "Low" | "Medium" | "High" | "Critical"; + +export const TICKET_STATUSES: readonly TicketStatus[] = [ + "Open", + "InProgress", + "Resolved", + "Closed", +] as const; + +export const TICKET_PRIORITIES: readonly TicketPriority[] = [ + "Low", + "Medium", + "High", + "Critical", +] as const; + +// ─── DTOs ───────────────────────────────────────────────────────────────── + +export type TicketDto = { + id: string; + number: string; + title: string; + description?: string | null; + status: TicketStatus; + priority: TicketPriority; + reporterUserId: string; + assignedToUserId?: string | null; + resolutionNote?: string | null; + createdAtUtc: string; + updatedAtUtc?: string | null; + resolvedAtUtc?: string | null; + closedAtUtc?: string | null; + commentCount: number; + deletedOnUtc?: string | null; + deletedBy?: string | null; +}; + +export type TicketCommentDto = { + id: string; + ticketId: string; + authorUserId: string; + body: string; + createdAtUtc: string; +}; + +// ─── Inputs ─────────────────────────────────────────────────────────────── + +export type SearchTicketsParams = { + search?: string; + status?: TicketStatus | null; + priority?: TicketPriority | null; + assignedToUserId?: string | null; + reporterUserId?: string | null; + pageNumber?: number; + pageSize?: number; + sortBy?: string; + sortDir?: "asc" | "desc"; +}; + +export type CreateTicketInput = { + title: string; + description?: string | null; + priority?: TicketPriority; + assignedToUserId?: string | null; +}; + +// ─── Endpoints ──────────────────────────────────────────────────────────── + +const BASE = "/api/v1"; + +export function searchTickets( + params: SearchTicketsParams = {}, +): Promise> { + const q = new URLSearchParams(); + if (params.search) q.set("search", params.search); + if (params.status) q.set("status", params.status); + if (params.priority) q.set("priority", params.priority); + if (params.assignedToUserId) q.set("assignedToUserId", params.assignedToUserId); + if (params.reporterUserId) q.set("reporterUserId", params.reporterUserId); + q.set("pageNumber", String(params.pageNumber ?? 1)); + q.set("pageSize", String(params.pageSize ?? 20)); + if (params.sortBy) q.set("sortBy", params.sortBy); + if (params.sortDir) q.set("sortDir", params.sortDir); + return apiFetch>(`${BASE}/tickets?${q.toString()}`); +} + +export function getTicketById(id: string): Promise { + return apiFetch(`${BASE}/tickets/${encodeURIComponent(id)}`); +} + +export function createTicket(input: CreateTicketInput): Promise { + return apiFetch(`${BASE}/tickets`, { + method: "POST", + body: JSON.stringify({ + title: input.title, + description: input.description ?? null, + priority: input.priority ?? "Medium", + assignedToUserId: input.assignedToUserId ?? null, + }), + }); +} + +export function assignTicket(ticketId: string, assigneeUserId: string | null): Promise { + return apiFetch(`${BASE}/tickets/${encodeURIComponent(ticketId)}/assign`, { + method: "POST", + body: JSON.stringify({ assigneeUserId }), + }); +} + +export function resolveTicket(ticketId: string, resolutionNote?: string | null): Promise { + return apiFetch(`${BASE}/tickets/${encodeURIComponent(ticketId)}/resolve`, { + method: "POST", + body: JSON.stringify({ resolutionNote: resolutionNote ?? null }), + }); +} + +export function reopenTicket(ticketId: string): Promise { + return apiFetch(`${BASE}/tickets/${encodeURIComponent(ticketId)}/reopen`, { + method: "POST", + }); +} + +export function addTicketComment(ticketId: string, body: string): Promise { + return apiFetch(`${BASE}/tickets/${encodeURIComponent(ticketId)}/comments`, { + method: "POST", + body: JSON.stringify({ body }), + }); +} + +export function listTicketComments(ticketId: string): Promise { + return apiFetch( + `${BASE}/tickets/${encodeURIComponent(ticketId)}/comments`, + ); +} + +// ─── Trash + Restore (each top-level resource exposes the same shape) ───── + +export function listTrashedTickets( + pageNumber = 1, + pageSize = 20, +): Promise> { + const q = new URLSearchParams({ + pageNumber: String(pageNumber), + pageSize: String(pageSize), + }); + return apiFetch>(`${BASE}/tickets/trash?${q.toString()}`); +} + +export function restoreTicket(ticketId: string): Promise { + return apiFetch(`${BASE}/tickets/${encodeURIComponent(ticketId)}/restore`, { + method: "POST", + }); +} diff --git a/clients/dashboard/src/auth/api.ts b/clients/dashboard/src/auth/api.ts new file mode 100644 index 0000000000..1ed1c1e64f --- /dev/null +++ b/clients/dashboard/src/auth/api.ts @@ -0,0 +1,25 @@ +import { apiFetch } from "@/lib/api-client"; + +export type TokenResponse = { + accessToken: string; + refreshToken: string; + accessTokenExpiresAt: string; + refreshTokenExpiresAt: string; +}; + +export function issueToken(input: { + email: string; + password: string; + tenant: string; +}) { + return apiFetch("/api/v1/identity/token/issue", { + method: "POST", + body: JSON.stringify({ email: input.email, password: input.password }), + // X-FSH-App tells the API this credential request originated from the + // tenant dashboard. The server uses it to enforce the SuperAdmin / app + // boundary — a root-tenant login submitted with X-FSH-App=dashboard is + // rejected with 403 instead of receiving a usable token. + headers: { tenant: input.tenant, "X-FSH-App": "dashboard" }, + skipAuth: true, + }); +} diff --git a/clients/dashboard/src/auth/auth-context.tsx b/clients/dashboard/src/auth/auth-context.tsx new file mode 100644 index 0000000000..843f3a127c --- /dev/null +++ b/clients/dashboard/src/auth/auth-context.tsx @@ -0,0 +1,194 @@ +import { createContext, useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { tokenStore } from "@/auth/token-store"; +import { decodeJwt, type JwtClaims } from "@/auth/jwt"; +import { issueToken } from "@/auth/api"; +import { endImpersonation, startImpersonation } from "@/api/identity"; + +export type AuthUser = { + id: string; + email?: string; + name?: string; + tenant?: string; + permissions: string[]; +}; + +export type ImpersonationInfo = { + /** The original operator's user id, taken from the act_sub claim. */ + actorUserId: string; + /** The original operator's tenant, taken from the act_tenant claim. */ + actorTenant?: string; + /** Display name for the original operator if the token carries act_name. */ + actorName?: string; +}; + +export type AuthContextValue = { + user: AuthUser | null; + isAuthenticated: boolean; + /** Truthy iff the current access token carries act_sub (impersonation mode). */ + impersonation: ImpersonationInfo | null; + login: (input: { email: string; password: string; tenant: string }) => Promise; + logout: () => void; + /** Begin impersonating another user. Resolves once the new token is installed. */ + beginImpersonation: (input: { + targetUserId: string; + targetTenantId: string; + reason?: string; + }) => Promise; + /** Stop impersonating and restore the operator session. */ + stopImpersonation: () => Promise; +}; + +export const AuthContext = createContext(null); + +function claimsToUser(claims: JwtClaims | null): AuthUser | null { + if (!claims?.sub) return null; + const permissions = Array.isArray(claims.permissions) + ? claims.permissions + : typeof claims.permissions === "string" + ? [claims.permissions] + : []; + // `name` is the standard short claim; `unique_name` is what + // JwtSecurityTokenHandler emits for ClaimTypes.Name. Treat empty + // strings as missing so the topbar falls through to email/Unknown + // instead of rendering blank. + const name = pickFirstNonEmpty(claims.name, claims.unique_name); + const email = pickFirstNonEmpty(claims.email); + return { + id: claims.sub, + email, + name, + tenant: claims.tenant, + permissions, + }; +} + +function claimsToImpersonation(claims: JwtClaims | null): ImpersonationInfo | null { + if (!claims?.act_sub) return null; + return { + actorUserId: claims.act_sub, + actorTenant: claims.act_tenant, + actorName: claims.act_name, + }; +} + +function pickFirstNonEmpty(...candidates: Array): string | undefined { + for (const c of candidates) { + if (typeof c === "string" && c.trim().length > 0) return c; + } + return undefined; +} + +export function AuthProvider({ children }: { children: ReactNode }) { + const queryClient = useQueryClient(); + const [user, setUser] = useState(() => + claimsToUser(decodeJwt(tokenStore.getAccessToken())), + ); + const [impersonation, setImpersonation] = useState(() => + claimsToImpersonation(decodeJwt(tokenStore.getAccessToken())), + ); + + useEffect(() => { + const refresh = () => { + const claims = decodeJwt(tokenStore.getAccessToken()); + setUser(claimsToUser(claims)); + setImpersonation(claimsToImpersonation(claims)); + }; + const unsubscribe = tokenStore.subscribe(refresh); + + // The token store's subscribe() only fires for in-app mutations. Storage + // changes from another tab fire a `storage` event, and same-tab manual + // clears (e.g. via DevTools) need to be picked up when the user returns + // to the tab — otherwise `isAuthenticated` stays true while the token + // is gone, and protected requests silently 401 with no header attached. + const onStorage = (e: StorageEvent) => { + if (e.key === null || e.key.startsWith("fsh.dashboard.")) refresh(); + }; + const onVisibility = () => { + if (document.visibilityState === "visible") refresh(); + }; + window.addEventListener("storage", onStorage); + document.addEventListener("visibilitychange", onVisibility); + + return () => { + unsubscribe(); + window.removeEventListener("storage", onStorage); + document.removeEventListener("visibilitychange", onVisibility); + }; + }, []); + + const login = useCallback( + async (input: { email: string; password: string; tenant: string }) => { + tokenStore.setTenant(input.tenant); + const tokens = await issueToken(input); + // Defence-in-depth: even though the API rejects root-tenant logins + // submitted with X-FSH-App=dashboard, double-check the issued token + // so a future API regression can't quietly drop a root token into + // a tenant-dashboard session. + const claims = decodeJwt(tokens.accessToken); + if (claims?.tenant === "root") { + tokenStore.clear(); + throw new Error( + "SuperAdmin accounts must use the admin app. Sign in there instead.", + ); + } + tokenStore.setTokens(tokens.accessToken, tokens.refreshToken); + // Drop any cached query state from before login. Without this, a + // failed pre-login probe (e.g. OverviewPage's billing fetch + // firing during the brief window before ProtectedRoute redirects + // to /login, or a stale error from a previous session) sticks in + // the react-query cache as a 401 and renders as an ErrorBand on + // the next page — react-query's retry config blocks auto-retries + // for 401, so the stale error would never refetch on its own. + queryClient.clear(); + }, + [queryClient], + ); + + const logout = useCallback(() => { + tokenStore.clear(); + queryClient.clear(); + }, [queryClient]); + + const beginImpersonation = useCallback( + async (input: { targetUserId: string; targetTenantId: string; reason?: string }) => { + const response = await startImpersonation(input); + // Swap the active token. queryClient.clear() drops cached queries + // so the next render fetches with the new identity — otherwise + // user/role/permission caches from the actor session would leak. + tokenStore.beginImpersonation(response.accessToken, response.impersonatedTenantId); + queryClient.clear(); + }, + [queryClient], + ); + + const stopImpersonation = useCallback(async () => { + try { + const fresh = await endImpersonation(); + tokenStore.endImpersonationWithFreshTokens(fresh.accessToken, fresh.refreshToken); + } catch { + // End endpoint failed (server unreachable / token invalid). Fall + // back to whatever we stashed locally; the operator may need to + // re-authenticate if the stashed access token has expired. + tokenStore.restoreStashedActor(); + throw new Error("End impersonation failed; restored local session."); + } finally { + queryClient.clear(); + } + }, [queryClient]); + + const value = useMemo( + () => ({ + user, + isAuthenticated: user !== null, + impersonation, + login, + logout, + beginImpersonation, + stopImpersonation, + }), + [user, impersonation, login, logout, beginImpersonation, stopImpersonation], + ); + + return {children}; +} diff --git a/clients/dashboard/src/auth/jwt.ts b/clients/dashboard/src/auth/jwt.ts new file mode 100644 index 0000000000..819d27d139 --- /dev/null +++ b/clients/dashboard/src/auth/jwt.ts @@ -0,0 +1,39 @@ +export type JwtClaims = { + sub?: string; + email?: string; + name?: string; + /** + * `unique_name` is what JwtSecurityTokenHandler emits when the source + * is `ClaimTypes.Name` and no explicit `name` claim is also added. + * Read as a fallback so tokens issued without a registered `name` + * claim still resolve a username. + */ + unique_name?: string; + tenant?: string; + permissions?: string[] | string; + exp?: number; + /** + * Actor claims set by the StartImpersonation flow. When present, the + * current access token represents an impersonation session — the + * `sub`/`tenant` claims describe the impersonated user, and these + * `act_*` claims preserve the original operator's identity so the + * EndImpersonation endpoint can swap back without re-authenticating. + */ + act_sub?: string; + act_tenant?: string; + act_name?: string; +}; + +export function decodeJwt(token: string | null | undefined): JwtClaims | null { + if (!token) return null; + const parts = token.split("."); + if (parts.length !== 3) return null; + try { + const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/"); + const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4); + const json = atob(padded); + return JSON.parse(json) as JwtClaims; + } catch { + return null; + } +} diff --git a/clients/dashboard/src/auth/protected-route.tsx b/clients/dashboard/src/auth/protected-route.tsx new file mode 100644 index 0000000000..849cf7356c --- /dev/null +++ b/clients/dashboard/src/auth/protected-route.tsx @@ -0,0 +1,13 @@ +import { Navigate, Outlet, useLocation } from "react-router-dom"; +import { useAuth } from "@/auth/use-auth"; + +export function ProtectedRoute() { + const { isAuthenticated } = useAuth(); + const location = useLocation(); + + if (!isAuthenticated) { + return ; + } + + return ; +} diff --git a/clients/dashboard/src/auth/token-store.ts b/clients/dashboard/src/auth/token-store.ts new file mode 100644 index 0000000000..77433d81be --- /dev/null +++ b/clients/dashboard/src/auth/token-store.ts @@ -0,0 +1,113 @@ +const ACCESS_KEY = "fsh.dashboard.accessToken"; +const REFRESH_KEY = "fsh.dashboard.refreshToken"; +const TENANT_KEY = "fsh.dashboard.tenant"; + +// Impersonation stash. While an operator is impersonating another user, +// the live token store holds the impersonation tokens; the operator's +// original tokens sit under these keys so the End flow can fall back to +// them locally if the server-side End call fails (e.g. network down). +const STASH_ACCESS_KEY = "fsh.dashboard.impersonation.actorAccessToken"; +const STASH_REFRESH_KEY = "fsh.dashboard.impersonation.actorRefreshToken"; +const STASH_TENANT_KEY = "fsh.dashboard.impersonation.actorTenant"; + +type Listener = () => void; + +const listeners = new Set(); + +function emit() { + for (const listener of listeners) listener(); +} + +export const tokenStore = { + getAccessToken: () => localStorage.getItem(ACCESS_KEY), + getRefreshToken: () => localStorage.getItem(REFRESH_KEY), + getTenant: () => localStorage.getItem(TENANT_KEY), + + setTokens(accessToken: string, refreshToken: string) { + localStorage.setItem(ACCESS_KEY, accessToken); + localStorage.setItem(REFRESH_KEY, refreshToken); + emit(); + }, + + setTenant(tenant: string) { + localStorage.setItem(TENANT_KEY, tenant); + emit(); + }, + + clear() { + localStorage.removeItem(ACCESS_KEY); + localStorage.removeItem(REFRESH_KEY); + // Also clear any impersonation stash so a fresh login doesn't + // inherit half of a previous operator's session. + localStorage.removeItem(STASH_ACCESS_KEY); + localStorage.removeItem(STASH_REFRESH_KEY); + localStorage.removeItem(STASH_TENANT_KEY); + emit(); + }, + + /** + * Swap the active token to an impersonation access token while preserving + * the original operator's tokens locally. The impersonation token has no + * refresh counterpart server-side, so we drop the refresh slot — auto- + * refresh in the api client checks for refreshToken presence and will + * skip silently (impersonation sessions are intentionally short-lived). + */ + beginImpersonation(impersonationAccessToken: string, impersonatedTenant: string | null) { + const access = localStorage.getItem(ACCESS_KEY); + const refresh = localStorage.getItem(REFRESH_KEY); + const tenant = localStorage.getItem(TENANT_KEY); + if (access) localStorage.setItem(STASH_ACCESS_KEY, access); + if (refresh) localStorage.setItem(STASH_REFRESH_KEY, refresh); + if (tenant) localStorage.setItem(STASH_TENANT_KEY, tenant); + + localStorage.setItem(ACCESS_KEY, impersonationAccessToken); + localStorage.removeItem(REFRESH_KEY); + if (impersonatedTenant) localStorage.setItem(TENANT_KEY, impersonatedTenant); + emit(); + }, + + /** + * Replace the live tokens with a fresh actor pair returned by the End + * Impersonation endpoint, and clear the stash. Use this on End success. + */ + endImpersonationWithFreshTokens(accessToken: string, refreshToken: string) { + const stashTenant = localStorage.getItem(STASH_TENANT_KEY); + localStorage.setItem(ACCESS_KEY, accessToken); + localStorage.setItem(REFRESH_KEY, refreshToken); + if (stashTenant) localStorage.setItem(TENANT_KEY, stashTenant); + localStorage.removeItem(STASH_ACCESS_KEY); + localStorage.removeItem(STASH_REFRESH_KEY); + localStorage.removeItem(STASH_TENANT_KEY); + emit(); + }, + + /** + * Last-resort local restore — used if the End endpoint fails. Reinstall + * the stashed actor tokens so the operator at least has *some* session + * (the original access token may itself be expired by now, in which + * case auto-refresh with the stashed refresh token kicks in). + */ + restoreStashedActor(): boolean { + const access = localStorage.getItem(STASH_ACCESS_KEY); + const refresh = localStorage.getItem(STASH_REFRESH_KEY); + const tenant = localStorage.getItem(STASH_TENANT_KEY); + if (!access) return false; + localStorage.setItem(ACCESS_KEY, access); + if (refresh) localStorage.setItem(REFRESH_KEY, refresh); + if (tenant) localStorage.setItem(TENANT_KEY, tenant); + localStorage.removeItem(STASH_ACCESS_KEY); + localStorage.removeItem(STASH_REFRESH_KEY); + localStorage.removeItem(STASH_TENANT_KEY); + emit(); + return true; + }, + + hasImpersonationStash: () => localStorage.getItem(STASH_ACCESS_KEY) !== null, + + subscribe(listener: Listener) { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, +}; diff --git a/clients/dashboard/src/auth/use-auth.ts b/clients/dashboard/src/auth/use-auth.ts new file mode 100644 index 0000000000..e66d0827d8 --- /dev/null +++ b/clients/dashboard/src/auth/use-auth.ts @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import { AuthContext } from "@/auth/auth-context"; + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) { + throw new Error("useAuth must be used within AuthProvider"); + } + return ctx; +} diff --git a/clients/dashboard/src/components/command-palette/command-palette.tsx b/clients/dashboard/src/components/command-palette/command-palette.tsx new file mode 100644 index 0000000000..9a121e9e8a --- /dev/null +++ b/clients/dashboard/src/components/command-palette/command-palette.tsx @@ -0,0 +1,404 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from "react"; +import { useNavigate } from "react-router-dom"; +import { Command } from "cmdk"; +import { + Activity, + HeartPulse, + KeyRound, + LayoutDashboard, + LogOut, + Monitor, + Moon, + Palette, + Receipt, + ScrollText, + Search, + Settings as SettingsIcon, + Shield, + Sparkles, + Sun, + UserRound, +} from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@/components/ui/dialog"; +import { useAuth } from "@/auth/use-auth"; +import { useTheme } from "@/components/theme/theme-provider"; +import { accents } from "@/components/theme/appearance-options"; +import { cn } from "@/lib/cn"; + +type CommandPaletteContextValue = { + open: boolean; + setOpen: (next: boolean) => void; + toggle: () => void; +}; + +const CommandPaletteContext = createContext(null); + +export function useCommandPalette() { + const ctx = useContext(CommandPaletteContext); + if (!ctx) throw new Error("useCommandPalette must be used within CommandPaletteProvider"); + return ctx; +} + +/** + * Palette provider — owns open state, registers the global ⌘K / Ctrl+K + * listener, and renders the palette dialog. Components anywhere in the + * tree can call useCommandPalette() to open it imperatively. + */ +export function CommandPaletteProvider({ children }: { children: ReactNode }) { + const [open, setOpen] = useState(false); + const toggle = useCallback(() => setOpen((o) => !o), []); + + // Global keybinding — ⌘K on macOS, Ctrl+K elsewhere. + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { + e.preventDefault(); + toggle(); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [toggle]); + + const value = useMemo( + () => ({ open, setOpen, toggle }), + [open, toggle], + ); + + // The dialog itself uses useNavigate, which requires the router + // context. Since the provider is rendered as a sibling of the + // RouterProvider (so children of every route share the open state), + // the dialog can't live here. It's rendered separately by + // inside the AppShell. + return ( + + {children} + + ); +} + +/** + * Renders the palette dialog. Must be mounted inside the React Router + * tree so useNavigate is available. Reads open/close from the provider. + */ +export function CommandPaletteRoot() { + const { open, setOpen } = useCommandPalette(); + return ; +} + +// ──────────────────────────────────────────────────────────────────── +// Internal — the dialog body itself +// ──────────────────────────────────────────────────────────────────── + +type ActionItem = { + id: string; + label: string; + hint?: string; + Icon: React.ComponentType<{ className?: string }>; + /** Free-form keywords for fuzzy matching. */ + keywords?: string[]; + shortcut?: string; + perform: () => void; +}; + +type ActionGroup = { + heading: string; + items: ActionItem[]; +}; + +function CommandPaletteDialog({ + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: (next: boolean) => void; +}) { + const navigate = useNavigate(); + const { logout } = useAuth(); + const { setMode, setAccent } = useTheme(); + + // Build the action set fresh each time the palette opens. The ones + // that navigate close the palette; the ones that mutate appearance + // don't, so the user can preview multiple choices. + const groups = useMemo(() => { + const close = () => onOpenChange(false); + const go = (path: string) => () => { + navigate(path); + close(); + }; + return [ + { + heading: "Navigate", + items: [ + { + id: "nav-overview", + label: "Overview", + hint: "Tenant telemetry & usage", + Icon: LayoutDashboard, + keywords: ["home", "dashboard"], + perform: go("/"), + }, + { + id: "nav-activity", + label: "Live activity", + hint: "Real-time event stream", + Icon: Activity, + keywords: ["events", "sse", "log"], + perform: go("/activity"), + }, + { + id: "nav-invoices", + label: "Invoices", + hint: "Billing history", + Icon: Receipt, + keywords: ["billing", "payment"], + perform: go("/invoices"), + }, + { + id: "nav-health", + label: "Health", + hint: "Readiness probe & dependencies", + Icon: HeartPulse, + keywords: ["status", "uptime", "system", "ready", "redis", "postgres"], + perform: go("/system/health"), + }, + { + id: "nav-audits", + label: "Audit trail", + hint: "Activity, security, entity-change events", + Icon: ScrollText, + keywords: ["audit", "log", "compliance", "security", "trace", "correlation"], + perform: go("/system/audits"), + }, + { + id: "nav-settings", + label: "Settings", + Icon: SettingsIcon, + keywords: ["preferences", "config"], + perform: go("/settings"), + }, + ], + }, + { + heading: "Account", + items: [ + { + id: "acc-profile", + label: "Profile", + hint: "Name, email, contact", + Icon: UserRound, + perform: go("/settings/profile"), + }, + { + id: "acc-security", + label: "Security", + hint: "Password, 2FA, sessions", + Icon: Shield, + keywords: ["password", "2fa", "sessions"], + perform: go("/settings/security"), + }, + { + id: "acc-keys", + label: "API keys", + hint: "Generate & rotate", + Icon: KeyRound, + keywords: ["token", "credentials"], + perform: go("/settings/api-keys"), + }, + { + id: "acc-notifications", + label: "Notifications", + hint: "Email preferences", + Icon: Sparkles, + perform: go("/settings/notifications"), + }, + ], + }, + { + heading: "Theme", + items: [ + { + id: "theme-light", + label: "Switch to light", + Icon: Sun, + keywords: ["bright", "day"], + perform: () => setMode("light"), + }, + { + id: "theme-dark", + label: "Switch to dark", + Icon: Moon, + keywords: ["night", "oled"], + perform: () => setMode("dark"), + }, + { + id: "theme-system", + label: "Follow system theme", + Icon: Monitor, + keywords: ["auto"], + perform: () => setMode("system"), + }, + ], + }, + { + heading: "Accent", + items: accents.map((a) => ({ + id: `accent-${a.id}`, + label: `Set accent: ${a.label}`, + hint: a.description, + Icon: Palette, + keywords: ["color", "brand", a.id], + perform: () => setAccent(a.id), + })), + }, + { + heading: "Session", + items: [ + { + id: "sess-logout", + label: "Sign out", + hint: "End this session", + Icon: LogOut, + keywords: ["logout", "exit", "quit"], + perform: () => { + close(); + logout(); + }, + }, + ], + }, + ]; + }, [navigate, onOpenChange, setMode, setAccent, logout]); + + return ( + + + Command palette + + Search across pages, account actions, theme and accent. Use arrow keys to navigate; Enter to select. + + + + {/* Search row */} +
    + + + + Esc + +
    + + {/* Results */} + + +

    No matches

    +

    + Try a different keyword — pages, settings, theme, accent, or sign-out. +

    +
    + + {groups.map((group) => ( + + {group.items.map((item) => ( + + ))} + + ))} +
    + + {/* Footer */} +
    +
    + + + + navigate + + + + select + +
    + + v0.1 + +
    +
    +
    +
    + ); +} + +function CommandRow({ item }: { item: ActionItem }) { + const { Icon, label, hint, keywords, perform } = item; + return ( + + + + + + {label} + {hint && ( + + {hint} + + )} + + + ); +} diff --git a/clients/dashboard/src/components/layout/app-shell.tsx b/clients/dashboard/src/components/layout/app-shell.tsx new file mode 100644 index 0000000000..c23c36fe85 --- /dev/null +++ b/clients/dashboard/src/components/layout/app-shell.tsx @@ -0,0 +1,28 @@ +import { Outlet } from "react-router-dom"; +import { Sidebar } from "@/components/layout/sidebar"; +import { Topbar } from "@/components/layout/topbar"; +import { ImpersonationBanner } from "@/components/layout/impersonation-banner"; +import { SseProvider } from "@/sse/sse-context"; +import { CommandPaletteRoot } from "@/components/command-palette/command-palette"; + +export function AppShell() { + return ( + +
    + +
    + +
    + +
    + +
    +
    +
    +
    + {/* Mounted inside the router subtree so useNavigate inside the + palette resolves correctly. */} + +
    + ); +} diff --git a/clients/dashboard/src/components/layout/impersonation-banner.tsx b/clients/dashboard/src/components/layout/impersonation-banner.tsx new file mode 100644 index 0000000000..1375f39eb3 --- /dev/null +++ b/clients/dashboard/src/components/layout/impersonation-banner.tsx @@ -0,0 +1,182 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useMutation } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { ArrowRight, LogOut, ShieldAlert, UserCog } from "lucide-react"; +import { useAuth } from "@/auth/use-auth"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/cn"; + +/** + * Sticky banner shown across every page while the operator is impersonating + * another user. Reads the act_* claims from the live access token via + * AuthContext.impersonation. + * + * Two palettes: + * - same-tenant impersonation → amber, "Impersonating " + * - cross-tenant impersonation → red/destructive, "CROSS-TENANT + * IMPERSONATION · {actorTenant} → {tenant}". Cross-tenant means a + * SuperAdmin acting *as* a member of a different tenant — much + * larger blast radius, deserves a louder warning. + */ +export function ImpersonationBanner() { + const { impersonation, user, stopImpersonation } = useAuth(); + const navigate = useNavigate(); + const [pending, setPending] = useState(false); + + const stop = useMutation({ + mutationFn: async () => { + setPending(true); + try { + await stopImpersonation(); + } finally { + setPending(false); + } + }, + onSuccess: () => { + toast.success("Returned to your session"); + navigate("/", { replace: true }); + }, + onError: (err) => { + toast.error("Could not end impersonation cleanly", { + description: err instanceof Error ? err.message : "Restored local session.", + }); + navigate("/", { replace: true }); + }, + }); + + if (!impersonation) return null; + + const subjectLabel = user?.name ?? user?.email ?? "Unknown"; + const tenantLabel = user?.tenant ?? "—"; + const actorLabel = impersonation.actorName ?? impersonation.actorUserId.slice(0, 8) + "…"; + const actorTenantLabel = impersonation.actorTenant ?? "—"; + const isCrossTenant = + impersonation.actorTenant !== undefined && impersonation.actorTenant !== user?.tenant; + + // Token name flips between two CSS variables so every styled child can use + // the same expression. Cross-tenant escalates to --color-destructive, + // same-tenant stays on --color-warning. + const tone = isCrossTenant ? "var(--color-destructive)" : "var(--color-warning)"; + + return ( +
    + {/* Diagonal stripe pattern — pulses harder for cross-tenant */} +
    + +
    + + + + + {isCrossTenant ? ( + <> + + Cross-tenant impersonation + + + + + + acting as {subjectLabel} + + + · operator {actorLabel} + + + ) : ( + <> + + Impersonating + + + {subjectLabel} + + + + · acting as your operator {actorLabel} + + + )} +
    + + +
    + ); +} + +function TenantChip({ + label, + tone, + mono, + emphasis, +}: { + label: string; + tone: string; + mono?: boolean; + emphasis?: boolean; +}) { + return ( + + {label} + + ); +} diff --git a/clients/dashboard/src/components/layout/sidebar.tsx b/clients/dashboard/src/components/layout/sidebar.tsx new file mode 100644 index 0000000000..99fedd2fa0 --- /dev/null +++ b/clients/dashboard/src/components/layout/sidebar.tsx @@ -0,0 +1,506 @@ +import { useCallback, useEffect, useState } from "react"; +import { NavLink, useLocation } from "react-router-dom"; +import { + Activity, + ChevronDown, + FolderTree, + HeartPulse, + LayoutDashboard, + Package, + PanelLeftClose, + PanelLeftOpen, + Receipt, + ScrollText, + Settings, + ShieldCheck, + Tags, + Ticket, + Trash2, + Users, + UsersRound, + Wifi, +} from "lucide-react"; +import { cn } from "@/lib/cn"; + +const COLLAPSED_KEY = "fsh.sidebar.collapsed"; + +type NavSpec = { + to: string; + label: string; + icon: React.ComponentType<{ className?: string }>; +}; + +type NavSection = { + id: string; + caption: string; + /** Section-level icon used as a fallback when the sidebar is + * collapsed and the section is rendered as a stack of item icons. */ + icon: React.ComponentType<{ className?: string }>; + items: NavSpec[]; +}; + +// Top-level items live OUTSIDE any section. Overview opens the app; +// Settings is account-scoped and lives at the very bottom. +const topNavTop: NavSpec[] = [ + { to: "/", label: "Overview", icon: LayoutDashboard }, +]; + +const topNavBottom: NavSpec[] = [ + { to: "/settings", label: "Settings", icon: Settings }, +]; + +// Section accordion. Single-select — only one section open at a time. +const sections: NavSection[] = [ + { + id: "operations", + caption: "Operations", + icon: Activity, + items: [ + { to: "/activity", label: "Live activity", icon: Activity }, + { to: "/invoices", label: "Invoices", icon: Receipt }, + ], + }, + { + id: "catalog", + caption: "Catalog", + icon: Package, + items: [ + { to: "/catalog/products", label: "Products", icon: Package }, + { to: "/catalog/brands", label: "Brands", icon: Tags }, + { to: "/catalog/categories", label: "Categories", icon: FolderTree }, + ], + }, + { + id: "helpdesk", + caption: "Helpdesk", + icon: Ticket, + items: [ + { to: "/tickets", label: "Tickets", icon: Ticket }, + ], + }, + { + id: "identity", + caption: "Identity", + icon: Users, + items: [ + { to: "/identity/users", label: "Users", icon: Users }, + { to: "/identity/roles", label: "Roles", icon: ShieldCheck }, + { to: "/identity/groups", label: "Groups", icon: UsersRound }, + ], + }, + { + id: "system", + caption: "System", + icon: HeartPulse, + items: [ + { to: "/system/health", label: "Health", icon: HeartPulse }, + { to: "/system/audits", label: "Audit trail", icon: ScrollText }, + { to: "/system/sessions", label: "Sessions", icon: Wifi }, + { to: "/system/trash", label: "Trash", icon: Trash2 }, + ], + }, +]; + +/** Persisted collapsed state. Reads localStorage on mount; writes on change. */ +function useCollapsedSidebar() { + const [collapsed, setRaw] = useState(() => { + if (typeof window === "undefined") return false; + try { + return window.localStorage.getItem(COLLAPSED_KEY) === "true"; + } catch { + return false; + } + }); + const setCollapsed = useCallback((next: boolean) => { + setRaw(next); + try { + window.localStorage.setItem(COLLAPSED_KEY, String(next)); + } catch { + /* storage unavailable */ + } + }, []); + return { + collapsed, + toggle: () => setCollapsed(!collapsed), + }; +} + +/** Find the section whose items contain the given path (best prefix match). */ +function findSectionForPath(pathname: string): string | null { + let bestId: string | null = null; + let bestLen = 0; + for (const s of sections) { + for (const item of s.items) { + if ( + (item.to === "/" && pathname === "/") || + (item.to !== "/" && pathname.startsWith(item.to)) + ) { + if (item.to.length > bestLen) { + bestLen = item.to.length; + bestId = s.id; + } + } + } + } + return bestId; +} + +export function Sidebar() { + const { collapsed, toggle } = useCollapsedSidebar(); + const location = useLocation(); + + // Single-select accordion: which section is currently open. Defaults + // to the section that owns the current route. Manual clicks override + // until the user navigates again. + const [openSection, setOpenSection] = useState(() => + findSectionForPath(location.pathname), + ); + + // On every route change, re-sync the expanded section to the route's + // owning section. This re-opens the right section when the user + // navigates via the command palette, browser back/forward, or any + // link outside the sidebar. + useEffect(() => { + const next = findSectionForPath(location.pathname); + if (next !== null) { + setOpenSection(next); + } + // If the route is a top-level page (Overview / Settings), close the + // accordion entirely so no section is highlighted. + else { + setOpenSection(null); + } + }, [location.pathname]); + + return ( + + ); +} + +// ──────────────────────────────────────────────────────────────────────── +// Accordion section (expanded sidebar only). +// +// Closed: a flat hover-able row showing the section icon + caption + a +// small chevron-down on the right. +// +// Open: wrapped in a tone-aware card surface so the active section +// reads as the focal column of the nav. Caption sits up top, items +// below with 2px brand bar on the active item. +// ──────────────────────────────────────────────────────────────────────── + +function AccordionSection({ + section, + isOpen, + onToggle, +}: { + section: NavSection; + isOpen: boolean; + onToggle: () => void; +}) { + const SectionIcon = section.icon; + return ( +
    + {/* Section header. Structured to mirror NavItemLink — same height, + same gap, same padding, same icon size, same text-sm/medium — + so the sidebar reads as one consistent typographic system. + Differentiation from a nav item is carried by the trailing + chevron + the card-surface treatment when this section is + active, not by font/size shifts. */} + + + {/* Items panel — animated open/close via the grid-template-rows + 0fr ↔ 1fr trick. The wrapper is a CSS grid with a single + implicit row whose track size animates between 0fr (closed, + panel collapses) and 1fr (open, panel takes its natural + height). The inner div needs `overflow: hidden` + `min-h-0` + so the contents are clipped during the transition rather + than overflowing into the next section. Items also fade in + with a tiny delay so the slide and the visual reveal stay + in lockstep. */} +
    +
    +
    + {section.items.map((item) => ( + + ))} +
    +
    +
    +
    + ); +} + +// ──────────────────────────────────────────────────────────────────────── +// Nav item link. Reused for top-level items (Overview, Settings), +// inside an open accordion (indent=true), and inside the collapsed +// sidebar's flat icon stack. +// ──────────────────────────────────────────────────────────────────────── + +function NavItemLink({ + item, + collapsed, + indent, +}: { + item: NavSpec; + collapsed: boolean; + /** Adds a small left padding so accordion items align under the + * section caption with breathing room. Not applied to top-level. */ + indent: boolean; +}) { + const Icon = item.icon; + return ( + + cn( + "group/nav relative flex h-9 items-center gap-3 rounded-md text-sm font-medium", + "transition-colors duration-[var(--duration-fast)] ease-[var(--ease-out-cubic)]", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-ring)]", + isActive + ? "bg-[var(--color-primary-soft)] text-[var(--color-primary)]" + : "text-[var(--color-muted-foreground)] hover:bg-[var(--color-accent)] hover:text-[var(--color-foreground)]", + collapsed ? "justify-center px-0" : indent ? "px-3" : "px-3", + ) + } + > + {({ isActive }) => ( + <> + {/* 2px brand bar slides in for the active item. */} + + + + + {!collapsed && ( + {item.label} + )} + + {/* Hover tooltip — only rendered in collapsed mode. */} + {collapsed && ( + + {item.label} + + )} + + )} + + ); +} diff --git a/clients/dashboard/src/components/layout/topbar.tsx b/clients/dashboard/src/components/layout/topbar.tsx new file mode 100644 index 0000000000..43756b8951 --- /dev/null +++ b/clients/dashboard/src/components/layout/topbar.tsx @@ -0,0 +1,475 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { + ChevronDown, + ChevronRight, + KeyRound, + LogOut, + Monitor, + Moon, + Search, + Settings as SettingsIcon, + Sun, + UserRound, +} from "lucide-react"; +import { useCommandPalette } from "@/components/command-palette/command-palette"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogBody, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Avatar } from "@/components/ui/avatar"; +import { useAuth } from "@/auth/use-auth"; +import { useSse } from "@/sse/sse-context"; +import { useTheme, type ThemeMode } from "@/components/theme/theme-provider"; +import { cn } from "@/lib/cn"; + +// ───────────────────────────────────────────────────────────────────── +// Theme tile — full card with icon + label + miniature preview swatch. +// Used inside the dropdown's Theme row. +// ───────────────────────────────────────────────────────────────────── + +type ThemeTileSpec = { + value: ThemeMode; + label: string; + Icon: React.ComponentType<{ className?: string }>; + /** Inline preview swatch styling — tiny mock of light / split / dark. */ + preview: React.CSSProperties; +}; + +const themeTiles: ThemeTileSpec[] = [ + { + value: "light", + label: "Light", + Icon: Sun, + preview: { + background: + "linear-gradient(135deg, oklch(0.985 0.002 270), oklch(0.928 0.006 270))", + }, + }, + { + value: "system", + label: "System", + Icon: Monitor, + preview: { + background: + "linear-gradient(135deg, oklch(0.985 0.002 270) 0%, oklch(0.985 0.002 270) 49%, oklch(0.165 0.011 270) 51%, oklch(0.165 0.011 270) 100%)", + }, + }, + { + value: "dark", + label: "Dark", + Icon: Moon, + preview: { + background: + "linear-gradient(135deg, oklch(0.165 0.011 270), oklch(0.080 0.009 270))", + }, + }, +]; + +function ThemeTile({ + spec, + active, + onClick, +}: { + spec: ThemeTileSpec; + active: boolean; + onClick: () => void; +}) { + const { Icon, label } = spec; + return ( + + ); +} + +// ───────────────────────────────────────────────────────────────────── +// Quick-action item — icon in a brand-soft chip + label + chevron. +// Reads as a command, not a passive list row. +// ───────────────────────────────────────────────────────────────────── + +function QuickAction({ + icon: Icon, + label, + description, + onSelect, +}: { + icon: React.ComponentType<{ className?: string }>; + label: string; + description?: string; + onSelect: () => void; +}) { + return ( + + + + + + {label} + {description && ( + + {description} + + )} + + + + ); +} + +// ───────────────────────────────────────────────────────────────────── +// Topbar +// ───────────────────────────────────────────────────────────────────── + +export function Topbar() { + const { user, logout } = useAuth(); + const { status: sseStatus, eventCount } = useSse(); + const { mode, setMode } = useTheme(); + const { setOpen: setPaletteOpen } = useCommandPalette(); + const navigate = useNavigate(); + const [confirmOpen, setConfirmOpen] = useState(false); + + const onConfirmSignOut = () => { + setConfirmOpen(false); + logout(); + }; + + const avatarStatus = + sseStatus === "connected" + ? ("online" as const) + : sseStatus === "error" + ? ("warning" as const) + : sseStatus === "idle" + ? undefined + : ("offline" as const); + + const presence = (() => { + if (sseStatus === "connected") { + return { + color: "var(--color-success)", + text: `Connected · ${new Intl.NumberFormat("en-US").format(eventCount)} events`, + }; + } + if (sseStatus === "error") { + return { color: "var(--color-destructive)", text: "Stream offline" }; + } + if (sseStatus === "connecting") { + return { color: "var(--color-muted-foreground)", text: "Connecting…" }; + } + if (sseStatus === "reconnecting") { + return { color: "var(--color-warning)", text: "Reconnecting…" }; + } + return { color: "var(--color-muted-foreground)", text: "Idle" }; + })(); + + return ( +
    + {/* Command palette trigger — opens via ⌘K from anywhere; the + chip in the topbar is a discoverability affordance. */} + + + {/* `modal={false}` is required because we open the sign-out + confirmation Dialog from a DropdownMenuItem. Default modal mode + locks pointer-events on the body, which can leave the page + unclickable after the dialog closes due to a Radix lifecycle + race between the two overlays. Non-modal still auto-closes on + outside click and keeps keyboard navigation. */} + + + + + + + {/* ── Hero ──────────────────────────────────────────────── + Atmospheric panel: brand-tinted radial mesh + soft inset + highlight. Avatar floats in halo. Status row anchors at + the bottom. */} +
    + {/* Brand glow — corner radial + diagonal counter-tint. */} +
    +
    +
    + +
    +
    + {user?.name ?? user?.email ?? "Unknown"} +
    + {user?.email && user.name && ( +
    + {user.email} +
    + )} +
    + + Tenant + + + {user?.tenant ?? "—"} + +
    +
    +
    + + {/* Live presence row — pulse dot in current status hue, + mono "Connected · N events" line. */} +
    + + + {presence.text} + +
    +
    +
    + + + + {/* ── Theme — three preview tiles ───────────────────────── */} + Theme +
    + {themeTiles.map((spec) => ( + setMode(spec.value)} + /> + ))} +
    + + + + {/* ── Quick actions ─────────────────────────────────────── */} + Account +
    + navigate("/settings/profile")} + /> + navigate("/settings")} + /> + navigate("/settings/api-keys")} + /> +
    + + + + {/* ── Sign out (destructive) ────────────────────────────── */} +
    + setConfirmOpen(true)} + className="!my-0 rounded-lg !px-2.5 !py-2" + > + + + + Sign out + + ⌘⇧Q + + +
    + + {/* ── Footer ────────────────────────────────────────────── */} +
    +

    + v0.1 · console +

    + + fullstackhero.net ↗ + +
    + + + + {/* Sign-out confirmation dialog. */} + + + + Sign out of FullStackHero? + + You'll need to sign in again to access this tenant. Any unsaved + work in this session will be lost. + + + +
    + +
    +
    + {user?.name ?? user?.email ?? "Unknown"} +
    + {user?.email && user.name && ( +
    + {user.email} +
    + )} +
    + + {user?.tenant ?? "—"} + +
    +
    + + + + +
    +
    +
    + ); +} diff --git a/clients/dashboard/src/components/list/combobox.tsx b/clients/dashboard/src/components/list/combobox.tsx new file mode 100644 index 0000000000..74df301397 --- /dev/null +++ b/clients/dashboard/src/components/list/combobox.tsx @@ -0,0 +1,395 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { Check, ChevronDown, Search, X } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRow, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { cn } from "@/lib/cn"; + +export type ComboboxOption = { + value: string; + label: string; + /** Optional left-of-label glyph (depth indent for trees, brand swatch, etc). */ + prefix?: React.ReactNode; + /** Optional right-of-label muted hint (counts, slug, etc). */ + hint?: string; +}; + +type Variant = "filter" | "field"; + +/** + * Combobox — popover-driven select that matches the dashboard's polished + * design vocabulary. Replaces native setFilter(e.target.value)} + placeholder={`Filter ${label.toLowerCase()}…`} + // Stop Radix's typeahead from swallowing the user's input. + onKeyDown={(e) => { + if (e.key !== "Escape") e.stopPropagation(); + }} + className={cn( + "h-6 w-full bg-transparent text-sm", + "placeholder:text-[var(--color-muted-foreground)]/70", + // The popover row is already a contained visual context — skip + // the global :focus-visible halo so it doesn't draw a hard + // rectangle + 6px outer bloom around the search input. + "outline-none focus:outline-none focus-visible:outline-none focus-visible:shadow-none", + )} + /> + {filter && ( + + )} + + )} + +
      + {emptyOptionLabel && (!filter || emptyOptionLabel.toLowerCase().includes(filter.toLowerCase())) && ( + + )} + + {filtered.length === 0 ? ( +
    • + No matches. +
    • + ) : ( + filtered.map((opt) => ( + + )) + )} +
    + + + ); +} + +function Option({ + selected, + onPick, + children, +}: { + selected: boolean; + onPick: () => void; + children: React.ReactNode; +}) { + return ( +
  • + +
  • + ); +} + +// ─── Triggers ───────────────────────────────────────────────────────── + +const FilterTrigger = ({ + label, + selected, + hasValue, + clearable, + onClear, + disabled, + className, + ...props +}: { + label: string; + selected: ComboboxOption | null; + hasValue: boolean; + clearable: boolean; + onClear: () => void; + disabled?: boolean; + className?: string; +} & React.ButtonHTMLAttributes) => { + return ( + + + {clearable && hasValue && ( + + )} + + ); +}; + +const FieldTrigger = ({ + id, + placeholder, + selected, + hasValue, + clearable, + onClear, + disabled, + required, + className, + ...props +}: { + id?: string; + placeholder: string; + selected: ComboboxOption | null; + hasValue: boolean; + clearable: boolean; + onClear: () => void; + disabled?: boolean; + required?: boolean; + className?: string; +} & React.ButtonHTMLAttributes) => { + return ( + + ); +}; diff --git a/clients/dashboard/src/components/list/density-toggle.tsx b/clients/dashboard/src/components/list/density-toggle.tsx new file mode 100644 index 0000000000..8fc7efd323 --- /dev/null +++ b/clients/dashboard/src/components/list/density-toggle.tsx @@ -0,0 +1,90 @@ +import { Rows3, Rows4 } from "lucide-react"; +import { cn } from "@/lib/cn"; + +export type Density = "cozy" | "compact"; + +/** + * Segmented density toggle. Consumers persist the value to localStorage + * themselves (key per page) and pass it back via `density` / `onChange`. + */ +export function DensityToggle({ + density, + onChange, +}: { + density: Density; + onChange: (d: Density) => void; +}) { + return ( +
    + onChange("cozy")} + label="Cozy rows" + > + + + onChange("compact")} + label="Compact rows" + > + + +
    + ); +} + +function DensityButton({ + active, + onClick, + label, + children, +}: { + active: boolean; + onClick: () => void; + label: string; + children: React.ReactNode; +}) { + return ( + + ); +} + +/** + * Hook that wires a `localStorage`-backed density toggle to a key. + * Page passes the key, gets back `[density, setDensity]`. + */ +import { useEffect, useState } from "react"; + +export function usePersistedDensity(storageKey: string): [Density, (d: Density) => void] { + const [density, setDensity] = useState(() => { + if (typeof window === "undefined") return "cozy"; + return (localStorage.getItem(storageKey) as Density | null) ?? "cozy"; + }); + useEffect(() => { + try { + localStorage.setItem(storageKey, density); + } catch { + /* storage unavailable */ + } + }, [storageKey, density]); + return [density, setDensity]; +} diff --git a/clients/dashboard/src/components/list/empty-state.tsx b/clients/dashboard/src/components/list/empty-state.tsx new file mode 100644 index 0000000000..8b0365f23d --- /dev/null +++ b/clients/dashboard/src/components/list/empty-state.tsx @@ -0,0 +1,171 @@ +import type { ReactNode } from "react"; +import { Sparkles } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/cn"; + +type Action = { + label: string; + onClick: () => void; + icon?: ReactNode; +}; + +/** + * EmptyState — the "plinth" treatment for zero-result list pages. + * + * Visual concept: a museum/showroom plinth where the missing item would sit. + * A slow conic halo orbits behind the icon (like a slow-pacing spotlight), + * a soft floor gradient rises from below the plinth, and a single cast + * shadow grounds the icon. Designed to match the rest of the dashboard's + * atmospheric vocabulary — gradient-border surfaces, mono-caps eyebrows, + * display headlines. + * + * Two action slots: a primary CTA (always shown — usually "create the first + * X" or "add a new X") and an optional secondary (clear filters / search). + */ +export function EmptyState({ + eyebrow, + headline, + body, + icon, + primaryAction, + secondaryAction, +}: { + eyebrow: string; + headline: ReactNode; + body: ReactNode; + icon: ReactNode; + primaryAction: Action; + secondaryAction?: Action; +}) { + return ( +
    + {/* Floor — soft radial that rises from beneath the plinth, suggesting + a vanishing horizon. Layered above the cast shadow so the shadow + reads as depth, not haze. */} +
    + {/* Grain — barely-there texture so the gradient doesn't band. */} +
    \")", + }} + /> + +
    + {/* Plinth — icon with orbiting halo + cast shadow */} + {icon} + + {/* Eyebrow */} + + {eyebrow} + + + {/* Display headline — generous max-width keeps two-line wraps from + looking awkward; tracking tightens for that quiet, confident feel */} +

    + {headline} +

    + +

    + {body} +

    + +
    + {secondaryAction && ( + + )} + +
    +
    +
    + ); +} + +/** + * Plinth — the visual anchor. Three concentric layers from back to front: + * + * 1. Cast shadow — a soft elliptical radial gradient beneath, grounding + * the icon visually so it doesn't float like a sticker. + * 2. Conic halo — a slow-rotating dual-stop conic gradient masked into + * a ring. Reads as a single bright wedge orbiting the icon over 18s. + * 3. Icon plate — gradient-bordered, soft brand-tinted fill, on top. + */ +function Plinth({ children }: { children: ReactNode }) { + return ( +
    + {/* Cast shadow */} + + + {/* Orbiting halo — masked conic ring */} + + + {/* Static base ring for a constant outline so the orbit reads as a + highlight rather than the only chrome */} + + + {/* Icon plate */} + + {children} + +
    + ); +} diff --git a/clients/dashboard/src/components/list/error-band.tsx b/clients/dashboard/src/components/list/error-band.tsx new file mode 100644 index 0000000000..ca8cb65595 --- /dev/null +++ b/clients/dashboard/src/components/list/error-band.tsx @@ -0,0 +1,15 @@ +/** + * Inline error band displayed between the toolbar and the list when a + * query has failed. Picks up the destructive token + the mono-caps + * "failure ·" eyebrow used elsewhere in the dashboard. + */ +export function ErrorBand({ message }: { message: string }) { + return ( +
    + + failure ·{" "} + + {message} +
    + ); +} diff --git a/clients/dashboard/src/components/list/field.tsx b/clients/dashboard/src/components/list/field.tsx new file mode 100644 index 0000000000..438dc79902 --- /dev/null +++ b/clients/dashboard/src/components/list/field.tsx @@ -0,0 +1,38 @@ +import { Label } from "@/components/ui/label"; + +/** + * Form field wrapper used across editor dialogs. Mono-caps tracked + * label, optional `*` hint that's a primary-toned middle dot, and a + * faint hint line below the control. + */ +export function Field({ + id, + label, + hint, + required, + children, +}: { + id: string; + label: string; + hint?: string; + required?: boolean; + children: React.ReactNode; +}) { + return ( +
    + + {children} + {hint && ( +

    + {hint} +

    + )} +
    + ); +} diff --git a/clients/dashboard/src/components/list/index.ts b/clients/dashboard/src/components/list/index.ts new file mode 100644 index 0000000000..7b8beab246 --- /dev/null +++ b/clients/dashboard/src/components/list/index.ts @@ -0,0 +1,14 @@ +export { ListHero } from "./list-hero"; +export { PageHero } from "./page-hero"; +export { Stat, StatStrip, type StatTone } from "./stat"; +export { SortChips, type SortDir, type SortOption } from "./sort-chips"; +export { + DensityToggle, + usePersistedDensity, + type Density, +} from "./density-toggle"; +export { Pagination } from "./pagination"; +export { Field } from "./field"; +export { ErrorBand } from "./error-band"; +export { Combobox, type ComboboxOption } from "./combobox"; +export { EmptyState } from "./empty-state"; diff --git a/clients/dashboard/src/components/list/list-hero.tsx b/clients/dashboard/src/components/list/list-hero.tsx new file mode 100644 index 0000000000..bfb7a9dcfa --- /dev/null +++ b/clients/dashboard/src/components/list/list-hero.tsx @@ -0,0 +1,202 @@ +import { + Plus, + RefreshCw, + Search, + X, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/cn"; +import { pad2 } from "@/lib/list-helpers"; + +/** + * Atmospheric hero band shared by every list page in the dashboard + * (Brands · Categories · Products · …). Wraps the page in a layered + * radial-gradient + grain backdrop, surfaces the count as a tinted + * inline number, and bakes the search bar into the same surface. + * + * Composes top-down: + * eyebrow row — section · area · tenant · sub-eyebrow + * title row — display heading + count + subtitle + actions + * search row — glassy search field + */ +export function ListHero({ + eyebrow, + tenant, + subEyebrow, + title, + totalCount, + subtitle, + searchValue, + onSearch, + searchPlaceholder = "Find an item by name…", + isFetching, + onRefresh, + ctaLabel = "New", + onCreate, +}: { + eyebrow: string; + tenant: string; + subEyebrow?: string; + title: string; + totalCount: number | null; + subtitle: string; + searchValue: string; + onSearch: (v: string) => void; + searchPlaceholder?: string; + isFetching: boolean; + onRefresh: () => void; + ctaLabel?: string; + onCreate: () => void; +}) { + return ( +
    +
    +
    \")", + }} + /> + +
    +
    + + {eyebrow} + + + + {tenant} + + {subEyebrow && ( + <> + + + {subEyebrow} + + + )} +
    + +
    +
    + {/* leading-[1.1] + pb-1 leave room for descenders. With the + previous 1.02, titles containing g/y/p/q (e.g. anything + ending in "Settings" or "Pricing") had their bottom hairs + clipped by the section's outer `overflow-hidden`. The + existing catalog titles ("Brands", "Products", + "Categories") have no descenders so the bug stayed + dormant. */} +

    + {title} + + {totalCount === null ? "—" : pad2(totalCount)} + +

    +

    + {subtitle} +

    +
    + +
    + + +
    +
    + +
    + +
    +
    +
    + ); +} + +function HeroSearch({ + value, + onChange, + placeholder, +}: { + value: string; + onChange: (v: string) => void; + placeholder: string; +}) { + return ( +
    + + onChange(e.target.value)} + placeholder={placeholder} + aria-label={placeholder} + className={cn( + "h-full flex-1 border-0 bg-transparent pl-3 pr-2 text-[15px]", + "focus-visible:ring-0 focus-visible:ring-offset-0", + "shadow-none placeholder:text-[var(--color-muted-foreground)]/80", + )} + /> + {value && ( + + )} +
    + ); +} diff --git a/clients/dashboard/src/components/list/page-hero.tsx b/clients/dashboard/src/components/list/page-hero.tsx new file mode 100644 index 0000000000..6f30d1f9eb --- /dev/null +++ b/clients/dashboard/src/components/list/page-hero.tsx @@ -0,0 +1,126 @@ +import type { ReactNode } from "react"; +import { cn } from "@/lib/cn"; + +/** + * Atmospheric page hero used across non-list surfaces (health, audit + * trail, sessions, trash, settings). Shares the same chrome as + * `ListHero` — radial brand wash + soft noise overlay — but omits the + * built-in search input and CTA button. Callers slot in their own + * actions on the right. + * + * Composition: + * eyebrow row — section · area · tenant · sub-eyebrow + * title row — display heading + optional badge + actions slot + * subtitle — muted body line below + */ +export function PageHero({ + eyebrow, + tenant, + subEyebrow, + title, + badge, + subtitle, + actions, + className, +}: { + eyebrow: string; + tenant?: string; + subEyebrow?: string; + title: string; + /** Optional after-title element — pill, count, status indicator. */ + badge?: ReactNode; + /** Muted body line under the title. Plain string or rich React. */ + subtitle?: ReactNode; + /** Right-aligned actions (buttons, refresh, etc.). */ + actions?: ReactNode; + className?: string; +}) { + return ( +
    + {/* Brand wash — three soft radial gradients. Same recipe as + ListHero so the two visually unify. */} +
    +
    \")", + }} + /> + +
    + {/* Eyebrow row */} +
    + + {eyebrow} + + {tenant && ( + <> + + + {tenant} + + + )} + {subEyebrow && ( + <> + + + {subEyebrow} + + + )} +
    + + {/* Title + actions */} +
    +
    + {/* leading-[1.12] (was 1.04) + pb-1 leave room for descenders. + Without the extra height, glyphs like g/y/p/q hang past + the line-box and get clipped by the section's outer + `overflow-hidden` (which we keep so the radial gradients + don't bleed past the rounded corners). + The title is rendered directly rather than wrapped in a + `.truncate` span — that wrapper added its own + `overflow: hidden` clipping the descender at a second + layer. Page-hero titles are short ("Settings", + "Audit trail", "Health"), so unconstrained wrapping is + fine. */} +

    + {title} + {badge} +

    + {subtitle && ( +

    + {subtitle} +

    + )} +
    + + {actions && ( +
    + {actions} +
    + )} +
    +
    +
    + ); +} diff --git a/clients/dashboard/src/components/list/pagination.tsx b/clients/dashboard/src/components/list/pagination.tsx new file mode 100644 index 0000000000..1878a488df --- /dev/null +++ b/clients/dashboard/src/components/list/pagination.tsx @@ -0,0 +1,44 @@ +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +/** + * Pagination footer used at the bottom of every list page. + * Mono-caps "Showing N of T · folio P / TP" + Prev/Next. + */ +export function Pagination({ + page, + totalPages, + totalCount, + shown, + fetching, + hasPrev, + hasNext, + onPrev, + onNext, +}: { + page: number; + totalPages: number; + totalCount: number; + shown: number; + fetching: boolean; + hasPrev: boolean; + hasNext: boolean; + onPrev: () => void; + onNext: () => void; +}) { + return ( +
    + + Showing {shown} of {totalCount} · folio {page} / {totalPages} + +
    + + +
    +
    + ); +} diff --git a/clients/dashboard/src/components/list/sort-chips.tsx b/clients/dashboard/src/components/list/sort-chips.tsx new file mode 100644 index 0000000000..cef61e5445 --- /dev/null +++ b/clients/dashboard/src/components/list/sort-chips.tsx @@ -0,0 +1,85 @@ +import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react"; +import { cn } from "@/lib/cn"; + +export type SortDir = "asc" | "desc"; + +export type SortOption = { key: K; label: string }; + +/** + * Pill-rail of sort chips used as the primary sort control on list + * pages. Replaces traditional sortable column headers with a more modern + * "sort by [chip] [chip] [chip]" presentation. + */ +export function SortChips({ + options, + sortKey, + sortDir, + onSort, + intro = "sort by", + prefixLabel, +}: { + options: SortOption[]; + sortKey: K; + sortDir: SortDir; + onSort: (k: K) => void; + intro?: string; + /** Optional left-of-chips eyebrow, e.g. "results" / "registry". */ + prefixLabel?: string; +}) { + return ( +
    + {prefixLabel && ( + <> + + {prefixLabel} + + + + )} + + {intro} + + {options.map((opt) => ( + onSort(opt.key)} + /> + ))} +
    + ); +} + +function SortChip({ + label, + active, + dir, + onClick, +}: { + label: string; + active: boolean; + dir: SortDir | null; + onClick: () => void; +}) { + const Icon = !active ? ArrowUpDown : dir === "asc" ? ArrowUp : ArrowDown; + return ( + + ); +} diff --git a/clients/dashboard/src/components/list/stat.tsx b/clients/dashboard/src/components/list/stat.tsx new file mode 100644 index 0000000000..858bf1a62c --- /dev/null +++ b/clients/dashboard/src/components/list/stat.tsx @@ -0,0 +1,77 @@ +import { cn } from "@/lib/cn"; + +export type StatTone = "default" | "warning" | "danger"; + +/** + * Atomic stat card used by every list-page stat strip. Mono-caps label, + * display-weight value, prose hint. Tone shifts the value color. + */ +export function Stat({ + label, + value, + hint, + accent, + tone = "default", +}: { + label: string; + value: React.ReactNode; + hint: string; + accent?: boolean; + tone?: StatTone; +}) { + // Uniform height: every Stat in a strip lays out as + // [label · 1 line] / [value · fixed leading] / [hint · 1 line truncated], + // and the outer container fills any remaining grid-row height via + // `h-full`. The result is a perfectly aligned strip even when the + // parent grid stretches one tile taller than its content. + return ( +
    +
    + {label} +
    +
    + {value} +
    +
    + {hint} +
    +
    + ); +} + +/** + * Layout shell — `fsh-enter fsh-enter-2` stagger + responsive grid. + * Pages compose any number of children inside. + */ +export function StatStrip({ + cols = 3, + children, +}: { + cols?: 2 | 3 | 4; + children: React.ReactNode; +}) { + return ( +
    + {children} +
    + ); +} diff --git a/clients/dashboard/src/components/route-error.tsx b/clients/dashboard/src/components/route-error.tsx new file mode 100644 index 0000000000..8a18ea721a --- /dev/null +++ b/clients/dashboard/src/components/route-error.tsx @@ -0,0 +1,50 @@ +import { isRouteErrorResponse, useNavigate, useRouteError } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; + +/** + * Route-level error element. React Router v7 passes any error thrown during rendering, + * loading, or an action to the nearest . Without one, a render + * error white-screens the whole app — this replaces that with a recoverable view. + */ +export function RouteError() { + const error = useRouteError(); + const navigate = useNavigate(); + + const { title, detail } = describe(error); + + return ( +
    + + + Something went wrong + {title} + + + {detail && ( +
    +              {detail}
    +            
    + )} +
    + + + + +
    +
    + ); +} + +function describe(error: unknown): { title: string; detail?: string } { + if (isRouteErrorResponse(error)) { + return { + title: `${error.status} ${error.statusText}`, + detail: typeof error.data === "string" ? error.data : JSON.stringify(error.data, null, 2), + }; + } + if (error instanceof Error) { + return { title: error.message, detail: error.stack }; + } + return { title: "Unexpected error", detail: String(error) }; +} diff --git a/clients/dashboard/src/components/sse/live-feed.tsx b/clients/dashboard/src/components/sse/live-feed.tsx new file mode 100644 index 0000000000..fe03825b4a --- /dev/null +++ b/clients/dashboard/src/components/sse/live-feed.tsx @@ -0,0 +1,135 @@ +import { useMemo } from "react"; +import { Activity } from "lucide-react"; +import { useSse, type SseEvent } from "@/sse/sse-context"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/cn"; + +const timeFmt = new Intl.DateTimeFormat("en-US", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, +}); + +function formatTime(ts: number) { + return timeFmt.format(new Date(ts)); +} + +function formatPayload(data: unknown, raw: string): string { + if (typeof data === "string") return data; + try { + return JSON.stringify(data, null, 2); + } catch { + return raw; + } +} + +/** + * Heuristic for a status tone given the event type. Generic verbs map + * to specific tones so the feed reads at-a-glance: failures pop red, + * successes pop green, everything else stays neutral. + */ +function eventTone(type: string): "default" | "success" | "warning" | "danger" | "brand" { + const t = type.toLowerCase(); + if (t.includes("fail") || t.includes("error") || t.includes("revoke")) return "danger"; + if (t.includes("warn") || t.includes("retry")) return "warning"; + if (t.includes("login") || t.includes("issued") || t.includes("created")) return "success"; + if (t.includes("token") || t.includes("auth")) return "brand"; + return "default"; +} + +function FeedRow({ ev }: { ev: SseEvent }) { + const tone = useMemo(() => eventTone(ev.type), [ev.type]); + return ( +
  • +
    + {formatTime(ev.receivedAt)} +
    +
    + {ev.type} +
    +
    +
    +          {formatPayload(ev.data, ev.rawData)}
    +        
    +
    +
  • + ); +} + +export function LiveFeed({ limit = 25 }: { limit?: number }) { + const { events, status, eventCount } = useSse(); + const visible = useMemo(() => events.slice(0, limit), [events, limit]); + const isLive = status === "connected"; + + return ( + + +
    +
    + + Live activity + {isLive ? ( + streaming + ) : status === "error" ? ( + offline + ) : ( + {status} + )} + + + Real-time backend events delivered over Server-Sent Events. + +
    +
    + + total + + + {new Intl.NumberFormat("en-US").format(eventCount)} + +
    +
    +
    + + {visible.length === 0 ? ( +
    + + + +
    + {isLive ? "Listening for activity" : "No events yet"} +
    +

    + {isLive + ? "The stream is open. Events will appear here as the backend publishes them." + : "The activity stream is not connected. Events will queue once the connection comes online."} +

    +
    + ) : ( +
      + {visible.map((ev) => ( + + ))} +
    + )} +
    +
    + ); +} diff --git a/clients/dashboard/src/components/sse/sse-status-badge.tsx b/clients/dashboard/src/components/sse/sse-status-badge.tsx new file mode 100644 index 0000000000..e3ddfb11e6 --- /dev/null +++ b/clients/dashboard/src/components/sse/sse-status-badge.tsx @@ -0,0 +1,43 @@ +import { useSse } from "@/sse/sse-context"; +import { cn } from "@/lib/cn"; + +const LABELS = { + idle: "Idle", + connecting: "Connecting…", + connected: "Live", + reconnecting: "Reconnecting…", + error: "Offline", +} as const; + +/** + * SSE indicator pill. When connected the dot breathes via the + * `pulse-dot` utility (currentColor-driven, so the glow inherits the + * dot's status hue). All colors come from semantic tokens — no literals. + */ +export function SseStatusBadge() { + const { status, eventCount } = useSse(); + const isLive = status === "connected"; + const isError = status === "error"; + + const dotColor = isLive + ? "var(--color-success)" + : isError + ? "var(--color-destructive)" + : "var(--color-muted-foreground)"; + + return ( +
    + + {LABELS[status]} + {isLive && ( + + · {eventCount} + + )} +
    + ); +} diff --git a/clients/dashboard/src/components/theme/appearance-options.ts b/clients/dashboard/src/components/theme/appearance-options.ts new file mode 100644 index 0000000000..3f546f8524 --- /dev/null +++ b/clients/dashboard/src/components/theme/appearance-options.ts @@ -0,0 +1,174 @@ +/** + * Selectable appearance options — fonts and accent palettes. + * + * Fonts are applied by setting `--font-sans` on :root. + * Accents are applied by toggling a single `accent-{id}` class on :root; + * the class definitions live in globals.css and override the eleven + * `--brand-*` stops the entire token system reads from. + */ + +export type FontOption = { + id: string; + label: string; + description: string; + /** CSS font-family value applied to --font-sans. */ + family: string; + /** Sample shown on the swatch card. */ + sample?: string; +}; + +const SHARED_FALLBACKS = + "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif"; + +export const fonts: FontOption[] = [ + { + id: "geist", + label: "Geist", + description: "Designed for screens. Default.", + family: `'Geist', ${SHARED_FALLBACKS}`, + }, + { + id: "inter-tight", + label: "Inter Tight", + description: "Tighter modern Inter.", + family: `'Inter Tight', ${SHARED_FALLBACKS}`, + }, + { + id: "dm-sans", + label: "DM Sans", + description: "Geometric, friendly.", + family: `'DM Sans', ${SHARED_FALLBACKS}`, + }, + { + id: "ibm-plex", + label: "IBM Plex Sans", + description: "Editorial grotesque.", + family: `'IBM Plex Sans', ${SHARED_FALLBACKS}`, + }, + { + id: "manrope", + label: "Manrope", + description: "Warm, geometric.", + family: `'Manrope', ${SHARED_FALLBACKS}`, + }, + { + id: "plus-jakarta", + label: "Plus Jakarta Sans", + description: "Modern, lightly geometric.", + family: `'Plus Jakarta Sans', ${SHARED_FALLBACKS}`, + }, + { + id: "outfit", + label: "Outfit", + description: "Confident geometric sans.", + family: `'Outfit', ${SHARED_FALLBACKS}`, + }, + { + id: "sora", + label: "Sora", + description: "Distinctive, contemporary.", + family: `'Sora', ${SHARED_FALLBACKS}`, + }, + { + id: "lexend", + label: "Lexend", + description: "Tuned for reading speed.", + family: `'Lexend', ${SHARED_FALLBACKS}`, + }, + { + id: "figtree", + label: "Figtree", + description: "Friendly, approachable.", + family: `'Figtree', ${SHARED_FALLBACKS}`, + }, + { + id: "onest", + label: "Onest", + description: "Clean, neutral grotesque.", + family: `'Onest', ${SHARED_FALLBACKS}`, + }, + { + id: "roboto-flex", + label: "Roboto Flex", + description: "Google's flagship variable.", + family: `'Roboto Flex', ${SHARED_FALLBACKS}`, + }, +]; + +export const DEFAULT_FONT = "geist"; + +export type AccentOption = { + id: string; + label: string; + description: string; + /** Display-only OKLCH at the 600 stop, used to colour swatch chips. */ + swatch: string; +}; + +export const accents: AccentOption[] = [ + { id: "indigo", label: "Indigo", description: "Confident tech-forward default.", swatch: "oklch(0.555 0.220 268)" }, + { id: "violet", label: "Violet", description: "Saturated, expressive.", swatch: "oklch(0.555 0.220 305)" }, + { id: "sky", label: "Sky", description: "Cool, calm, professional.", swatch: "oklch(0.555 0.220 232)" }, + { id: "emerald", label: "Emerald", description: "Fresh, success-leaning.", swatch: "oklch(0.555 0.220 152)" }, + { id: "amber", label: "Amber", description: "Warm, energetic.", swatch: "oklch(0.620 0.180 76)" }, + { id: "rose", label: "Rose", description: "Bold, attention-getting.", swatch: "oklch(0.555 0.220 12)" }, +]; + +export const DEFAULT_ACCENT = "indigo"; +export const CUSTOM_ACCENT_ID = "custom"; + +export const FONT_STORAGE_KEY = "fsh.font"; +export const ACCENT_STORAGE_KEY = "fsh.accent"; +/** Stores `{ h, c }` for the custom accent — h = hue (0-360), + * c = chroma scale (0.6 → 1.2 of the indigo template). */ +export const CUSTOM_ACCENT_STORAGE_KEY = "fsh.accent.custom"; + +export type DensityMode = "comfortable" | "compact"; +export const DENSITY_STORAGE_KEY = "fsh.density"; +export const DEFAULT_DENSITY: DensityMode = "comfortable"; + +// ──────────────────────────────────────────────────────────────────────── +// Custom accent — derive the eleven --brand-* stops from a single hue. +// +// The (lightness, chroma) ladder mirrors the indigo template in +// globals.css. For a custom accent we keep L + C constant and vary only +// H, then optionally scale chroma uniformly so users can dial down or +// pump up saturation. +// ──────────────────────────────────────────────────────────────────────── + +export type BrandStop = { stop: number; l: number; c: number }; + +export const BRAND_LADDER: ReadonlyArray = [ + { stop: 50, l: 0.972, c: 0.020 }, + { stop: 100, l: 0.945, c: 0.040 }, + { stop: 200, l: 0.895, c: 0.078 }, + { stop: 300, l: 0.825, c: 0.130 }, + { stop: 400, l: 0.720, c: 0.180 }, + { stop: 500, l: 0.620, c: 0.210 }, + { stop: 600, l: 0.555, c: 0.220 }, + { stop: 700, l: 0.485, c: 0.205 }, + { stop: 800, l: 0.405, c: 0.175 }, + { stop: 900, l: 0.325, c: 0.135 }, + { stop: 950, l: 0.230, c: 0.090 }, +]; + +export type CustomAccentSpec = { + /** Hue in degrees, 0-360. */ + h: number; + /** Chroma multiplier — 1.0 == indigo template intensity. */ + c: number; +}; + +export const DEFAULT_CUSTOM_ACCENT: CustomAccentSpec = { h: 12, c: 1.0 }; + +/** Build the eleven `--brand-*` value strings for a given custom spec. */ +export function buildCustomBrandStops( + spec: CustomAccentSpec, +): ReadonlyArray<{ var: string; value: string }> { + const h = ((spec.h % 360) + 360) % 360; + const cScale = Math.max(0.4, Math.min(1.4, spec.c)); + return BRAND_LADDER.map(({ stop, l, c }) => ({ + var: `--brand-${stop}`, + value: `oklch(${l.toFixed(3)} ${(c * cScale).toFixed(3)} ${h.toFixed(0)})`, + })); +} diff --git a/clients/dashboard/src/components/theme/theme-provider.tsx b/clients/dashboard/src/components/theme/theme-provider.tsx new file mode 100644 index 0000000000..c440dfaa8d --- /dev/null +++ b/clients/dashboard/src/components/theme/theme-provider.tsx @@ -0,0 +1,345 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from "react"; +import { flushSync } from "react-dom"; +import { + ACCENT_STORAGE_KEY, + accents, + buildCustomBrandStops, + CUSTOM_ACCENT_ID, + CUSTOM_ACCENT_STORAGE_KEY, + DEFAULT_ACCENT, + DEFAULT_CUSTOM_ACCENT, + DEFAULT_DENSITY, + DEFAULT_FONT, + DENSITY_STORAGE_KEY, + FONT_STORAGE_KEY, + fonts, + type CustomAccentSpec, + type DensityMode, +} from "@/components/theme/appearance-options"; + +export type ThemeMode = "light" | "dark" | "system"; +type ResolvedTheme = "light" | "dark"; + +type ThemeContextValue = { + mode: ThemeMode; + resolved: ResolvedTheme; + setMode: (mode: ThemeMode) => void; + font: string; + setFont: (id: string) => void; + accent: string; + setAccent: (id: string) => void; + /** Currently configured custom accent spec — drives the live preview + * in the appearance UI even when a preset is selected. */ + customAccent: CustomAccentSpec; + setCustomAccent: (spec: CustomAccentSpec) => void; + density: DensityMode; + setDensity: (next: DensityMode) => void; +}; + +const ThemeContext = createContext(null); +const THEME_STORAGE_KEY = "fsh.theme"; +const ACCENT_CLASS_PREFIX = "accent-"; +const FALLBACK_TRANSITION_MS = 280; + +function readStoredMode(): ThemeMode { + if (typeof window === "undefined") return "system"; + try { + const stored = window.localStorage.getItem(THEME_STORAGE_KEY); + return stored === "light" || stored === "dark" || stored === "system" ? stored : "system"; + } catch { + return "system"; + } +} + +function readStoredString(key: string, fallback: string): string { + if (typeof window === "undefined") return fallback; + try { + return window.localStorage.getItem(key) ?? fallback; + } catch { + return fallback; + } +} + +function systemPrefersDark(): boolean { + if (typeof window === "undefined") return false; + return window.matchMedia("(prefers-color-scheme: dark)").matches; +} + +function applyDarkClass(next: ResolvedTheme) { + document.documentElement.classList.toggle("dark", next === "dark"); +} + +let fallbackTimer: number | undefined; + +/** + * Wraps a DOM mutation in the smoothest available crossfade: + * + * 1. View Transitions API — single bitmap snapshot crossfade. The only + * mechanism that can morph gradients, box-shadows, SVG fills, + * conic/radial backgrounds, and image-based surfaces in lockstep + * with text and bg-color. Handles every property uniformly. + * 2. Firefox / older browsers — opt into the scoped `theme-switching` + * blanket transition for the subset of properties CSS *can* + * interpolate. + * 3. Reduced-motion / cold load — apply instantly, no animation. + * + * The commit callback MUST update the DOM synchronously. We use + * `flushSync` at the call site so React's state updates land inside the + * View Transitions snapshot window — otherwise the toggle thumb would + * lead the actual class flip by a frame and the snapshot would be + * inconsistent with the new state. + */ +function withThemeTransition(commit: () => void): void { + const root = document.documentElement; + const ready = root.classList.contains("theme-ready"); + const reduceMotion = + typeof window !== "undefined" && + window.matchMedia("(prefers-reduced-motion: reduce)").matches; + const canViewTransition = typeof document.startViewTransition === "function"; + + if (!ready || reduceMotion) { + commit(); + return; + } + + if (canViewTransition) { + // Mark the document for the duration of the transition so component + // CSS can opt out of competing transitions (e.g. the seg-thumb's own + // transform animation, which would otherwise interpolate inside the + // captured snapshot and tear). + root.classList.add("vt-active"); + const transition = document.startViewTransition!(commit); + const cleanup = () => root.classList.remove("vt-active"); + transition.finished.then(cleanup, cleanup); + return; + } + + root.classList.add("theme-switching"); + if (fallbackTimer !== undefined) window.clearTimeout(fallbackTimer); + try { + commit(); + } finally { + fallbackTimer = window.setTimeout(() => { + root.classList.remove("theme-switching"); + fallbackTimer = undefined; + }, FALLBACK_TRANSITION_MS); + } +} + +function applyFont(id: string) { + if (typeof document === "undefined") return; + const opt = fonts.find((f) => f.id === id) ?? fonts[0]; + document.documentElement.style.setProperty("--font-sans", opt.family); +} + +const BRAND_VARS: ReadonlyArray = [ + "--brand-50", "--brand-100", "--brand-200", "--brand-300", "--brand-400", + "--brand-500", "--brand-600", "--brand-700", "--brand-800", "--brand-900", + "--brand-950", +]; + +function clearCustomBrandInlineStyles(root: HTMLElement) { + for (const v of BRAND_VARS) root.style.removeProperty(v); +} + +function applyCustomBrandInlineStyles(root: HTMLElement, spec: CustomAccentSpec) { + for (const { var: name, value } of buildCustomBrandStops(spec)) { + root.style.setProperty(name, value); + } +} + +function applyAccent(id: string, customSpec: CustomAccentSpec) { + if (typeof document === "undefined") return; + const root = document.documentElement; + // Strip any existing accent-* class then add the new one. The default + // (indigo) lives in :root so it needs no class. + root.className = root.className + .split(/\s+/) + .filter((c) => !c.startsWith(ACCENT_CLASS_PREFIX)) + .join(" "); + + if (id === CUSTOM_ACCENT_ID) { + // Custom accent — inline-style the eleven --brand-* stops from the + // current spec. Inline styles win over the accent-* class rules so + // we don't need to add any class. + applyCustomBrandInlineStyles(root, customSpec); + return; + } + + // Preset (or default). Make sure inline overrides aren't lingering + // from a previous custom selection — otherwise the chosen preset + // would be invisible. + clearCustomBrandInlineStyles(root); + + if (id !== DEFAULT_ACCENT && accents.some((a) => a.id === id)) { + root.classList.add(`${ACCENT_CLASS_PREFIX}${id}`); + } +} + +function readStoredCustomAccent(): CustomAccentSpec { + if (typeof window === "undefined") return DEFAULT_CUSTOM_ACCENT; + try { + const raw = window.localStorage.getItem(CUSTOM_ACCENT_STORAGE_KEY); + if (!raw) return DEFAULT_CUSTOM_ACCENT; + const parsed = JSON.parse(raw) as Partial; + return { + h: typeof parsed.h === "number" ? parsed.h : DEFAULT_CUSTOM_ACCENT.h, + c: typeof parsed.c === "number" ? parsed.c : DEFAULT_CUSTOM_ACCENT.c, + }; + } catch { + return DEFAULT_CUSTOM_ACCENT; + } +} + +function applyDensity(value: DensityMode) { + if (typeof document === "undefined") return; + document.documentElement.classList.toggle("density-compact", value === "compact"); +} + +export function ThemeProvider({ children }: { children: ReactNode }) { + const [mode, setModeState] = useState(() => readStoredMode()); + const [resolved, setResolved] = useState(() => + readStoredMode() === "dark" + ? "dark" + : readStoredMode() === "light" + ? "light" + : systemPrefersDark() + ? "dark" + : "light", + ); + const [font, setFontState] = useState(() => + readStoredString(FONT_STORAGE_KEY, DEFAULT_FONT), + ); + const [accent, setAccentState] = useState(() => + readStoredString(ACCENT_STORAGE_KEY, DEFAULT_ACCENT), + ); + const [customAccent, setCustomAccentState] = useState(() => + readStoredCustomAccent(), + ); + const [density, setDensityState] = useState(() => { + const stored = readStoredString(DENSITY_STORAGE_KEY, DEFAULT_DENSITY); + return stored === "compact" ? "compact" : DEFAULT_DENSITY; + }); + + // Apply font / accent / density — covers initial render and any + // subsequent change. The dark class is owned by withThemeTransition + // and the index.html bootstrap script; no useEffect for `resolved`. + // Custom-accent application also re-runs whenever `customAccent` + // changes so a hue tweak previews live. + useEffect(() => applyFont(font), [font]); + useEffect(() => applyAccent(accent, customAccent), [accent, customAccent]); + useEffect(() => applyDensity(density), [density]); + + // Subscribe to system preference while in "system" mode. Future OS + // changes route through withThemeTransition so they crossfade too. + useEffect(() => { + if (mode !== "system") return undefined; + const mq = window.matchMedia("(prefers-color-scheme: dark)"); + const update = () => { + const next: ResolvedTheme = mq.matches ? "dark" : "light"; + withThemeTransition(() => { + flushSync(() => setResolved(next)); + applyDarkClass(next); + }); + }; + mq.addEventListener("change", update); + return () => mq.removeEventListener("change", update); + }, [mode]); + + const setMode = useCallback((next: ThemeMode) => { + const nextResolved: ResolvedTheme = + next === "dark" + ? "dark" + : next === "light" + ? "light" + : systemPrefersDark() + ? "dark" + : "light"; + + withThemeTransition(() => { + // flushSync ensures the new mode/resolved render lands BEFORE the + // View Transitions API captures the new snapshot — otherwise the + // thumb position in the new snapshot wouldn't match the new state. + flushSync(() => { + setModeState(next); + setResolved(nextResolved); + }); + applyDarkClass(nextResolved); + }); + + try { + window.localStorage.setItem(THEME_STORAGE_KEY, next); + } catch { + /* storage unavailable */ + } + }, []); + + const setFont = useCallback((id: string) => { + setFontState(id); + try { + window.localStorage.setItem(FONT_STORAGE_KEY, id); + } catch { + /* storage unavailable */ + } + }, []); + + const setAccent = useCallback((id: string) => { + setAccentState(id); + try { + window.localStorage.setItem(ACCENT_STORAGE_KEY, id); + } catch { + /* storage unavailable */ + } + }, []); + + const setCustomAccent = useCallback((spec: CustomAccentSpec) => { + setCustomAccentState(spec); + try { + window.localStorage.setItem(CUSTOM_ACCENT_STORAGE_KEY, JSON.stringify(spec)); + } catch { + /* storage unavailable */ + } + }, []); + + const setDensity = useCallback((next: DensityMode) => { + setDensityState(next); + try { + window.localStorage.setItem(DENSITY_STORAGE_KEY, next); + } catch { + /* storage unavailable */ + } + }, []); + + const value = useMemo( + () => ({ + mode, resolved, setMode, + font, setFont, + accent, setAccent, + customAccent, setCustomAccent, + density, setDensity, + }), + [ + mode, resolved, setMode, + font, setFont, + accent, setAccent, + customAccent, setCustomAccent, + density, setDensity, + ], + ); + + return {children}; +} + +export function useTheme() { + const ctx = useContext(ThemeContext); + if (!ctx) throw new Error("useTheme must be used within ThemeProvider"); + return ctx; +} diff --git a/clients/dashboard/src/components/theme/theme-toggle.tsx b/clients/dashboard/src/components/theme/theme-toggle.tsx new file mode 100644 index 0000000000..baf25875f9 --- /dev/null +++ b/clients/dashboard/src/components/theme/theme-toggle.tsx @@ -0,0 +1,57 @@ +import { Monitor, Moon, Sun } from "lucide-react"; +import { useTheme, type ThemeMode } from "@/components/theme/theme-provider"; +import { cn } from "@/lib/cn"; + +const options: Array<{ + value: ThemeMode; + label: string; + Icon: React.ComponentType<{ className?: string }>; +}> = [ + { value: "light", label: "Light", Icon: Sun }, + { value: "system", label: "System", Icon: Monitor }, + { value: "dark", label: "Dark", Icon: Moon }, +]; + +/** + * Three-segment theme switcher with an animated thumb that slides + * between segments. The thumb position is driven by a CSS attribute + * selector on `data-mode`, so the animation runs without React having + * to coordinate any stateful transition. + */ +export function ThemeToggle() { + const { mode, setMode } = useTheme(); + return ( +
    + + {options.map(({ value, label, Icon }) => { + const active = mode === value; + return ( + + ); + })} +
    + ); +} diff --git a/clients/dashboard/src/components/ui/avatar.tsx b/clients/dashboard/src/components/ui/avatar.tsx new file mode 100644 index 0000000000..93c6fae1bf --- /dev/null +++ b/clients/dashboard/src/components/ui/avatar.tsx @@ -0,0 +1,118 @@ +import * as React from "react"; +import { cn } from "@/lib/cn"; + +type AvatarSize = "sm" | "md" | "lg"; + +type AvatarProps = React.HTMLAttributes & { + /** Display name; up to two leading characters become the fallback initials. */ + name?: string | null; + /** Optional image URL. Falls back to the initials when missing or fails to load. */ + src?: string | null; + size?: AvatarSize; + /** When true, emits a small pulsing status dot at the bottom-right. */ + status?: "online" | "offline" | "warning"; + /** Adds a soft brand halo around the avatar (used in the dropdown hero). */ + halo?: boolean; +}; + +const sizeClass: Record = { + sm: "h-7 w-7 text-[11px]", + md: "h-9 w-9 text-[13px]", + lg: "h-12 w-12 text-[16px]", +}; + +const dotSize: Record = { + sm: "h-1.5 w-1.5", + md: "h-2 w-2", + lg: "h-2.5 w-2.5", +}; + +function getInitials(name?: string | null): string { + if (!name) return "?"; + const trimmed = name.trim(); + if (trimmed.length === 0) return "?"; + const parts = trimmed.split(/\s+/).slice(0, 2); + // For "Mukesh Murugan" → "MM"; for single name "admin@root.com" → "A". + return parts.map((p) => p.charAt(0).toUpperCase()).join(""); +} + +function statusColor(status: AvatarProps["status"]): string | undefined { + switch (status) { + case "online": + return "var(--color-success)"; + case "warning": + return "var(--color-warning)"; + case "offline": + return "var(--color-muted-foreground)"; + default: + return undefined; + } +} + +/** + * Avatar — circular surface that carries the brand-mark vocabulary. + * The base is the rotating conic gradient under a top-edge highlight; + * the user's initial sits on top in primary-foreground. When `src` is + * provided, the image covers the gradient. An optional status dot + * anchors at the bottom-right and pulses for `online`. + */ +export const Avatar = React.forwardRef( + ({ name, src, size = "md", status, halo, className, ...props }, ref) => { + const initials = getInitials(name); + const dotColor = statusColor(status); + const [imgFailed, setImgFailed] = React.useState(false); + const showImage = Boolean(src) && !imgFailed; + + return ( + + + {showImage ? ( + {name setImgFailed(true)} + className="h-full w-full object-cover" + /> + ) : ( + {initials} + )} + + {dotColor && ( + + )} + + ); + }, +); +Avatar.displayName = "Avatar"; diff --git a/clients/dashboard/src/components/ui/badge.tsx b/clients/dashboard/src/components/ui/badge.tsx new file mode 100644 index 0000000000..552549c483 --- /dev/null +++ b/clients/dashboard/src/components/ui/badge.tsx @@ -0,0 +1,44 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/cn"; + +/** + * Badge — compact status pill. Variants map to semantic tokens so a + * brand re-tone propagates without touching call sites. The `soft` + * style uses the matching `*-soft` background where defined and falls + * back to a tinted layer otherwise. + */ +const badgeVariants = cva( + "inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-[11px] font-medium tracking-tight", + { + variants: { + variant: { + default: + "border-[var(--color-border)] bg-[var(--color-surface-2)] text-[var(--color-foreground)]", + brand: + "border-transparent bg-[var(--color-primary-soft)] text-[var(--color-primary)]", + success: + "border-transparent bg-[oklch(from_var(--color-success)_l_c_h_/_0.14)] text-[var(--color-success)]", + warning: + "border-transparent bg-[oklch(from_var(--color-warning)_l_c_h_/_0.16)] text-[var(--color-warning)]", + info: + "border-transparent bg-[oklch(from_var(--color-info)_l_c_h_/_0.14)] text-[var(--color-info)]", + danger: + "border-transparent bg-[oklch(from_var(--color-destructive)_l_c_h_/_0.14)] text-[var(--color-destructive)]", + outline: + "border-[var(--color-border-strong)] bg-transparent text-[var(--color-muted-foreground)]", + }, + }, + defaultVariants: { variant: "default" }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +export function Badge({ className, variant, ...props }: BadgeProps) { + return ; +} + +export { badgeVariants }; diff --git a/clients/dashboard/src/components/ui/button.tsx b/clients/dashboard/src/components/ui/button.tsx new file mode 100644 index 0000000000..2396c88e6b --- /dev/null +++ b/clients/dashboard/src/components/ui/button.tsx @@ -0,0 +1,85 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/cn"; + +/** + * Button — single source of truth for clickable actions. Variants are + * tuned to the dashboard's premium-SaaS aesthetic: + * - default: solid brand fill with a faint top-edge sheen and a brand + * halo on hover. Reads as "the one thing on this surface." + * - outline: 1px hairline that swaps to a soft brand tint on hover. + * - ghost / link: chrome-free for in-flow actions. + * + * Radius and motion timing come from the design tokens so any global + * tuning propagates without touching this file. + */ +const buttonVariants = cva( + [ + "relative inline-flex cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap", + "rounded-md text-sm font-medium tracking-tight", + "transition-[background-color,box-shadow,transform,color] duration-[var(--duration-default)] ease-[var(--ease-out-cubic)]", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-background)]", + "disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50", + "active:translate-y-px", + ].join(" "), + { + variants: { + variant: { + default: [ + "bg-[var(--color-primary)] text-[var(--color-primary-foreground)]", + "shadow-[0_1px_0_oklch(1_0_0_/_0.12)_inset,0_1px_2px_oklch(0_0_0_/_0.12)]", + "hover:bg-[var(--color-primary-hover)]", + "hover:shadow-[0_1px_0_oklch(1_0_0_/_0.18)_inset,0_4px_18px_-6px_oklch(from_var(--color-primary)_l_c_h_/_0.55)]", + ].join(" "), + destructive: [ + "bg-[var(--color-destructive)] text-[var(--color-destructive-foreground)]", + "shadow-[0_1px_0_oklch(1_0_0_/_0.12)_inset,0_1px_2px_oklch(0_0_0_/_0.12)]", + "hover:brightness-[1.05]", + ].join(" "), + outline: [ + "border border-[var(--color-border-strong)] bg-[var(--color-surface-2)] text-[var(--color-foreground)]", + "hover:bg-[var(--color-surface-4)] hover:border-[var(--color-border-strong)]", + ].join(" "), + secondary: + "bg-[var(--color-secondary)] text-[var(--color-secondary-foreground)] hover:bg-[var(--color-surface-4)]", + ghost: + "text-[var(--color-foreground)] hover:bg-[var(--color-accent)] hover:text-[var(--color-accent-foreground)]", + link: "text-[var(--color-primary)] underline-offset-4 hover:underline", + soft: "bg-[var(--color-primary-soft)] text-[var(--color-primary)] hover:brightness-[1.08]", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-6", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +export const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { buttonVariants }; diff --git a/clients/dashboard/src/components/ui/card.tsx b/clients/dashboard/src/components/ui/card.tsx new file mode 100644 index 0000000000..49734d4640 --- /dev/null +++ b/clients/dashboard/src/components/ui/card.tsx @@ -0,0 +1,87 @@ +import * as React from "react"; +import { cn } from "@/lib/cn"; + +type CardProps = React.HTMLAttributes & { + /** When true, the card softens its shadow in on hover. */ + interactive?: boolean; +}; + +/** + * Card — primary content surface, modernized. + * + * Earlier revisions stacked a luminance-ramped gradient border, an inset + * top-edge highlight ("glossy lip"), and a double drop-shadow on hover. + * The combination read as 2014-era skeuomorphism on dense pages. + * + * Now: a single hairline border at low alpha, no resting shadow at all + * (depth comes from surface-tier contrast — surface-1 on surface-2 on + * surface-3 — not chrome), and on `interactive` a soft pillow-shadow + * that fades in slowly with a quiet border-strong tint. Linear / Mercury + * vocabulary. The `card-shell` utility owns the styling so we can tune + * it in one place. + */ +export const Card = React.forwardRef( + ({ className, interactive, ...props }, ref) => ( +
    + ), +); +Card.displayName = "Card"; + +export const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
    + ), +); +CardHeader.displayName = "CardHeader"; + +export const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
    + ), +); +CardTitle.displayName = "CardTitle"; + +export const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
    + ), +); +CardDescription.displayName = "CardDescription"; + +export const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +
    + ), +); +CardContent.displayName = "CardContent"; + +export const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
    + ), +); +CardFooter.displayName = "CardFooter"; diff --git a/clients/dashboard/src/components/ui/dialog.tsx b/clients/dashboard/src/components/ui/dialog.tsx new file mode 100644 index 0000000000..7076fc624a --- /dev/null +++ b/clients/dashboard/src/components/ui/dialog.tsx @@ -0,0 +1,128 @@ +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; +import { cn } from "@/lib/cn"; + +/** + * Dialog primitives — Radix-based, styled to the FSH design system. + * Usage: + * + * + * + * ... + * ... + * + * ... + * + * + * + * Open/close transitions are driven by [data-state] attributes Radix + * sets on the overlay and content, paired with the keyframes below. + */ + +export const Dialog = DialogPrimitive.Root; +export const DialogTrigger = DialogPrimitive.Trigger; +export const DialogPortal = DialogPrimitive.Portal; +export const DialogClose = DialogPrimitive.Close; + +export const DialogOverlay = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = "DialogOverlay"; + +export const DialogContent = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + + + +)); +DialogContent.displayName = "DialogContent"; + +export function DialogHeader({ className, ...props }: React.HTMLAttributes) { + return ( +
    + ); +} + +export function DialogFooter({ className, ...props }: React.HTMLAttributes) { + return ( +
    + ); +} + +export const DialogTitle = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = "DialogTitle"; + +export const DialogDescription = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = "DialogDescription"; + +export function DialogBody({ className, ...props }: React.HTMLAttributes) { + return
    ; +} diff --git a/clients/dashboard/src/components/ui/dropdown-menu.tsx b/clients/dashboard/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000000..e5c40dbd90 --- /dev/null +++ b/clients/dashboard/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,135 @@ +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { ChevronRight } from "lucide-react"; +import { cn } from "@/lib/cn"; + +/** + * Dropdown primitives — Radix-based, styled to the FSH design system. + * Trigger transitions are driven by [data-state] attributes Radix sets + * on the content, paired with the dialog-in/out keyframes from + * globals.css. Content uses the gradient-border + frosted treatment so + * it shares vocabulary with Card and Dialog. + */ + +export const DropdownMenu = DropdownMenuPrimitive.Root; +export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; +export const DropdownMenuPortal = DropdownMenuPrimitive.Portal; +export const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +export const DropdownMenuContent = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 8, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = "DropdownMenuContent"; + +export const DropdownMenuItem = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef & { + destructive?: boolean; + } +>(({ className, destructive, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = "DropdownMenuItem"; + +export const DropdownMenuLinkItem = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef & { + href: string; + } +>(({ href, children, onClick, ...props }, ref) => { + return ( + + + {children} + + + + ); +}); +DropdownMenuLinkItem.displayName = "DropdownMenuLinkItem"; + +export const DropdownMenuLabel = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = "DropdownMenuLabel"; + +export const DropdownMenuSeparator = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = "DropdownMenuSeparator"; + +/** + * A non-interactive row meant to host arbitrary content (e.g. a + * segmented theme toggle). Sits on the same horizontal rail as items + * but doesn't participate in roving focus. + */ +export function DropdownMenuRow({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
    + ); +} diff --git a/clients/dashboard/src/components/ui/input.tsx b/clients/dashboard/src/components/ui/input.tsx new file mode 100644 index 0000000000..3155c5703c --- /dev/null +++ b/clients/dashboard/src/components/ui/input.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import { cn } from "@/lib/cn"; + +export type InputProps = React.InputHTMLAttributes; + +export const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; diff --git a/clients/dashboard/src/components/ui/label.tsx b/clients/dashboard/src/components/ui/label.tsx new file mode 100644 index 0000000000..a9fb7a1bc1 --- /dev/null +++ b/clients/dashboard/src/components/ui/label.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cn } from "@/lib/cn"; + +export const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; diff --git a/clients/dashboard/src/components/ui/skeleton.tsx b/clients/dashboard/src/components/ui/skeleton.tsx new file mode 100644 index 0000000000..79ddf6bd88 --- /dev/null +++ b/clients/dashboard/src/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import * as React from "react"; +import { cn } from "@/lib/cn"; + +/** + * Skeleton — placeholder block with a brand-tinted shimmer sweep. + * Use instead of "Loading…" copy for any content that has predictable + * dimensions (rows, cards, charts). The shimmer keyframe lives in + * globals.css and honours prefers-reduced-motion. + */ +export const Skeleton = React.forwardRef>( + ({ className, ...props }, ref) => ( +
    + ), +); +Skeleton.displayName = "Skeleton"; diff --git a/clients/dashboard/src/components/ui/switch.tsx b/clients/dashboard/src/components/ui/switch.tsx new file mode 100644 index 0000000000..d85584b584 --- /dev/null +++ b/clients/dashboard/src/components/ui/switch.tsx @@ -0,0 +1,51 @@ +import * as React from "react"; +import { cn } from "@/lib/cn"; + +type SwitchProps = { + checked: boolean; + onCheckedChange: (checked: boolean) => void; + disabled?: boolean; + id?: string; + "aria-label"?: string; + "aria-labelledby"?: string; +}; + +/** + * Switch — accessible toggle. Tracks a controlled boolean and slides a + * thumb between off and on. Keyboard: space / enter activates (button + * default). The thumb position is tweened with motion tokens. + */ +export const Switch = React.forwardRef( + ({ checked, onCheckedChange, disabled, ...props }, ref) => { + return ( + + ); + }, +); +Switch.displayName = "Switch"; diff --git a/clients/dashboard/src/env.ts b/clients/dashboard/src/env.ts new file mode 100644 index 0000000000..d133f08038 --- /dev/null +++ b/clients/dashboard/src/env.ts @@ -0,0 +1,8 @@ +// Dev builds proxy `/api` → VITE_API_BASE_URL via vite.config.ts, so the app can use relative URLs. +// Production builds should set VITE_API_BASE_URL to the fully-qualified API origin. +const apiBase = (import.meta.env.VITE_API_BASE_URL ?? "").replace(/\/$/, ""); + +export const env = { + apiBase, + defaultTenant: import.meta.env.VITE_DEFAULT_TENANT ?? "root", +}; diff --git a/clients/dashboard/src/lib/api-client.ts b/clients/dashboard/src/lib/api-client.ts new file mode 100644 index 0000000000..7ab421c616 --- /dev/null +++ b/clients/dashboard/src/lib/api-client.ts @@ -0,0 +1,150 @@ +import { env } from "@/env"; +import { tokenStore } from "@/auth/token-store"; + +export type ApiError = { + status: number; + title?: string; + detail?: string; + errors?: Record; + // Dev-only extension surfaced on 401 by ConfigureJwtBearerOptions. + reason?: string; + // Allow any other ProblemDetails extensions through. + [key: string]: unknown; +}; + +export class ApiRequestError extends Error { + readonly status: number; + readonly problem?: ApiError; + + constructor(status: number, message: string, problem?: ApiError) { + super(message); + this.status = status; + this.problem = problem; + } +} + +type RequestInitEx = RequestInit & { skipAuth?: boolean }; + +let refreshPromise: Promise | null = null; + +async function refreshAccessToken() { + const refreshToken = tokenStore.getRefreshToken(); + const accessToken = tokenStore.getAccessToken(); + if (!refreshToken || !accessToken) { + throw new ApiRequestError(401, "No refresh token"); + } + + // Server's RefreshTokenCommand requires both `token` (the existing, possibly expired + // access token, used to cross-check the subject) and `refreshToken`. Sending only one + // of them fails FluentValidation and surfaces as 500. + const tenant = tokenStore.getTenant() ?? env.defaultTenant; + const response = await fetch(`${env.apiBase}/api/v1/identity/token/refresh`, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(tenant ? { tenant } : {}), + }, + body: JSON.stringify({ token: accessToken, refreshToken }), + }); + + if (!response.ok) { + tokenStore.clear(); + throw new ApiRequestError(response.status, "Refresh failed"); + } + + // Server's RefreshTokenCommandResponse returns `{ token, refreshToken, refreshTokenExpiryTime }` — + // note the rotated access token is on `token`, not `accessToken`. + const tokens = (await response.json()) as { + token: string; + refreshToken: string; + }; + tokenStore.setTokens(tokens.token, tokens.refreshToken); +} + +async function parseError(response: Response): Promise { + const contentType = response.headers.get("content-type") ?? ""; + if (!contentType.includes("json")) { + return undefined; + } + try { + return (await response.json()) as ApiError; + } catch { + return undefined; + } +} + +export async function apiFetch( + path: string, + init: RequestInitEx = {}, +): Promise { + const { skipAuth, headers, ...rest } = init; + + const mergedHeaders = new Headers(headers); + if (!mergedHeaders.has("Content-Type") && rest.body && typeof rest.body === "string") { + mergedHeaders.set("Content-Type", "application/json"); + } + + if (!skipAuth) { + const accessToken = tokenStore.getAccessToken(); + if (accessToken) { + mergedHeaders.set("Authorization", `Bearer ${accessToken}`); + } else { + // We're not anonymous (skipAuth=false) but the token is gone — likely a + // manual localStorage clear that AuthContext missed. Clear remaining + // session state and surface a clean 401 so the UI flips to /login + // instead of repeatedly firing tokenless requests. + tokenStore.clear(); + throw new ApiRequestError(401, "Not signed in", { + status: 401, + title: "Unauthorized", + detail: "Your session is no longer available. Please sign in again.", + }); + } + } + + const tenant = tokenStore.getTenant() ?? env.defaultTenant; + if (tenant && !mergedHeaders.has("tenant")) { + mergedHeaders.set("tenant", tenant); + } + + const url = path.startsWith("http") ? path : `${env.apiBase}${path}`; + let response = await fetch(url, { ...rest, headers: mergedHeaders }); + + if (response.status === 401 && !skipAuth && tokenStore.getRefreshToken()) { + refreshPromise ??= refreshAccessToken().finally(() => { + refreshPromise = null; + }); + + try { + await refreshPromise; + } catch (e) { + throw e instanceof ApiRequestError + ? e + : new ApiRequestError(401, "Session expired"); + } + + const retryHeaders = new Headers(mergedHeaders); + retryHeaders.set("Authorization", `Bearer ${tokenStore.getAccessToken() ?? ""}`); + response = await fetch(url, { ...rest, headers: retryHeaders }); + } + + if (!response.ok) { + const problem = await parseError(response); + throw new ApiRequestError( + response.status, + problem?.title ?? problem?.detail ?? response.statusText, + problem, + ); + } + + if (response.status === 204) { + return undefined as T; + } + + const contentType = response.headers.get("content-type") ?? ""; + if (!contentType.includes("json")) { + return undefined as T; + } + + return (await response.json()) as T; +} diff --git a/clients/dashboard/src/lib/cn.ts b/clients/dashboard/src/lib/cn.ts new file mode 100644 index 0000000000..a5ef193506 --- /dev/null +++ b/clients/dashboard/src/lib/cn.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/clients/dashboard/src/lib/list-helpers.ts b/clients/dashboard/src/lib/list-helpers.ts new file mode 100644 index 0000000000..a649263386 --- /dev/null +++ b/clients/dashboard/src/lib/list-helpers.ts @@ -0,0 +1,78 @@ +import { ApiRequestError } from "@/lib/api-client"; + +const dateLong = new Intl.DateTimeFormat("en-US", { + month: "short", + day: "2-digit", + year: "numeric", +}); + +export function formatDate(iso: string | null | undefined) { + if (!iso) return "—"; + return dateLong.format(new Date(iso)); +} + +// "APR 30 2026" — mono-caps tabular form for ledger/registry rows. +export function formatDateMono(iso: string | null | undefined) { + if (!iso) return "—"; + return dateLong.format(new Date(iso)).toUpperCase().replace(",", ""); +} + +// "3d ago", "2mo ago" — terse relative time for the secondary line. +export function formatRelative(iso: string | null | undefined) { + if (!iso) return ""; + const diffMs = Date.now() - new Date(iso).getTime(); + if (Number.isNaN(diffMs) || diffMs < 0) return ""; + const sec = Math.floor(diffMs / 1000); + if (sec < 60) return "just now"; + const min = Math.floor(sec / 60); + if (min < 60) return `${min}m ago`; + const hr = Math.floor(min / 60); + if (hr < 24) return `${hr}h ago`; + const day = Math.floor(hr / 24); + if (day < 30) return `${day}d ago`; + const mo = Math.floor(day / 30); + if (mo < 12) return `${mo}mo ago`; + const yr = Math.floor(day / 365); + return `${yr}y ago`; +} + +export function pad2(n: number) { + return n.toString().padStart(2, "0"); +} + +// Mirror the server's slug derivation so editors can show a live preview. +export function slugify(value: string) { + const lower = value.trim().toLowerCase(); + const chars = [...lower].map((c) => (/[a-z0-9]/.test(c) ? c : "-")); + let s = chars.join("").replace(/^-+|-+$/g, ""); + while (s.includes("--")) s = s.replace(/--/g, "-"); + return s; +} + +export function formatMoney(amount: number, currency: string) { + try { + return new Intl.NumberFormat(undefined, { + style: "currency", + currency, + }).format(amount); + } catch { + return `${amount.toFixed(2)} ${currency}`; + } +} + +// Surface API/network/runtime errors with the same formatting everywhere. +// Prefers the Dev-only `reason` extension on ProblemDetails so JwtBearer +// rejection causes (expired token, signing key drift, etc) are visible +// in toast descriptions during development. +export function describe(err: unknown): string { + if (err instanceof ApiRequestError) { + const reason = + err.problem?.reason ?? + err.problem?.detail ?? + err.problem?.title ?? + err.message; + return `${err.status} ${reason}`; + } + if (err instanceof Error) return err.message; + return String(err); +} diff --git a/clients/dashboard/src/lib/query-client.ts b/clients/dashboard/src/lib/query-client.ts new file mode 100644 index 0000000000..3089edb4b4 --- /dev/null +++ b/clients/dashboard/src/lib/query-client.ts @@ -0,0 +1,17 @@ +import { QueryClient } from "@tanstack/react-query"; +import { ApiRequestError } from "@/lib/api-client"; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: (failureCount, error) => { + if (error instanceof ApiRequestError && (error.status === 401 || error.status === 403)) { + return false; + } + return failureCount < 2; + }, + staleTime: 30_000, + refetchOnWindowFocus: false, + }, + }, +}); diff --git a/clients/dashboard/src/main.tsx b/clients/dashboard/src/main.tsx new file mode 100644 index 0000000000..95ccfed62e --- /dev/null +++ b/clients/dashboard/src/main.tsx @@ -0,0 +1,15 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "@/App"; +import "@/styles/globals.css"; + +const rootElement = document.getElementById("root"); +if (!rootElement) { + throw new Error("Root element '#root' not found"); +} + +createRoot(rootElement).render( + + + , +); diff --git a/clients/dashboard/src/pages/activity.tsx b/clients/dashboard/src/pages/activity.tsx new file mode 100644 index 0000000000..d889422c21 --- /dev/null +++ b/clients/dashboard/src/pages/activity.tsx @@ -0,0 +1,54 @@ +import { LiveFeed } from "@/components/sse/live-feed"; +import { useAuth } from "@/auth/use-auth"; +import { useSse } from "@/sse/sse-context"; +import { Badge } from "@/components/ui/badge"; + +export function ActivityPage() { + const { user } = useAuth(); + const { status, eventCount } = useSse(); + + return ( +
    +
    +
    +
    + + Tenant + + + {user?.tenant ?? "—"} + +
    +

    + Live activity +

    +

    + Full event log streamed from the API over Server-Sent Events. +

    +
    + +
    + {status === "connected" ? ( + streaming + ) : status === "error" ? ( + offline + ) : ( + {status} + )} +
    + + total + + + {new Intl.NumberFormat("en-US").format(eventCount)} + +
    +
    +
    + +
    + +
    +
    + ); +} diff --git a/clients/dashboard/src/pages/audits.tsx b/clients/dashboard/src/pages/audits.tsx new file mode 100644 index 0000000000..f7ac1d7460 --- /dev/null +++ b/clients/dashboard/src/pages/audits.tsx @@ -0,0 +1,1434 @@ +import { useEffect, useMemo, useState } from "react"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { + Activity, + AlertTriangle, + ChevronRight, + CircleAlert, + Database, + ExternalLink, + Filter, + Hash, + Loader2, + RefreshCw, + Search, + Shield, + ShieldCheck, + Tag, + X, +} from "lucide-react"; +import { + AuditEventType, + AuditSeverity, + AUDIT_EVENT_TYPE_LABELS, + AUDIT_SEVERITY_LABELS, + AUDIT_TAG_LABELS, + decodeTags, + getAuditById, + getAuditsByCorrelation, + getAuditSummary, + listAudits, + type AuditDetailDto, + type AuditSummaryDto, +} from "@/api/audits"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Skeleton } from "@/components/ui/skeleton"; +import { PageHero } from "@/components/list"; +import { + Dialog, + DialogClose, + DialogDescription, + DialogOverlay, + DialogPortal, + DialogTitle, +} from "@/components/ui/dialog"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { useAuth } from "@/auth/use-auth"; +import { cn } from "@/lib/cn"; + +const PAGE_SIZE = 25; + +// ──────────────────────────────────────────────────────────────────────── +// Time-range presets — keep the SQL window predictable from the UI. +// ──────────────────────────────────────────────────────────────────────── + +type RangeKey = "24h" | "7d" | "30d" | "90d"; +const RANGE_OPTIONS: Array<{ key: RangeKey; label: string; ms: number }> = [ + { key: "24h", label: "24h", ms: 24 * 60 * 60 * 1000 }, + { key: "7d", label: "7d", ms: 7 * 24 * 60 * 60 * 1000 }, + { key: "30d", label: "30d", ms: 30 * 24 * 60 * 60 * 1000 }, + { key: "90d", label: "90d", ms: 90 * 24 * 60 * 60 * 1000 }, +]; + +function rangeBounds(key: RangeKey): { from: string; to: string } { + const opt = RANGE_OPTIONS.find((r) => r.key === key) ?? RANGE_OPTIONS[1]; + const to = new Date(); + const from = new Date(to.getTime() - opt.ms); + return { from: from.toISOString(), to: to.toISOString() }; +} + +// ──────────────────────────────────────────────────────────────────────── +// Severity / event-type tone — keep visual language consistent across +// the row colour bar, drawer header, and severity pills. +// ──────────────────────────────────────────────────────────────────────── + +function severityTone(severity: number): "default" | "info" | "warning" | "danger" { + if (severity >= AuditSeverity.Critical) return "danger"; + if (severity >= AuditSeverity.Error) return "danger"; + if (severity >= AuditSeverity.Warning) return "warning"; + if (severity >= AuditSeverity.Information) return "info"; + return "default"; +} + +function severityColorVar(severity: number): string { + const tone = severityTone(severity); + return tone === "danger" + ? "var(--color-destructive)" + : tone === "warning" + ? "var(--color-warning)" + : tone === "info" + ? "var(--color-info)" + : "var(--color-muted-foreground)"; +} + +function eventTypeIcon(eventType: number): React.ComponentType> { + if (eventType === AuditEventType.Security) return Shield; + if (eventType === AuditEventType.Exception) return CircleAlert; + if (eventType === AuditEventType.EntityChange) return Database; + if (eventType === AuditEventType.Activity) return Activity; + return Hash; +} + +// ──────────────────────────────────────────────────────────────────────── +// Time formatting — mono ISO for the table, locale-aware for the drawer. +// ──────────────────────────────────────────────────────────────────────── + +function fmtIsoDense(iso: string): { date: string; time: string } { + // 2026-04-30 14:32:11.234 + const d = new Date(iso); + const yyyy = d.getUTCFullYear(); + const mm = String(d.getUTCMonth() + 1).padStart(2, "0"); + const dd = String(d.getUTCDate()).padStart(2, "0"); + const hh = String(d.getUTCHours()).padStart(2, "0"); + const mi = String(d.getUTCMinutes()).padStart(2, "0"); + const ss = String(d.getUTCSeconds()).padStart(2, "0"); + const ms = String(d.getUTCMilliseconds()).padStart(3, "0"); + return { date: `${yyyy}-${mm}-${dd}`, time: `${hh}:${mi}:${ss}.${ms}` }; +} + +function fmtRelative(iso: string, now: number = Date.now()): string { + const delta = Math.max(0, Math.floor((now - Date.parse(iso)) / 1000)); + if (delta < 5) return "just now"; + if (delta < 60) return `${delta}s ago`; + const m = Math.floor(delta / 60); + if (m < 60) return `${m}m ago`; + const h = Math.floor(m / 60); + if (h < 24) return `${h}h ago`; + const days = Math.floor(h / 24); + return `${days}d ago`; +} + +// ──────────────────────────────────────────────────────────────────────── +// Filter state — single source of truth, lifted into URL-synced shape. +// ──────────────────────────────────────────────────────────────────────── + +type FilterState = { + range: RangeKey; + eventType: number | null; + severity: number | null; + tagsMask: number; + source: string; + user: string; + correlation: string; + trace: string; + search: string; + page: number; +}; + +const INITIAL_FILTERS: FilterState = { + range: "7d", + eventType: null, + severity: null, + tagsMask: 0, + source: "", + user: "", + correlation: "", + trace: "", + search: "", + page: 1, +}; + +// ──────────────────────────────────────────────────────────────────────── +// Page +// ──────────────────────────────────────────────────────────────────────── + +export function AuditsPage() { + const { user } = useAuth(); + const [filters, setFilters] = useState(INITIAL_FILTERS); + const [searchInput, setSearchInput] = useState(""); + const [drawerId, setDrawerId] = useState(null); + + // Debounce the search input so we don't hammer the API on every keystroke. + useEffect(() => { + const t = window.setTimeout(() => { + setFilters((f) => ({ ...f, search: searchInput, page: 1 })); + }, 300); + return () => window.clearTimeout(t); + }, [searchInput]); + + const window_ = useMemo(() => rangeBounds(filters.range), [filters.range]); + + const auditsQuery = useQuery({ + queryKey: ["audits", "list", filters, window_], + queryFn: ({ signal }) => + listAudits( + { + pageNumber: filters.page, + pageSize: PAGE_SIZE, + fromUtc: window_.from, + toUtc: window_.to, + eventType: (filters.eventType ?? undefined) as AuditEventType | undefined, + severity: (filters.severity ?? undefined) as AuditSeverity | undefined, + tags: filters.tagsMask || undefined, + source: filters.source || undefined, + userId: filters.user || undefined, + correlationId: filters.correlation || undefined, + traceId: filters.trace || undefined, + search: filters.search || undefined, + }, + signal, + ), + placeholderData: keepPreviousData, + staleTime: 5_000, + }); + + const summaryQuery = useQuery({ + queryKey: ["audits", "summary", window_], + queryFn: ({ signal }) => + getAuditSummary({ fromUtc: window_.from, toUtc: window_.to }, signal), + staleTime: 30_000, + }); + + const totals = useMemo(() => { + const s = summaryQuery.data; + if (!s) return null; + const grand = Object.values(s.eventsByType).reduce((a, b) => a + b, 0); + return { + grand, + byType: { + activity: s.eventsByType[String(AuditEventType.Activity)] ?? 0, + entity: s.eventsByType[String(AuditEventType.EntityChange)] ?? 0, + security: s.eventsByType[String(AuditEventType.Security)] ?? 0, + exception: s.eventsByType[String(AuditEventType.Exception)] ?? 0, + }, + bySeverity: { + info: s.eventsBySeverity[String(AuditSeverity.Information)] ?? 0, + warn: s.eventsBySeverity[String(AuditSeverity.Warning)] ?? 0, + err: s.eventsBySeverity[String(AuditSeverity.Error)] ?? 0, + crit: s.eventsBySeverity[String(AuditSeverity.Critical)] ?? 0, + }, + bySource: Object.entries(s.eventsBySource) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5), + }; + }, [summaryQuery.data]); + + const paged = auditsQuery.data; + const items = paged?.items ?? []; + + const onResetFilters = () => { + setFilters(INITIAL_FILTERS); + setSearchInput(""); + }; + + const activeChipCount = + (filters.eventType !== null ? 1 : 0) + + (filters.severity !== null ? 1 : 0) + + (filters.tagsMask ? 1 : 0) + + (filters.source ? 1 : 0) + + (filters.user ? 1 : 0) + + (filters.correlation ? 1 : 0) + + (filters.trace ? 1 : 0); + + return ( +
    + { + void auditsQuery.refetch(); + void summaryQuery.refetch(); + }} + > + + Refresh + + } + /> + + {/* ── Summary strip ─────────────────────────────────────────────── */} +
    + +
    + + {/* ── Filter bar ────────────────────────────────────────────────── */} +
    + setFilters((f) => ({ ...f, ...p, page: 1 }))} + onSearchInput={setSearchInput} + onReset={onResetFilters} + /> +
    + + {/* ── Table ────────────────────────────────────────────────────── */} +
    + setDrawerId(row.id)} + /> +
    + + {/* ── Pagination ───────────────────────────────────────────────── */} + {paged && paged.totalCount > 0 && ( +
    + setFilters((f) => ({ ...f, page: p }))} + /> +
    + )} + + {/* ── Detail drawer ────────────────────────────────────────────── */} + setDrawerId(null)} + onJumpAudit={(id) => setDrawerId(id)} + onJumpCorrelation={(cid) => { + setDrawerId(null); + setFilters((f) => ({ ...INITIAL_FILTERS, range: f.range, correlation: cid })); + }} + onJumpTrace={(tid) => { + setDrawerId(null); + setFilters((f) => ({ ...INITIAL_FILTERS, range: f.range, trace: tid })); + }} + /> +
    + ); +} + +// ──────────────────────────────────────────────────────────────────────── +// Summary strip — compact totals bar above the filters. Shows grand +// total, breakdown by type as a horizontal bar, severity dots, and +// top-5 sources as right-aligned mono chips. +// ──────────────────────────────────────────────────────────────────────── + +function SummaryStrip({ + loading, + total, + byType, + bySeverity, + topSources, + range, +}: { + loading: boolean; + total: number; + byType?: { activity: number; entity: number; security: number; exception: number }; + bySeverity?: { info: number; warn: number; err: number; crit: number }; + topSources: Array<[string, number]>; + range: RangeKey; +}) { + if (loading || !byType || !bySeverity) { + return ( + + + + + + + + ); + } + + // Stack-bar segments — fall back to a single muted segment when the + // window has zero activity, so the strip still has visual mass. + const t = Math.max(1, total); + const segments = [ + { key: "activity", value: byType.activity, color: "var(--color-primary)" }, + { key: "entity", value: byType.entity, color: "var(--color-info)" }, + { key: "security", value: byType.security, color: "var(--color-success)" }, + { key: "exception", value: byType.exception, color: "var(--color-destructive)" }, + ]; + + return ( + + +
    +
    + Window {range} +
    +
    + {new Intl.NumberFormat("en-US").format(total)} +
    +
    events
    +
    + +
    +
    + {segments.map((s) => ( +
    + ))} +
    +
    + + + + + + + +
    +
    + +
    +
    + Top sources +
    +
    + {topSources.length === 0 && ( + + )} + {topSources.map(([name, count]) => ( + + {name} + {count} + + ))} +
    +
    + + + ); +} + +function Legend({ dot, label, value }: { dot: string; label: string; value: number }) { + return ( + + + {label} + {value} + + ); +} + +function SeverityDot({ color, label, value }: { color: string; label: string; value: number }) { + return ( + + + {label} + {value} + + ); +} + +// ──────────────────────────────────────────────────────────────────────── +// Filter bar — time range presets up top, search + advanced fields below. +// ──────────────────────────────────────────────────────────────────────── + +function FilterBar({ + filters, + searchInput, + activeChipCount, + onPatch, + onSearchInput, + onReset, +}: { + filters: FilterState; + searchInput: string; + activeChipCount: number; + onPatch: (patch: Partial) => void; + onSearchInput: (v: string) => void; + onReset: () => void; +}) { + const [advancedOpen, setAdvancedOpen] = useState(false); + + return ( + + + {/* Row 1: range presets + search box + advanced toggle */} +
    +
    + {RANGE_OPTIONS.map((opt) => ( + + ))} +
    + +
    + + onSearchInput(e.target.value)} + placeholder="Search payload, source, user…" + className="pl-9" + /> + {searchInput && ( + + )} +
    + + + + {activeChipCount > 0 && ( + + )} +
    + + {/* Row 2: event type + severity chips */} +
    + + Type + + {[AuditEventType.Activity, AuditEventType.Security, AuditEventType.EntityChange, AuditEventType.Exception].map((t) => ( + + onPatch({ eventType: filters.eventType === t ? null : t }) + } + > + {AUDIT_EVENT_TYPE_LABELS[t]} + + ))} + + + + + Severity + + {[AuditSeverity.Information, AuditSeverity.Warning, AuditSeverity.Error, AuditSeverity.Critical].map((s) => ( + + onPatch({ severity: filters.severity === s ? null : s }) + } + > + {AUDIT_SEVERITY_LABELS[s]} + + ))} +
    + + {/* Row 3: advanced (collapsible) */} + {advancedOpen && ( +
    + onPatch({ source: v })} + /> + onPatch({ user: v })} + /> + onPatch({ correlation: v })} + /> + onPatch({ trace: v })} + /> +
    +
    + Tags +
    +
    + {AUDIT_TAG_LABELS.map((t) => { + const active = (filters.tagsMask & t.flag) !== 0; + return ( + + onPatch({ + tagsMask: active + ? filters.tagsMask & ~t.flag + : filters.tagsMask | t.flag, + }) + } + > + {t.name} + + ); + })} +
    +
    +
    + )} +
    +
    + ); +} + +function Chip({ + active, + tone, + onClick, + children, +}: { + active: boolean; + tone?: "default" | "info" | "warning" | "danger"; + onClick: () => void; + children: React.ReactNode; +}) { + const activeColor = + tone === "danger" + ? "var(--color-destructive)" + : tone === "warning" + ? "var(--color-warning)" + : tone === "info" + ? "var(--color-info)" + : "var(--color-primary)"; + return ( + + ); +} + +function FieldInput({ + label, + placeholder, + value, + onChange, +}: { + label: string; + placeholder: string; + value: string; + onChange: (v: string) => void; +}) { + return ( + + ); +} + +// ──────────────────────────────────────────────────────────────────────── +// Table — dense forensic-style rows. Each row has a 2px tone bar tied to +// severity, mono timestamp split into date/time, type icon, source, and +// user. Click anywhere in the row to open the detail drawer. +// ──────────────────────────────────────────────────────────────────────── + +function AuditsTable({ + rows, + loading, + fetching, + error, + onRowClick, +}: { + rows: AuditSummaryDto[]; + loading: boolean; + fetching: boolean; + error: string | null; + onRowClick: (row: AuditSummaryDto) => void; +}) { + if (loading) { + return ( + + + {[0, 1, 2, 3, 4, 5, 6, 7].map((i) => ( +
    + + + + + +
    + ))} +
    +
    + ); + } + + if (error) { + return ( + + + +
    Failed to load audits
    +

    + {error} +

    +
    +
    + ); + } + + if (rows.length === 0) { + return ( + + + +
    No audits in this window
    +

    + Try widening the time range or relaxing the filters. Activity events + arrive as soon as the platform handles a request. +

    +
    +
    + ); + } + + return ( + + {fetching && ( +
    + +
    + )} + +
    + + + + + + + + + + + + + + + {rows.map((row, idx) => ( + onRowClick(row)} /> + ))} + +
    TimestampTypeSeveritySourceUserCorrelationTags
    +
    +
    +
    + ); +} + +function Th({ children, className }: { children: React.ReactNode; className?: string }) { + return
    {children}
    + +
    + + {ts.time} + + + {ts.date} + +
    +
    + + + {AUDIT_EVENT_TYPE_LABELS[row.eventType]} + + + + {AUDIT_SEVERITY_LABELS[row.severity]} + + + + {row.source ?? "—"} + + +
    + {row.userName ?? "—"} + {row.userId && ( +
    + {row.userId.slice(0, 8)}… +
    + )} +
    +
    + {row.correlationId ? ( + + {row.correlationId.length > 14 ? `${row.correlationId.slice(0, 14)}…` : row.correlationId} + + ) : ( + + )} + + {tags.length === 0 ? ( + + ) : ( +
    + {tags.slice(0, 2).map((name) => ( + + + {name} + + ))} + {tags.length > 2 && ( + + +{tags.length - 2} + + )} +
    + )} +
    + +