Data changes and vibe UI theme

This commit is contained in:
2026-06-17 23:33:09 -04:00
parent 4d45094492
commit 34cd7a9f13
19 changed files with 6130 additions and 4912 deletions
+4
View File
@@ -7,4 +7,8 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Model\Model.csproj" />
</ItemGroup>
</Project> </Project>
+97 -44
View File
@@ -1,16 +1,15 @@
using System.Text; using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Text.Json.Serialization; using Chrono.Model;
var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..")); var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
var docsDir = Path.Combine(repoRoot, "chrono.docs"); var docsDir = Path.Combine(repoRoot, "chrono.docs");
var webWwwRoot = Path.Combine(repoRoot, "Chrono", "Web", "wwwroot"); var webWwwRoot = Path.Combine(repoRoot, "Chrono", "Web", "wwwroot");
var generatedFile = Path.Combine(repoRoot, "Chrono", "Web", "Generated", "Cards.g.cs");
Console.WriteLine($"Repo root: {repoRoot}"); Console.WriteLine($"Repo root: {repoRoot}");
Console.WriteLine($"Docs dir: {docsDir}"); Console.WriteLine($"Docs dir: {docsDir}");
Console.WriteLine($"Web wwwroot: {webWwwRoot}"); Console.WriteLine($"Generated: {generatedFile}");
if (!Directory.Exists(docsDir)) if (!Directory.Exists(docsDir))
{ {
@@ -38,6 +37,10 @@ foreach (var file in mdFiles)
var category = yaml.GetValueOrDefault("category"); var category = yaml.GetValueOrDefault("category");
if (category == null) continue; if (category == null) continue;
var imageFile = StripWikiLink(yaml.GetValueOrDefault("imageLink"));
if (imageFile != null && !imageFile.EndsWith(".png"))
imageFile += ".png";
var card = new CardData var card = new CardData
{ {
Name = name, Name = name,
@@ -53,12 +56,9 @@ foreach (var file in mdFiles)
ImmortalizeTo = yaml.ContainsKey("immortalizeTo") ? ParseListOrScalar(yaml, "immortalizeTo").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList() : null, ImmortalizeTo = yaml.ContainsKey("immortalizeTo") ? ParseListOrScalar(yaml, "immortalizeTo").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList() : null,
ImmortalizeFrom = StripWikiLink(yaml.GetValueOrDefault("immortalizeFrom")), ImmortalizeFrom = StripWikiLink(yaml.GetValueOrDefault("immortalizeFrom")),
ImmortalizeWhen = NullIfNa(StripWikiLinks(yaml.GetValueOrDefault("immortalizeWhen"))), ImmortalizeWhen = NullIfNa(StripWikiLinks(yaml.GetValueOrDefault("immortalizeWhen"))),
ImageFile = StripWikiLink(yaml.GetValueOrDefault("imageLink")), ImageFile = imageFile,
}; };
if (card.ImageFile != null && !card.ImageFile.EndsWith(".png"))
card.ImageFile += ".png";
cards.Add(card); cards.Add(card);
} }
@@ -74,20 +74,47 @@ foreach (var card in cards)
File.Copy(src, dst, overwrite: true); File.Copy(src, dst, overwrite: true);
} }
// Write cards.json // Generate C# source file
var output = new { cards }; Directory.CreateDirectory(Path.GetDirectoryName(generatedFile)!);
var json = JsonSerializer.Serialize(output, new JsonSerializerOptions using var writer = new StreamWriter(generatedFile, false, Encoding.UTF8);
{ writer.WriteLine("// <auto-generated/>");
WriteIndented = true, writer.WriteLine("#nullable enable");
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, writer.WriteLine();
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, writer.WriteLine("namespace Chrono.Model;");
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, writer.WriteLine();
}); writer.WriteLine("public static class CardDatabase");
var jsonPath = Path.Combine(webWwwRoot, "sample-data", "cards.json"); writer.WriteLine("{");
File.WriteAllText(jsonPath, json); writer.WriteLine(" public static readonly System.Collections.Generic.List<CardData> Cards =");
writer.WriteLine(" [");
Console.WriteLine($"Processed {cards.Count} cards"); for (int i = 0; i < cards.Count; i++)
Console.WriteLine($"Written to {jsonPath}"); {
var c = cards[i];
writer.WriteLine(" new()");
writer.WriteLine(" {");
WriteProp(writer, "Name", c.Name, 3);
WriteProp(writer, "Category", c.Category, 3);
WriteNullProp(writer, "Cost", c.Cost, 3);
WriteNullProp(writer, "Attack", c.Attack, 3);
WriteNullProp(writer, "Health", c.Health, 3);
WriteStrProp(writer, "Description", c.Description, 3);
WriteStrProp(writer, "Faction", c.Faction, 3);
WriteStrProp(writer, "Set", c.Set, 3);
WriteStrProp(writer, "Speed", c.Speed, 3);
WriteListProp(writer, "Archetypes", c.Archetypes, 3);
WriteListProp(writer, "ImmortalizeTo", c.ImmortalizeTo, 3);
WriteStrProp(writer, "ImmortalizeFrom", c.ImmortalizeFrom, 3);
WriteStrProp(writer, "ImmortalizeWhen", c.ImmortalizeWhen, 3);
WriteStrProp(writer, "ImageFile", c.ImageFile, 3);
var comma = i < cards.Count - 1 ? "," : "";
writer.WriteLine($" }}{comma}");
}
writer.WriteLine(" ];");
writer.WriteLine("}");
Console.WriteLine($"Generated {cards.Count} cards in {generatedFile}");
return 0; return 0;
// --- Helpers --- // --- Helpers ---
@@ -105,14 +132,14 @@ static string? StripWikiLink(string? s)
return Regex.Replace(s, @"\[\[([^\]]*)\]\]", "$1").Trim('"'); return Regex.Replace(s, @"\[\[([^\]]*)\]\]", "$1").Trim('"');
} }
static string? NullIfNa(string? s) => s is "N/A" or null ? null : s.Trim('"');
static string? StripWikiLinks(string? s) static string? StripWikiLinks(string? s)
{ {
if (s == null || s == "N/A") return null; if (s == null || s == "N/A") return null;
return Regex.Replace(s.Trim('"'), @"\[\[([^\]]*)\]\]", "$1").Trim(); return Regex.Replace(s.Trim('"'), @"\[\[([^\]]*)\]\]", "$1").Trim();
} }
static string? NullIfNa(string? s) => s is "N/A" or null ? null : s.Trim('"');
static List<string> ParseList(Dictionary<string, string> yaml, string key) static List<string> ParseList(Dictionary<string, string> yaml, string key)
{ {
if (!yaml.TryGetValue(key, out var raw)) return []; if (!yaml.TryGetValue(key, out var raw)) return [];
@@ -171,12 +198,7 @@ static Dictionary<string, string> ParseYaml(string yaml)
currentKey = trimmed[..colonIndex].Trim(); currentKey = trimmed[..colonIndex].Trim();
var value = trimmed[(colonIndex + 1)..].Trim(); var value = trimmed[(colonIndex + 1)..].Trim();
if (value.Length == 0) if (value.Length == 0) continue;
{
// Could be a list or multi-line value starting on next line
continue;
}
dict[currentKey] = value; dict[currentKey] = value;
} }
@@ -186,20 +208,51 @@ static Dictionary<string, string> ParseYaml(string yaml)
return dict; return dict;
} }
record CardData static void WriteProp(StreamWriter w, string name, string value, int indent)
{ {
[JsonPropertyName("name")] public string Name { get; set; } = ""; w.WriteLine($"{new string(' ', indent * 4)}{name} = {ToLiteral(value)},");
[JsonPropertyName("category")] public string Category { get; set; } = ""; }
[JsonPropertyName("cost")] public int? Cost { get; set; }
[JsonPropertyName("attack")] public int? Attack { get; set; } static void WriteStrProp(StreamWriter w, string name, string? value, int indent)
[JsonPropertyName("health")] public int? Health { get; set; } {
[JsonPropertyName("description")] public string? Description { get; set; } if (value == null) return;
[JsonPropertyName("faction")] public string? Faction { get; set; } w.WriteLine($"{new string(' ', indent * 4)}{name} = {ToLiteral(value)},");
[JsonPropertyName("set")] public string? Set { get; set; } }
[JsonPropertyName("speed")] public string? Speed { get; set; }
[JsonPropertyName("archetypes")] public List<string>? Archetypes { get; set; } static void WriteNullProp(StreamWriter w, string name, int? value, int indent)
[JsonPropertyName("immortalizeTo")] public List<string>? ImmortalizeTo { get; set; } {
[JsonPropertyName("immortalizeFrom")] public string? ImmortalizeFrom { get; set; } if (value == null) return;
[JsonPropertyName("immortalizeWhen")] public string? ImmortalizeWhen { get; set; } w.WriteLine($"{new string(' ', indent * 4)}{name} = {value},");
[JsonPropertyName("imageFile")] public string? ImageFile { get; set; } }
static void WriteListProp(StreamWriter w, string name, List<string>? values, int indent)
{
if (values == null) return;
var pad = new string(' ', indent * 4);
w.WriteLine($"{pad}{name} = [");
foreach (var v in values)
w.WriteLine($"{pad} {ToLiteral(v)},");
w.WriteLine($"{pad}],");
}
static string ToLiteral(string? s)
{
if (s == null) return "null";
var sb = new StringBuilder();
sb.Append('"');
foreach (char c in s)
{
switch (c)
{
case '"': sb.Append("\\\""); break;
case '\\': sb.Append("\\\\"); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
case '\0': sb.Append("\\0"); break;
default: sb.Append(c); break;
}
}
sb.Append('"');
return sb.ToString();
} }
+6
View File
@@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web", "Web\Web.csproj", "{1
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "Build\Build.csproj", "{36E3775C-0E28-4EAE-AE92-4FB493E3787F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "Build\Build.csproj", "{36E3775C-0E28-4EAE-AE92-4FB493E3787F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Model", "Model\Model.csproj", "{3358AF7A-603B-4BAC-A7F5-07978FB87843}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -18,5 +20,9 @@ Global
{36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Debug|Any CPU.Build.0 = Debug|Any CPU {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Release|Any CPU.ActiveCfg = Release|Any CPU {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Release|Any CPU.Build.0 = Release|Any CPU {36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Release|Any CPU.Build.0 = Release|Any CPU
{3358AF7A-603B-4BAC-A7F5-07978FB87843}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3358AF7A-603B-4BAC-A7F5-07978FB87843}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3358AF7A-603B-4BAC-A7F5-07978FB87843}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3358AF7A-603B-4BAC-A7F5-07978FB87843}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal
+29
View File
@@ -0,0 +1,29 @@
namespace Chrono.Model;
public class CardData
{
public string Name { get; init; } = "";
public string Category { get; init; } = "";
public int? Cost { get; init; }
public int? Attack { get; init; }
public int? Health { get; init; }
public string? Description { get; init; }
public string? Faction { get; init; }
public string? Set { get; init; }
public string? Speed { get; init; }
public List<string> Archetypes { get; init; } = [];
public List<string>? ImmortalizeTo { get; init; }
public string? ImmortalizeFrom { get; init; }
public string? ImmortalizeWhen { get; init; }
public string? ImageFile { get; init; }
public bool IsAgent => Category == "Agent";
public bool IsSpell => Category == "Spell";
public bool IsToken => Category == "Token";
public string? CostDisplay => Cost?.ToString();
public string? AttackDisplay => Attack?.ToString();
public string? HealthDisplay => Health?.ToString();
public bool HasImmortalize => ImmortalizeTo is { Count: > 0 };
public bool IsImmortalized => ImmortalizeFrom != null;
public string ImagePath => $"cards/{ImageFile ?? "placeholder.png"}";
}
+9
View File
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -5,10 +5,6 @@
</div> </div>
<main> <main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4"> <article class="content px-4">
@Body @Body
</article> </article>
-10
View File
@@ -14,16 +14,6 @@
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink> </NavLink>
</div> </div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
<div class="nav-item px-3"> <div class="nav-item px-3">
<NavLink class="nav-link" href="cards"> <NavLink class="nav-link" href="cards">
<span class="bi bi-collection-fill-nav-menu" aria-hidden="true"></span> Cards <span class="bi bi-collection-fill-nav-menu" aria-hidden="true"></span> Cards
-37
View File
@@ -1,37 +0,0 @@
using System.Text.Json.Serialization;
namespace Web.Models;
public class CardCatalog
{
[JsonPropertyName("cards")]
public List<CardData> Cards { get; set; } = [];
}
public class CardData
{
[JsonPropertyName("name")] public string Name { get; set; } = "";
[JsonPropertyName("category")] public string Category { get; set; } = "";
[JsonPropertyName("cost")] public int? Cost { get; set; }
[JsonPropertyName("attack")] public int? Attack { get; set; }
[JsonPropertyName("health")] public int? Health { get; set; }
[JsonPropertyName("description")] public string? Description { get; set; }
[JsonPropertyName("faction")] public string? Faction { get; set; }
[JsonPropertyName("set")] public string? Set { get; set; }
[JsonPropertyName("speed")] public string? Speed { get; set; }
[JsonPropertyName("archetypes")] public List<string>? Archetypes { get; set; }
[JsonPropertyName("immortalizeTo")] public List<string>? ImmortalizeTo { get; set; }
[JsonPropertyName("immortalizeFrom")] public string? ImmortalizeFrom { get; set; }
[JsonPropertyName("immortalizeWhen")] public string? ImmortalizeWhen { get; set; }
[JsonPropertyName("imageFile")] public string? ImageFile { get; set; }
public bool IsAgent => Category == "Agent";
public bool IsSpell => Category == "Spell";
public bool IsToken => Category == "Token";
public string? CostDisplay => Cost?.ToString();
public string? AttackDisplay => Attack.HasValue ? Attack.Value.ToString() : null;
public string? HealthDisplay => Health.HasValue ? Health.Value.ToString() : null;
public bool HasImmortalize => ImmortalizeTo is { Count: > 0 };
public bool IsImmortalized => ImmortalizeFrom != null;
public string ImagePath => $"cards/{ImageFile ?? "placeholder.png"}";
}
+166 -88
View File
@@ -1,72 +1,129 @@
@page "/cards" @page "/cards"
@using System.Text.Json @using Chrono.Model
@using Web.Models
@inject HttpClient Http
<PageTitle>Card Gallery</PageTitle> <PageTitle>Card Gallery</PageTitle>
<div class="gallery-container"> <div class="gallery-container">
<h1 class="mb-3">Card Gallery</h1> <div class="gallery-header">
<h1>Card Gallery</h1>
<div class="row g-2 mb-3 filters"> <p class="text-secondary">Browse all @allCards.Count cards</p>
<div class="col-md-4">
<input @bind="search" @bind:event="oninput" class="form-control" placeholder="Search cards..." />
</div>
<div class="col-md-2">
<select @bind="categoryFilter" class="form-select">
<option value="">All Categories</option>
<option value="Agent">Agents</option>
<option value="Spell">Spells</option>
<option value="Token">Tokens</option>
</select>
</div>
<div class="col-md-2">
<select @bind="factionFilter" class="form-select">
<option value="">All Factions</option>
@foreach (var f in factions)
{
<option value="@f">@f</option>
}
</select>
</div>
<div class="col-md-2">
<select @bind="costFilter" class="form-select">
<option value="">All Costs</option>
@for (int i = 0; i <= 12; i++)
{
<option value="@i">@i</option>
}
</select>
</div>
<div class="col-md-2">
<button class="btn btn-outline-secondary w-100" @onclick="ClearFilters">Clear</button>
</div>
</div> </div>
<div class="text-muted mb-2">@filteredCards.Count() cards shown</div> <div class="category-tabs">
<button class="tab @(categoryFilter == "" ? "active" : "")" @onclick='@(() => SetCategory(""))'>
<i class="bi bi-grid-3x3-gap-fill"></i> All
</button>
<button class="tab agent @(categoryFilter == "Agent" ? "active" : "")" @onclick='@(() => SetCategory("Agent"))'>
<i class="bi bi-person-fill"></i> Agents
</button>
<button class="tab spell @(categoryFilter == "Spell" ? "active" : "")" @onclick='@(() => SetCategory("Spell"))'>
<i class="bi bi-wand"></i> Spells
</button>
<button class="tab token @(categoryFilter == "Token" ? "active" : "")" @onclick='@(() => SetCategory("Token"))'>
<i class="bi bi-coin"></i> Tokens
</button>
</div>
<div class="filter-bar">
<div class="search-wrapper">
<i class="bi bi-search search-icon"></i>
<input @bind="search" @bind:event="oninput" class="form-control search-input" placeholder="Search cards by name or description..." />
@if (search.Length > 0)
{
<button class="search-clear" @onclick="ClearSearch"><i class="bi bi-x-lg"></i></button>
}
</div>
<select @bind="factionFilter" class="form-select filter-select">
<option value="">All Factions</option>
@foreach (var f in factions)
{
<option value="@f">@f</option>
}
</select>
<select @bind="costFilter" class="form-select filter-select">
<option value="">All Costs</option>
@for (int i = 0; i <= 12; i++)
{
<option value="@i">@i</option>
}
</select>
</div>
@if (HasActiveFilters)
{
<div class="active-filters">
<span class="filter-label">Filters:</span>
@if (categoryFilter != "")
{
<span class="filter-chip">
<i class="bi bi-tag-fill"></i> @categoryFilter
<button @onclick="ClearCategoryFilter"><i class="bi bi-x"></i></button>
</span>
}
@if (factionFilter != "")
{
<span class="filter-chip">
<i class="bi bi-flag-fill"></i> @factionFilter
<button @onclick="ClearFactionFilter"><i class="bi bi-x"></i></button>
</span>
}
@if (costFilter != "")
{
<span class="filter-chip">
<i class="bi bi-lightning-fill"></i> Cost @costFilter
<button @onclick="ClearCostFilter"><i class="bi bi-x"></i></button>
</span>
}
<button class="clear-all" @onclick="ClearFilters">Clear all</button>
</div>
}
<div class="result-count">
<span>@filteredCards.Count() card@(filteredCards.Count() != 1 ? "s" : "") found</span>
@if (HasActiveFilters || search != "")
{
<span class="text-muted">out of @allCards.Count total</span>
}
</div>
@if (filteredCards.Any()) @if (filteredCards.Any())
{ {
<div class="card-grid"> <div class="card-grid">
@{ int idx = 0; }
@foreach (var card in filteredCards) @foreach (var card in filteredCards)
{ {
<div class="card-cell @(selectedCard == card ? "selected" : "")" <div class="card-cell @(selectedCard == card ? "selected" : "")"
style="--i: @idx"
@onclick="() => SelectCard(card)"> @onclick="() => SelectCard(card)">
<div class="card-image-wrapper"> <div class="card-image-wrapper">
<div class="card-shimmer"></div>
<img src="@card.ImagePath" alt="@card.Name" loading="lazy" <img src="@card.ImagePath" alt="@card.Name" loading="lazy"
onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22200%22 height=%22280%22><rect fill=%22%23333%22 width=%22200%22 height=%22280%22/><text fill=%22%23999%22 font-size=%2214%22 x=%22100%22 y=%22140%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22>No Image</text></svg>'"/> onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22200%22 height=%22280%22><rect fill=%22%23222244%22 width=%22200%22 height=%22280%22/><text fill=%22%23686888%22 font-size=%2214%22 x=%22100%22 y=%22140%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22>No Image</text></svg>'" />
@if (card.Cost.HasValue)
{
<div class="card-cost-badge">@card.Cost</div>
}
@if (card.HasImmortalize)
{
<div class="card-immortalize-badge" title="Immortalizes"><i class="bi bi-star-fill"></i></div>
}
</div> </div>
<div class="card-label"> <div class="card-label">
<div class="card-name">@card.Name</div> <div class="card-name">@card.Name</div>
<div class="card-cost">@(card.CostDisplay != null ? $"⚡{card.CostDisplay}" : "")</div> <div class="card-category-badge @card.Category?.ToLowerInvariant()">@card.Category</div>
</div> </div>
</div> </div>
idx++;
} }
</div> </div>
} }
else else
{ {
<div class="alert alert-info">No cards match your filters.</div> <div class="empty-state">
<i class="bi bi-search"></i>
<p>No cards match your filters.</p>
<button class="btn btn-outline-secondary" @onclick="ClearFilters">Reset Filters</button>
</div>
} }
</div> </div>
@@ -74,59 +131,83 @@
{ {
<div class="modal-backdrop" @onclick="CloseDetail"></div> <div class="modal-backdrop" @onclick="CloseDetail"></div>
<div class="card-detail"> <div class="card-detail">
<button class="btn-close detail-close" @onclick="CloseDetail"></button> <button class="detail-close" @onclick="CloseDetail"><i class="bi bi-x-lg"></i></button>
<div class="detail-layout"> <div class="detail-layout">
<div class="detail-image"> <div class="detail-image">
<img src="@selectedCard.ImagePath" alt="@selectedCard.Name" /> <img src="@selectedCard.ImagePath" alt="@selectedCard.Name" />
</div> </div>
<div class="detail-info"> <div class="detail-info">
<h2>@selectedCard.Name</h2> <div class="detail-header">
<div class="detail-meta"> <h2>@selectedCard.Name</h2>
<span class="badge bg-primary">@selectedCard.Category</span> <div class="detail-meta">
@if (selectedCard.Cost.HasValue) <span class="meta-badge category @selectedCard.Category?.ToLowerInvariant()">@selectedCard.Category</span>
{ @if (selectedCard.Cost.HasValue)
<span class="badge bg-warning text-dark">Cost: @selectedCard.Cost</span> {
} <span class="meta-badge cost"><i class="bi bi-lightning-fill"></i> @selectedCard.Cost</span>
@if (selectedCard.Attack.HasValue) }
{ @if (selectedCard.Attack.HasValue)
<span class="badge bg-danger">ATK: @selectedCard.Attack</span> {
} <span class="meta-badge attack"><i class="bi bi-crosshair"></i> @selectedCard.Attack</span>
@if (selectedCard.Health.HasValue) }
{ @if (selectedCard.Health.HasValue)
<span class="badge bg-success">HP: @selectedCard.Health</span> {
} <span class="meta-badge health"><i class="bi bi-heart-fill"></i> @selectedCard.Health</span>
@if (selectedCard.Speed != null) }
{ @if (selectedCard.Speed != null)
<span class="badge bg-info text-dark">@selectedCard.Speed</span> {
} <span class="meta-badge speed"><i class="bi bi-wind"></i> @selectedCard.Speed</span>
}
</div>
</div> </div>
@if (selectedCard.Faction != null) @if (selectedCard.Faction != null)
{ {
<p><strong>Faction:</strong> @selectedCard.Faction</p> <div class="detail-field">
<span class="field-label"><i class="bi bi-flag-fill"></i> Faction</span>
<span class="field-value">@selectedCard.Faction</span>
</div>
} }
@if (selectedCard.Description != null) @if (selectedCard.Description != null)
{ {
<p><strong>Description:</strong> @selectedCard.Description</p> <div class="detail-field description">
<span class="field-label"><i class="bi bi-chat-quote-fill"></i></span>
<span class="field-value">@selectedCard.Description</span>
</div>
} }
@if (selectedCard.Set != null) @if (selectedCard.Set != null)
{ {
<p><strong>Set:</strong> @selectedCard.Set</p> <div class="detail-field">
<span class="field-label"><i class="bi bi-collection"></i> Set</span>
<span class="field-value">@selectedCard.Set</span>
</div>
} }
@if (selectedCard.Archetypes is { Count: > 0 }) @if (selectedCard.Archetypes is { Count: > 0 })
{ {
<p><strong>Archetypes:</strong> @string.Join(", ", selectedCard.Archetypes)</p> <div class="detail-field">
<span class="field-label"><i class="bi bi-layers-fill"></i> Archetypes</span>
<span class="field-value">@string.Join(", ", selectedCard.Archetypes)</span>
</div>
} }
@if (selectedCard.ImmortalizeWhen != null) @if (selectedCard.ImmortalizeWhen != null)
{ {
<p><strong>Immortalize When:</strong> @selectedCard.ImmortalizeWhen</p> <div class="detail-field">
<span class="field-label"><i class="bi bi-star-fill"></i> Immortalize When</span>
<span class="field-value">@selectedCard.ImmortalizeWhen</span>
</div>
} }
@if (selectedCard.HasImmortalize) @if (selectedCard.HasImmortalize)
{ {
<p><strong>Immortalizes To:</strong> @string.Join(", ", selectedCard.ImmortalizeTo!)</p> <div class="detail-field">
<span class="field-label"><i class="bi bi-arrow-right-circle-fill"></i> Immortalizes To</span>
<span class="field-value">@string.Join(", ", selectedCard.ImmortalizeTo!)</span>
</div>
} }
@if (selectedCard.ImmortalizeFrom != null) @if (selectedCard.ImmortalizeFrom != null)
{ {
<p><strong>Immortalizes From:</strong> @selectedCard.ImmortalizeFrom</p> <div class="detail-field">
<span class="field-label"><i class="bi bi-arrow-left-circle-fill"></i> Immortalizes From</span>
<span class="field-value">@selectedCard.ImmortalizeFrom</span>
</div>
} }
</div> </div>
</div> </div>
@@ -143,26 +224,17 @@
private CardData? selectedCard; private CardData? selectedCard;
private List<string> factions = []; private List<string> factions = [];
protected override async Task OnInitializedAsync() private bool HasActiveFilters => categoryFilter != "" || factionFilter != "" || costFilter != "";
protected override void OnInitialized()
{ {
try allCards = CardDatabase.Cards;
{ factions = allCards
var catalog = await Http.GetFromJsonAsync<CardCatalog>("sample-data/cards.json"); .Select(c => c.Faction)
if (catalog?.Cards != null) .Where(f => f != null)
{ .Distinct()
allCards = catalog.Cards; .OrderBy(f => f)
factions = allCards .ToList()!;
.Select(c => c.Faction)
.Where(f => f != null)
.Distinct()
.OrderBy(f => f)
.ToList()!;
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to load cards: {ex.Message}");
}
} }
private IEnumerable<CardData> ApplyFilters() private IEnumerable<CardData> ApplyFilters()
@@ -177,6 +249,12 @@
); );
} }
private void SetCategory(string cat) => categoryFilter = categoryFilter == cat ? "" : cat;
private void ClearCategoryFilter() => categoryFilter = "";
private void ClearFactionFilter() => factionFilter = "";
private void ClearCostFilter() => costFilter = "";
private void ClearSearch() => search = "";
private void SelectCard(CardData card) => selectedCard = card; private void SelectCard(CardData card) => selectedCard = card;
private void CloseDetail() => selectedCard = null; private void CloseDetail() => selectedCard = null;
+558 -49
View File
@@ -1,54 +1,336 @@
.gallery-container { .gallery-container {
padding: 1rem; max-width: 1400px;
margin: 0 auto;
padding: 1.5rem;
} }
/* ── Header ── */
.gallery-header {
margin-bottom: 1.5rem;
}
.gallery-header h1 {
margin-bottom: 0.25rem;
}
.gallery-header p {
font-size: 0.9rem;
margin: 0;
}
/* ── Category Tabs ── */
.category-tabs {
display: flex;
gap: 0.4rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.tab {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.5rem 1rem;
border: 1px solid var(--border);
border-radius: 100px;
background: transparent;
color: var(--text-secondary);
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition);
font-family: inherit;
}
.tab:hover {
background: var(--bg-hover);
color: var(--text-primary);
border-color: var(--text-muted);
}
.tab.active {
background: var(--bg-elevated);
color: var(--text-primary);
border-color: var(--accent);
box-shadow: 0 0 12px var(--accent-glow);
}
.tab.active.agent {
border-color: #4fc3f7;
box-shadow: 0 0 12px rgba(79, 195, 247, 0.3);
}
.tab.active.spell {
border-color: #ce93d8;
box-shadow: 0 0 12px rgba(206, 147, 216, 0.3);
}
.tab.active.token {
border-color: #ffd54f;
box-shadow: 0 0 12px rgba(255, 213, 79, 0.3);
}
.tab i {
font-size: 0.9rem;
}
/* ── Filter Bar ── */
.filter-bar {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
align-items: stretch;
}
.search-wrapper {
position: relative;
flex: 1;
min-width: 0;
}
.search-icon {
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
z-index: 3;
pointer-events: none;
font-size: 0.85rem;
}
.search-input {
padding-left: 2.2rem;
padding-right: 2.2rem;
height: 100%;
}
.search-clear {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 0.2rem;
display: flex;
align-items: center;
font-size: 0.7rem;
z-index: 3;
}
.search-clear:hover {
color: var(--text-primary);
}
.filter-select {
width: auto;
min-width: 140px;
}
/* ── Active Filter Chips ── */
.active-filters {
display: flex;
align-items: center;
gap: 0.4rem;
margin-bottom: 0.75rem;
flex-wrap: wrap;
font-size: 0.85rem;
}
.filter-label {
color: var(--text-muted);
font-weight: 500;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.filter-chip {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.25rem 0.5rem;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 100px;
color: var(--text-secondary);
font-size: 0.8rem;
}
.filter-chip i:first-child {
font-size: 0.7rem;
}
.filter-chip button {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
font-size: 0.65rem;
line-height: 1;
}
.filter-chip button:hover {
color: var(--text-primary);
}
.clear-all {
background: none;
border: none;
color: var(--accent);
cursor: pointer;
font-size: 0.8rem;
padding: 0.25rem 0.5rem;
font-family: inherit;
}
.clear-all:hover {
text-decoration: underline;
}
/* ── Result Count ── */
.result-count {
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 1rem;
display: flex;
gap: 0.3rem;
}
/* ── Card Grid ── */
.card-grid { .card-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
gap: 1rem; gap: 1rem;
} }
/* ── Card Cell ── */
.card-cell { .card-cell {
cursor: pointer; cursor: pointer;
border-radius: 8px; border-radius: var(--radius);
overflow: hidden; overflow: hidden;
background: #1a1a2e; background: var(--bg-surface);
transition: transform 0.15s, box-shadow 0.15s; transition: transform 0.25s ease, box-shadow 0.25s ease;
border: 2px solid transparent; border: 2px solid transparent;
animation: card-enter 0.4s ease-out both;
animation-delay: calc(var(--i, 0) * 25ms);
will-change: transform;
} }
.card-cell:hover { .card-cell:hover {
transform: translateY(-3px); transform: translateY(-6px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); box-shadow: 0 8px 30px rgba(108, 99, 255, 0.2);
border-color: rgba(108, 99, 255, 0.2);
} }
.card-cell.selected { .card-cell.selected {
border-color: #ffd700; border-color: var(--gold);
box-shadow: 0 0 12px rgba(255, 215, 0, 0.5); box-shadow: 0 0 20px var(--gold-glow);
} }
.card-cell.selected:hover {
box-shadow: 0 8px 30px var(--gold-glow);
}
@keyframes card-enter {
from {
opacity: 0;
transform: translateY(16px) scale(0.97);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* ── Card Image ── */
.card-image-wrapper { .card-image-wrapper {
position: relative;
width: 100%; width: 100%;
aspect-ratio: 5 / 7; aspect-ratio: 5 / 7;
overflow: hidden; overflow: hidden;
background: #16213e; background: #111128;
}
.card-shimmer {
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.03) 50%, transparent 100%);
background-size: 200% 100%;
animation: shimmer 2.5s infinite;
pointer-events: none;
z-index: 0;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
} }
.card-image-wrapper img { .card-image-wrapper img {
position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
display: block; display: block;
z-index: 1;
} }
.card-cost-badge {
position: absolute;
top: 0.4rem;
left: 0.4rem;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
color: var(--gold);
font-weight: 700;
font-size: 0.8rem;
padding: 0.15rem 0.5rem;
border-radius: 100px;
border: 1px solid rgba(255, 215, 0, 0.3);
z-index: 2;
line-height: 1.4;
}
.card-cost-badge::before {
content: "⚡";
margin-right: 0.15rem;
}
.card-immortalize-badge {
position: absolute;
top: 0.4rem;
right: 0.4rem;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
color: var(--gold);
font-size: 0.7rem;
padding: 0.25rem;
border-radius: 50%;
z-index: 2;
width: 1.5rem;
height: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(255, 215, 0, 0.3);
}
/* ── Card Label ── */
.card-label { .card-label {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 0.3rem;
padding: 0.4rem 0.6rem; padding: 0.4rem 0.6rem;
background: #0f3460; background: var(--bg-elevated);
color: #eee; min-height: 2.4rem;
font-size: 0.8rem;
} }
.card-name { .card-name {
@@ -57,17 +339,73 @@
white-space: nowrap; white-space: nowrap;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
font-size: 0.8rem;
font-weight: 500;
color: var(--text-primary);
} }
.card-cost { .card-category-badge {
flex-shrink: 0; flex-shrink: 0;
margin-left: 0.4rem; font-size: 0.65rem;
font-weight: bold; font-weight: 600;
color: #ffd700; padding: 0.15rem 0.45rem;
border-radius: 100px;
text-transform: uppercase;
letter-spacing: 0.04em;
background: var(--bg-hover);
color: var(--text-secondary);
border: 1px solid var(--border);
} }
.filters { .card-category-badge.agent {
align-items: stretch; background: rgba(79, 195, 247, 0.15);
color: #4fc3f7;
border-color: rgba(79, 195, 247, 0.3);
}
.card-category-badge.spell {
background: rgba(206, 147, 216, 0.15);
color: #ce93d8;
border-color: rgba(206, 147, 216, 0.3);
}
.card-category-badge.token {
background: rgba(255, 213, 79, 0.15);
color: #ffd54f;
border-color: rgba(255, 213, 79, 0.3);
}
/* ── Empty State ── */
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: var(--text-muted);
}
.empty-state i {
font-size: 2.5rem;
display: block;
margin-bottom: 1rem;
}
.empty-state p {
font-size: 1.1rem;
margin-bottom: 1rem;
}
/* ── Detail Modal ── */
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 1040;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
animation: fade-in 0.2s ease-out;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
} }
.card-detail { .card-detail {
@@ -76,23 +414,51 @@
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 1050; z-index: 1050;
background: #1a1a2e; background: var(--bg-surface);
border-radius: 12px; border: 1px solid var(--border);
border-radius: 16px;
padding: 0; padding: 0;
max-width: 700px; max-width: 720px;
width: 90vw; width: 92vw;
max-height: 85vh; max-height: 88vh;
overflow-y: auto; overflow-y: auto;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
color: #eee; color: var(--text-primary);
animation: detail-enter 0.25s ease-out;
}
@keyframes detail-enter {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(0.92);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
} }
.detail-close { .detail-close {
position: absolute; position: absolute;
top: 0.5rem; top: 0.75rem;
right: 0.5rem; right: 0.75rem;
z-index: 1; z-index: 1;
filter: invert(1); background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--text-primary);
width: 2rem;
height: 2rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 0.8rem;
transition: all var(--transition);
}
.detail-close:hover {
background: rgba(255, 255, 255, 0.15);
} }
.detail-layout { .detail-layout {
@@ -102,12 +468,13 @@
} }
.detail-image { .detail-image {
flex: 0 0 240px; flex: 0 0 260px;
} }
.detail-image img { .detail-image img {
width: 100%; width: 100%;
border-radius: 8px; border-radius: var(--radius);
box-shadow: var(--shadow);
} }
.detail-info { .detail-info {
@@ -115,33 +482,151 @@
min-width: 0; min-width: 0;
} }
.detail-info h2 { .detail-header {
margin-top: 0; margin-bottom: 1rem;
margin-bottom: 0.75rem; }
.detail-header h2 {
margin: 0 0 0.75rem;
font-size: 1.4rem; font-size: 1.4rem;
line-height: 1.3;
} }
.detail-meta { .detail-meta {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.4rem; gap: 0.4rem;
margin-bottom: 1rem;
} }
.detail-info p { .meta-badge {
margin-bottom: 0.5rem; display: inline-flex;
font-size: 0.9rem; align-items: center;
line-height: 1.4; gap: 0.3rem;
font-size: 0.75rem;
font-weight: 600;
padding: 0.25rem 0.6rem;
border-radius: 100px;
background: var(--bg-hover);
color: var(--text-secondary);
border: 1px solid var(--border);
} }
.modal-backdrop { .meta-badge.category { }
position: fixed;
inset: 0; .meta-badge.category.agent {
z-index: 1040; background: rgba(79, 195, 247, 0.15);
background: rgba(0, 0, 0, 0.6); color: #4fc3f7;
border-color: rgba(79, 195, 247, 0.3);
} }
@media (max-width: 600px) { .meta-badge.category.spell {
background: rgba(206, 147, 216, 0.15);
color: #ce93d8;
border-color: rgba(206, 147, 216, 0.3);
}
.meta-badge.category.token {
background: rgba(255, 213, 79, 0.15);
color: #ffd54f;
border-color: rgba(255, 213, 79, 0.3);
}
.meta-badge.cost {
background: rgba(255, 215, 0, 0.12);
color: var(--gold);
border-color: rgba(255, 215, 0, 0.3);
}
.meta-badge.attack {
background: rgba(239, 83, 80, 0.15);
color: #ef5350;
border-color: rgba(239, 83, 80, 0.3);
}
.meta-badge.health {
background: rgba(102, 187, 106, 0.15);
color: #66bb6a;
border-color: rgba(102, 187, 106, 0.3);
}
.meta-badge.speed {
background: rgba(79, 195, 247, 0.12);
color: #4fc3f7;
border-color: rgba(79, 195, 247, 0.3);
}
/* ── Detail Fields ── */
.detail-field {
display: flex;
gap: 0.5rem;
margin-bottom: 0.6rem;
font-size: 0.88rem;
line-height: 1.45;
}
.detail-field.description {
background: var(--bg-elevated);
padding: 0.6rem 0.75rem;
border-radius: var(--radius-sm);
border-left: 3px solid var(--accent);
margin-top: 0.25rem;
}
.field-label {
flex-shrink: 0;
color: var(--text-muted);
font-size: 0.8rem;
font-weight: 500;
min-width: 7.5rem;
display: flex;
align-items: flex-start;
gap: 0.3rem;
}
.detail-field.description .field-label {
min-width: auto;
color: var(--accent);
}
.field-value {
color: var(--text-secondary);
}
.detail-field.description .field-value {
color: var(--text-primary);
font-style: italic;
}
/* ── Scrollbar for modal ── */
.card-detail::-webkit-scrollbar {
width: 4px;
}
.card-detail::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 2px;
}
/* ── Responsive ── */
@media (max-width: 768px) {
.gallery-container {
padding: 1rem;
}
.filter-bar {
flex-direction: column;
}
.filter-select {
width: 100%;
min-width: 0;
}
.card-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.75rem;
}
.detail-layout { .detail-layout {
flex-direction: column; flex-direction: column;
padding: 1rem; padding: 1rem;
@@ -149,12 +634,36 @@
.detail-image { .detail-image {
flex: 0 0 auto; flex: 0 0 auto;
max-width: 200px; max-width: 180px;
margin: 0 auto; margin: 0 auto;
} }
.card-grid { .card-detail {
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); max-height: 90vh;
gap: 0.75rem; }
.tab {
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
}
}
@media (max-width: 480px) {
.card-grid {
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 0.5rem;
}
.category-tabs {
gap: 0.3rem;
}
.tab {
padding: 0.35rem 0.65rem;
font-size: 0.75rem;
}
.tab i {
display: none;
} }
} }
-19
View File
@@ -1,19 +0,0 @@
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
-60
View File
@@ -1,60 +0,0 @@
@page "/weather"
@inject HttpClient Http
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
<p>
<em>Loading...</em>
</p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th>
<th aria-label="Temperature in Fahrenheit">Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
}
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
+14 -1
View File
@@ -12,11 +12,24 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all"/> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all"/>
</ItemGroup> </ItemGroup>
<Target Name="RunBuild" BeforeTargets="BeforeBuild"> <ItemGroup>
<ProjectReference Include="..\Model\Model.csproj" />
</ItemGroup>
<ItemGroup Label="Generated files">
<Compile Remove="Generated\**\*.cs" />
<Compile Include="Generated\**\*.cs" />
</ItemGroup>
<Target Name="RunBuild" BeforeTargets="CoreCompile">
<PropertyGroup> <PropertyGroup>
<_BuildProject>$(MSBuildThisFileDirectory)..\Build\Build.csproj</_BuildProject> <_BuildProject>$(MSBuildThisFileDirectory)..\Build\Build.csproj</_BuildProject>
</PropertyGroup> </PropertyGroup>
<Message Text="=== Running Build project (card metadata generation) ===" Importance="high" /> <Message Text="=== Running Build project (card metadata generation) ===" Importance="high" />
<Exec Command="dotnet run --project &quot;$(_BuildProject)&quot;" /> <Exec Command="dotnet run --project &quot;$(_BuildProject)&quot;" />
<ItemGroup>
<Compile Remove="Generated\**\*.cs" />
<Compile Include="Generated\**\*.cs" />
</ItemGroup>
</Target> </Target>
</Project> </Project>
+1
View File
@@ -1,5 +1,6 @@
@using System.Net.Http @using System.Net.Http
@using System.Net.Http.Json @using System.Net.Http.Json
@using Chrono.Model
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
+86 -97
View File
@@ -1,115 +1,104 @@
:root {
--bg-primary: #0b0b1a;
--bg-surface: #141428;
--bg-elevated: #1c1c3a;
--bg-hover: #252548;
--text-primary: #e8e8f0;
--text-secondary: #9898b8;
--text-muted: #686888;
--accent: #6c63ff;
--accent-glow: rgba(108, 99, 255, 0.3);
--gold: #ffd700;
--gold-glow: rgba(255, 215, 0, 0.4);
--border: #2a2a4a;
--radius: 10px;
--radius-sm: 6px;
--shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
--transition: 0.2s ease;
}
* {
scrollbar-width: thin;
scrollbar-color: #2a2a4a transparent;
}
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #2a2a4a; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #3a3a5a; }
html, body { html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
} }
h1:focus { h1, h2, h3, h4, h5, h6 {
outline: none; font-weight: 700;
letter-spacing: -0.02em;
} }
a, .btn-link { h1 {
color: #0071c1; font-size: 2rem;
background: linear-gradient(135deg, #e8e8f0 0%, #6c63ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
} }
a, .btn-link { color: var(--accent); }
.btn-primary { .btn-primary {
color: #fff; background: var(--accent);
background-color: #1b6ec2; border-color: var(--accent);
border-color: #1861ac;
} }
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { .btn-primary:hover {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; background: #7b73ff;
border-color: #7b73ff;
}
.btn-outline-secondary {
color: var(--text-secondary);
border-color: var(--border);
}
.btn-outline-secondary:hover {
background: var(--bg-hover);
border-color: var(--accent);
color: var(--text-primary);
}
.form-control, .form-select {
background: var(--bg-surface);
border-color: var(--border);
color: var(--text-primary);
border-radius: var(--radius-sm);
font-size: 0.9rem;
}
.form-control:focus, .form-select:focus {
background: var(--bg-elevated);
border-color: var(--accent);
color: var(--text-primary);
box-shadow: 0 0 0 0.2rem var(--accent-glow);
}
.form-control::placeholder {
color: var(--text-muted);
} }
.content { .content {
padding-top: 1.1rem; padding-top: 1.5rem;
} }
.valid.modified:not([type=checkbox]) { .sidebar {
outline: 1px solid #26b050; background-image: linear-gradient(180deg, #0d0d2b 0%, #1a0a2e 70%) !important;
} }
.invalid { .alert-info {
outline: 1px solid red; background: var(--bg-surface);
} border-color: var(--border);
color: var(--text-secondary);
.validation-message {
color: red;
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.loading-progress {
position: absolute;
display: block;
width: 8rem;
height: 8rem;
inset: 20vh 0 auto 0;
margin: 0 auto 0 auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}
code {
color: #c02d76;
}
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
} }
+4
View File
@@ -6,6 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web</title> <title>Web</title>
<base href="/" /> <base href="/" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
<link rel="preload" id="webassembly" /> <link rel="preload" id="webassembly" />
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="css/app.css" /> <link rel="stylesheet" href="css/app.css" />
File diff suppressed because it is too large Load Diff