From 111bc36fd4d20512d6546c5cf8c91a9c28b44a0a Mon Sep 17 00:00:00 2001 From: 6d486f49 Date: Thu, 18 Jun 2026 13:30:46 -0400 Subject: [PATCH] Vibed deck viewer --- Chrono/Build/Program.cs | 157 ++++++++++- Chrono/Model/DeckData.cs | 12 + Chrono/Web/Generated/Cards.g.cs | 9 + Chrono/Web/Generated/Decks.g.cs | 77 +++++ Chrono/Web/Layout/NavMenu.razor | 5 + Chrono/Web/Pages/DeckDetail.razor | 223 +++++++++++++++ Chrono/Web/Pages/DeckDetail.razor.css | 390 ++++++++++++++++++++++++++ Chrono/Web/Pages/Decks.razor | 69 +++++ Chrono/Web/Pages/Decks.razor.css | 93 ++++++ chrono.docs/.obsidian/types.json | 3 +- chrono.docs/.obsidian/workspace.json | 24 +- chrono.docs/Decks/Big Energy.md | 34 ++- 12 files changed, 1062 insertions(+), 34 deletions(-) create mode 100644 Chrono/Model/DeckData.cs create mode 100644 Chrono/Web/Generated/Decks.g.cs create mode 100644 Chrono/Web/Pages/DeckDetail.razor create mode 100644 Chrono/Web/Pages/DeckDetail.razor.css create mode 100644 Chrono/Web/Pages/Decks.razor create mode 100644 Chrono/Web/Pages/Decks.razor.css diff --git a/Chrono/Build/Program.cs b/Chrono/Build/Program.cs index 05e793b..086396a 100644 --- a/Chrono/Build/Program.cs +++ b/Chrono/Build/Program.cs @@ -30,7 +30,7 @@ foreach (var file in mdFiles) if (endIndex < 0) continue; - var frontmatter = content[3..endIndex].Trim(); + var frontmatter = content[3..endIndex].Trim().Replace("\r\n", "\n").Replace("\r", "\n"); var yaml = ParseYaml(frontmatter); var name = Path.GetFileNameWithoutExtension(file); @@ -117,6 +117,75 @@ writer.WriteLine(" ];"); writer.WriteLine("}"); Console.WriteLine($"Generated {cards.Count} cards in {generatedFile}"); + +// ── Decks ── +var decksDir = Path.Combine(docsDir, "Decks"); +var deckFiles = Directory.Exists(decksDir) ? Directory.GetFiles(decksDir, "*.md") : []; +var decks = new List(); + +foreach (var file in deckFiles) +{ + var content = Encoding.UTF8.GetString(File.ReadAllBytes(file)); + if (!content.StartsWith("---")) continue; + var endIndex = content.IndexOf("---", 3, StringComparison.Ordinal); + if (endIndex < 0) continue; + + var frontmatter = content[3..endIndex].Trim().Replace("\r\n", "\n").Replace("\r", "\n"); + var yaml = ParseYaml(frontmatter); + + var name = Path.GetFileNameWithoutExtension(file); + var isVisible = yaml.GetValueOrDefault("isVisible") == "true"; + + var deck = new DeckData + { + Name = name, + Cards = ParseList(yaml, "cards").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(), + Keycards = ParseList(yaml, "keycards").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(), + Divers = ParseList(yaml, "divers").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(), + Description = StripWikiLinks(NullIfNa(yaml.GetValueOrDefault("description")))?.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n\n", "\n").Replace("\n\n", "\n"), + Factions = ParseList(yaml, "factions").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(), + IsVisible = isVisible, + }; + + decks.Add(deck); +} + +Console.WriteLine($"Parsed {decks.Count} deck files"); + +// Generate Decks.g.cs +var deckGeneratedFile = Path.Combine(repoRoot, "Chrono", "Web", "Generated", "Decks.g.cs"); +Directory.CreateDirectory(Path.GetDirectoryName(deckGeneratedFile)!); +using var deckWriter = new StreamWriter(deckGeneratedFile, false, Encoding.UTF8); +deckWriter.WriteLine("// "); +deckWriter.WriteLine("#nullable enable"); +deckWriter.WriteLine(); +deckWriter.WriteLine("namespace Chrono.Model;"); +deckWriter.WriteLine(); +deckWriter.WriteLine("public static class DeckDatabase"); +deckWriter.WriteLine("{"); +deckWriter.WriteLine(" public static readonly System.Collections.Generic.List Decks ="); +deckWriter.WriteLine(" ["); + +for (var i = 0; i < decks.Count; i++) +{ + var d = decks[i]; + deckWriter.WriteLine(" new()"); + deckWriter.WriteLine(" {"); + WriteProp(deckWriter, "Name", d.Name, 3); + WriteListProp(deckWriter, "Cards", d.Cards, 3); + WriteListProp(deckWriter, "Keycards", d.Keycards, 3); + WriteListProp(deckWriter, "Divers", d.Divers, 3); + WriteStrProp(deckWriter, "Description", d.Description, 3); + WriteListProp(deckWriter, "Factions", d.Factions, 3); + deckWriter.WriteLine($" IsVisible = {(d.IsVisible ? "true" : "false")},"); + var comma = i < decks.Count - 1 ? "," : ""; + deckWriter.WriteLine($" }}{comma}"); +} + +deckWriter.WriteLine(" ];"); +deckWriter.WriteLine("}"); + +Console.WriteLine($"Generated {decks.Count} decks in {deckGeneratedFile}"); return 0; // --- Helpers --- @@ -181,10 +250,48 @@ static Dictionary ParseYaml(string yaml) var lines = yaml.Split('\n'); string? currentKey = null; var listBuffer = new List(); + var inBlockScalar = false; + string? blockScalarKey = null; + var blockScalarLines = new List(); + string? quoteKey = null; + var quoteLines = new List(); foreach (var line in lines) { var trimmed = line.Trim(); + + if (inBlockScalar) + { + if (line.Length == 0 || line[0] == ' ' || line[0] == '\t') + { + blockScalarLines.Add(trimmed); + continue; + } + if (blockScalarKey != null) + dict[blockScalarKey] = string.Join("\n", blockScalarLines); + inBlockScalar = false; + blockScalarKey = null; + blockScalarLines.Clear(); + } + + if (quoteKey != null) + { + if (line.Length == 0 || line[0] == ' ' || line[0] == '\t') + { + quoteLines.Add(trimmed); + if (trimmed.EndsWith("\"")) + { + dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml)).TrimEnd('"'); + quoteKey = null; + quoteLines.Clear(); + } + continue; + } + dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml)); + quoteKey = null; + quoteLines.Clear(); + } + if (trimmed.Length == 0) continue; if (trimmed.StartsWith("- ")) @@ -205,16 +312,62 @@ static Dictionary ParseYaml(string yaml) currentKey = trimmed[..colonIndex].Trim(); var value = trimmed[(colonIndex + 1)..].Trim(); - if (value.Length == 0) continue; + if (value is "|" or "|-") + { + inBlockScalar = true; + blockScalarKey = currentKey; + blockScalarLines.Clear(); + continue; + } + + if (value.StartsWith("\"") && !value.EndsWith("\"")) + { + quoteKey = currentKey; + quoteLines.Clear(); + quoteLines.Add(value.TrimStart('"')); + continue; + } + dict[currentKey] = value; } if (listBuffer.Count > 0 && currentKey != null) dict[currentKey] = string.Join("\n", listBuffer); + if (inBlockScalar && blockScalarKey != null) + dict[blockScalarKey] = string.Join("\n", blockScalarLines); + + if (quoteKey != null) + dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml)); + return dict; } +static string UnescapeYaml(string s) +{ + var sb = new StringBuilder(); + for (var i = 0; i < s.Length; i++) + { + if (s[i] == '\\' && i + 1 < s.Length) + { + switch (s[i + 1]) + { + case 'r': sb.Append('\r'); i++; break; + case 'n': sb.Append('\n'); i++; break; + case 't': sb.Append('\t'); i++; break; + case '\\': sb.Append('\\'); i++; break; + case '"': sb.Append('"'); i++; break; + default: sb.Append(s[i]); break; + } + } + else + { + sb.Append(s[i]); + } + } + return sb.ToString(); +} + static void WriteProp(StreamWriter w, string name, string value, int indent) { w.WriteLine($"{new string(' ', indent * 4)}{name} = {ToLiteral(value)},"); diff --git a/Chrono/Model/DeckData.cs b/Chrono/Model/DeckData.cs new file mode 100644 index 0000000..45c090d --- /dev/null +++ b/Chrono/Model/DeckData.cs @@ -0,0 +1,12 @@ +namespace Chrono.Model; + +public class DeckData +{ + public string Name { get; init; } = ""; + public List Cards { get; init; } = []; + public List Keycards { get; init; } = []; + public List Divers { get; init; } = []; + public string? Description { get; init; } + public List Factions { get; init; } = []; + public bool IsVisible { get; init; } +} diff --git a/Chrono/Web/Generated/Cards.g.cs b/Chrono/Web/Generated/Cards.g.cs index 50c25dc..1c18071 100644 --- a/Chrono/Web/Generated/Cards.g.cs +++ b/Chrono/Web/Generated/Cards.g.cs @@ -2028,6 +2028,7 @@ public static class CardDatabase ImmortalizeTo = [ ], ImmortalizeFrom = "Gardener Apprentice", + ImmortalizeWhen = "", }, new() { @@ -2885,6 +2886,14 @@ public static class CardDatabase ImageFile = "Overmind's Guilt.png", }, new() + { + Name = "Overpower", + Category = "Keyword", + Description = "Excess damage beyond the Durability of this Agent's blocker is dealt directly to the enemy core.", + Archetypes = [ + ], + }, + new() { Name = "Overseer of Trials", Category = "Agent", diff --git a/Chrono/Web/Generated/Decks.g.cs b/Chrono/Web/Generated/Decks.g.cs new file mode 100644 index 0000000..23c415f --- /dev/null +++ b/Chrono/Web/Generated/Decks.g.cs @@ -0,0 +1,77 @@ +// +#nullable enable + +namespace Chrono.Model; + +public static class DeckDatabase +{ + public static readonly System.Collections.Generic.List Decks = + [ + new() + { + Name = "Big Energy", + Cards = [ + "Brilliant Martyr", + "Kinetic Absorber", + "Hidden Locus", + "Suncursed Conduit", + "Swashbuckling Diehard", + "Debris Collector", + "Paradox Flow", + "Starfueled Medics", + "Lumbering Starseeker", + "Novathermal Mining", + "Radiant Channeling", + "Supernova", + "Gunnery Captain", + "Lightsteel Colossus", + "Devourer Spawn", + "Army of the Sun", + ], + Keycards = [ + "Kinetic Absorber", + "Hidden Locus", + "Debris Collector", + "Lumbering Starseeker", + "Lightsteel Colossus", + ], + Divers = [ + "Peaceful Synthesizer", + "Limit Breaker", + ], + Description = "The idea of this deck is to go heavy on stat-efficient cards. Like 2 for a 2/3 Kinetic Absorber, 5/6 Lumbering Starseeker, and 8 for a 9/14 Lightsteel Colossus. Debris Collector is also quite powerful and will Immortalize into a Living Comet 10/10 with Overpower pretty reliably, but it's also your only stat Mute target, so it might be a bit trash if that's popular in the meta.\nYou got Devourer Spawn to help push for lethal. It's going to have a bunch of keywords on it, given how popular timelines are in the meta.\nYou need to Overflow your mana once in a while, or some of your Supernova removal will be useless. Be pass heavy.\nYou're going to win on very low health if you win. Such is life.\nNo card draw, so Army of the Sun is your hope if things go long. At the worst, it's a spell that summons a big unit right away if it goes off.\nStarfueled Medics and Swashbuckling Diehard are just there to take up space and be mid-range drops. And technically, they can be win conditions, I suppose.\nAttack aggressively with Brilliant Martyr. You really want Star Siphon to ramp.\nAs divers, Peaceful Synthesizer is a 1 drop that will quickly turn into a 3/3. Can't get more stat efficient than that. And Limit Breaker is an early-game growing threat that will demand an answer from the enemy.", + Factions = [ + "Sungrace", + ], + IsVisible = true, + }, + new() + { + Name = "Rewind Me", + Cards = [ + "Curious Acolyte", + "Chronicle of the One", + "Prayer of Rescue", + "Backhand", + "Snap Back", + "Sunbringer Artillerist", + "Sunshock", + "Temple Analyst", + "Holy Cleaner", + "Balance Blade", + "Rescind Authorization", + "Out of Line", + "Divergence Assassin", + "Chronal Quarantine", + "Holder of the Instruments", + ], + Keycards = [ + ], + Divers = [ + ], + Factions = [ + ], + IsVisible = false, + } + ]; +} diff --git a/Chrono/Web/Layout/NavMenu.razor b/Chrono/Web/Layout/NavMenu.razor index 5ae94bc..34e76b0 100644 --- a/Chrono/Web/Layout/NavMenu.razor +++ b/Chrono/Web/Layout/NavMenu.razor @@ -24,6 +24,11 @@ Agents + diff --git a/Chrono/Web/Pages/DeckDetail.razor b/Chrono/Web/Pages/DeckDetail.razor new file mode 100644 index 0000000..c1085ec --- /dev/null +++ b/Chrono/Web/Pages/DeckDetail.razor @@ -0,0 +1,223 @@ +@page "/decks/{Name}" + +@deck?.Name + +
+ Back to Decks + + @if (deck == null) + { +
+ +

Deck not found.

+
+ } + else + { +
+
+

@deck.Name

+ @if (deck.Factions.Count > 0) + { +
+ @foreach (var f in deck.Factions) + { + @f + } +
+ } +

@deck.Cards.Count cards

+
+ + @if (deck.Keycards.Count > 0) + { +
+

Keycards

+
+ @foreach (var cardName in deck.Keycards) + { + var card = LookupCard(cardName); + + } +
+
+ } + + @if (deck.Divers.Count > 0) + { +
+

Divers

+
+ @foreach (var cardName in deck.Divers) + { + var card = LookupCard(cardName); + + } +
+
+ } + +
+

Cards

+
+ @foreach (var cardName in deck.Cards) + { + var card = LookupCard(cardName); + + } +
+
+ + @if (deck.Description != null) + { +
+

Description

+
+ @foreach (var paragraph in deck.Description.Split('\n', StringSplitOptions.RemoveEmptyEntries)) + { +

@paragraph

+ } +
+
+ } +
+ } +
+ +@if (selectedCard != null) +{ + +
+ +
+
+ @selectedCard.Name +
+
+
+

@selectedCard.Name

+
+ @selectedCard.Category + @if (selectedCard.Cost.HasValue) + { + @selectedCard.Cost + } + @if (selectedCard.Attack.HasValue) + { + @selectedCard.Attack + } + @if (selectedCard.Health.HasValue) + { + @selectedCard.Health + } + @if (selectedCard.Speed != null) + { + @selectedCard.Speed + } +
+
+ + @if (selectedCard.Faction != null) + { +
+ Faction + @selectedCard.Faction +
+ } + @if (selectedCard.Description != null) + { +
+ + @selectedCard.Description +
+ } + @if (selectedCard.Set != null) + { +
+ Set + @selectedCard.Set +
+ } + @if (selectedCard.Archetypes is { Count: > 0 }) + { +
+ Archetypes + @string.Join(", ", selectedCard.Archetypes) +
+ } + @if (selectedCard.ImmortalizeWhen != null) + { +
+ Immortalize When + @selectedCard.ImmortalizeWhen +
+ } + @if (selectedCard.HasImmortalize) + { +
+ Immortalizes To + @string.Join(", ", selectedCard.ImmortalizeTo!) +
+ } + @if (selectedCard.ImmortalizeFrom != null) + { +
+ Immortalizes From + @selectedCard.ImmortalizeFrom +
+ } +
+
+
+} + +@code { + [Parameter] + public string Name { get; set; } = ""; + + private DeckData? deck; + private CardData? selectedCard; + + protected override void OnParametersSet() + { + var decoded = Uri.UnescapeDataString(Name); + deck = DeckDatabase.Decks.FirstOrDefault(d => d.IsVisible && d.Name == decoded); + } + + private CardData? LookupCard(string cardName) + { + return CardDatabase.Cards.FirstOrDefault(c => + string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase)); + } + + private void SelectCard(CardData? card) + { + selectedCard = card; + } + + private void CloseDetail() + { + selectedCard = null; + } +} diff --git a/Chrono/Web/Pages/DeckDetail.razor.css b/Chrono/Web/Pages/DeckDetail.razor.css new file mode 100644 index 0000000..1390b99 --- /dev/null +++ b/Chrono/Web/Pages/DeckDetail.razor.css @@ -0,0 +1,390 @@ +.deck-detail-page { + max-width: 800px; + margin: 0 auto; + padding: 0 1rem 3rem; +} + +.back-link { + display: inline-flex; + align-items: center; + gap: 0.4rem; + color: var(--text-secondary); + text-decoration: none; + font-size: 0.85rem; + margin-bottom: 1.5rem; + transition: color var(--transition); +} + +.back-link:hover { + color: var(--accent); +} + +.deck-article { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 2rem; +} + +.deck-header { + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--border); +} + +.deck-header h1 { + margin: 0 0 0.5rem; +} + +.deck-factions { + display: flex; + gap: 0.4rem; + margin-bottom: 0.5rem; + flex-wrap: wrap; +} + +.faction-badge { + display: inline-block; + font-size: 0.75rem; + font-weight: 600; + padding: 0.2rem 0.7rem; + border-radius: 999px; + background: var(--bg-elevated); + color: var(--accent); + border: 1px solid var(--border); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.deck-card-count { + font-size: 0.85rem; + color: var(--text-muted); + margin: 0; +} + +.deck-section { + margin-bottom: 2rem; +} + +.deck-section:last-child { + margin-bottom: 0; +} + +.deck-section h2 { + font-size: 1.05rem; + font-weight: 600; + margin: 0 0 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--text-primary); +} + +.deck-card-row { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.mini-card-btn { + background: none; + border: none; + padding: 0; + cursor: pointer; + font-family: inherit; + display: block; +} + +.mini-card { + width: 110px; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + overflow: hidden; + transition: border-color var(--transition), transform var(--transition); +} + +.mini-card:hover { + border-color: var(--accent); + transform: translateY(-2px); +} + +.mini-card-img { + width: 110px; + height: 100px; + overflow: hidden; + background: var(--bg-primary); + display: flex; + align-items: center; + justify-content: center; +} + +.mini-card-img img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.mini-card-name { + font-size: 0.75rem; + font-weight: 500; + padding: 0.35rem 0.4rem; + text-align: center; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.deck-description { + font-size: 0.95rem; + line-height: 1.7; + color: var(--text-secondary); +} + +.deck-description p { + margin: 0 0 1rem; +} + +.deck-description p:last-child { + margin-bottom: 0; +} + +.empty-state { + text-align: center; + padding: 4rem 1rem; + color: var(--text-muted); +} + +.empty-state i { + font-size: 3rem; + display: block; + margin-bottom: 1rem; +} + +/* ── Detail Modal (shared with Cards page) ── */ +.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 { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1050; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 16px; + padding: 0; + max-width: 720px; + width: 92vw; + max-height: 88vh; + overflow-y: auto; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6); + 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 { + position: absolute; + top: 0.75rem; + right: 0.75rem; + z-index: 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 { + display: flex; + gap: 1.5rem; + padding: 1.5rem; +} + +.detail-image { + flex: 0 0 260px; +} + +.detail-image img { + width: 100%; + border-radius: var(--radius); + box-shadow: var(--shadow); +} + +.detail-info { + flex: 1; + min-width: 0; +} + +.detail-header { + margin-bottom: 1rem; +} + +.detail-header h2 { + margin: 0 0 0.75rem; + font-size: 1.4rem; + line-height: 1.3; +} + +.detail-meta { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + +.meta-badge { + display: inline-flex; + align-items: center; + 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); +} + +.meta-badge.category.agent { + background: rgba(79, 195, 247, 0.15); + color: #4fc3f7; + border-color: rgba(79, 195, 247, 0.3); +} + +.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-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; +} + +.card-detail::-webkit-scrollbar { + width: 4px; +} + +.card-detail::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 2px; +} + +@media (max-width: 768px) { + .detail-layout { + flex-direction: column; + padding: 1rem; + } + + .detail-image { + flex: 0 0 auto; + max-width: 180px; + margin: 0 auto; + } + + .card-detail { + max-height: 90vh; + } +} diff --git a/Chrono/Web/Pages/Decks.razor b/Chrono/Web/Pages/Decks.razor new file mode 100644 index 0000000..c2963ed --- /dev/null +++ b/Chrono/Web/Pages/Decks.razor @@ -0,0 +1,69 @@ +@page "/decks" + +Decks + + + +@code { + private List decks = []; + + protected override void OnInitialized() + { + decks = DeckDatabase.Decks.Where(d => d.IsVisible).ToList(); + } + + private static string Truncate(string text, int maxLength) + { + if (text.Length <= maxLength) return text; + var lastSpace = text.LastIndexOf(' ', maxLength); + return text[..(lastSpace > 0 ? lastSpace : maxLength)] + "..."; + } +} diff --git a/Chrono/Web/Pages/Decks.razor.css b/Chrono/Web/Pages/Decks.razor.css new file mode 100644 index 0000000..8d66a3d --- /dev/null +++ b/Chrono/Web/Pages/Decks.razor.css @@ -0,0 +1,93 @@ +.decks-page { + max-width: 900px; + margin: 0 auto; + padding: 0 1rem 2rem; +} + +.decks-header { + margin-bottom: 2rem; +} + +.decks-header h1 { + margin: 0 0 0.25rem; +} + +.deck-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.deck-card { + display: block; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem 1.5rem; + text-decoration: none; + color: inherit; + transition: border-color var(--transition), box-shadow var(--transition); +} + +.deck-card:hover { + border-color: var(--accent); + box-shadow: 0 0 20px var(--accent-glow); +} + +.deck-card-title { + font-size: 1.2rem; + font-weight: 600; + margin: 0 0 0.5rem; + color: var(--text-primary); +} + +.deck-card-factions { + display: flex; + gap: 0.4rem; + margin-bottom: 0.5rem; + flex-wrap: wrap; +} + +.faction-badge { + display: inline-block; + font-size: 0.75rem; + font-weight: 600; + padding: 0.15rem 0.6rem; + border-radius: 999px; + background: var(--bg-elevated); + color: var(--accent); + border: 1px solid var(--border); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.deck-card-meta { + display: flex; + gap: 0.75rem; + font-size: 0.8rem; + color: var(--text-muted); +} + +.deck-card-meta span + span::before { + content: "·"; + margin-right: 0.75rem; +} + +.deck-card-excerpt { + margin-top: 0.6rem; + font-size: 0.85rem; + color: var(--text-secondary); + line-height: 1.5; +} + +.empty-state { + text-align: center; + padding: 4rem 1rem; + color: var(--text-muted); +} + +.empty-state i { + font-size: 3rem; + display: block; + margin-bottom: 1rem; +} diff --git a/chrono.docs/.obsidian/types.json b/chrono.docs/.obsidian/types.json index 986dccf..ff04ea9 100644 --- a/chrono.docs/.obsidian/types.json +++ b/chrono.docs/.obsidian/types.json @@ -11,6 +11,7 @@ "divers": "multitext", "archetypes": "multitext", "keycards": "multitext", - "isVisible": "checkbox" + "isVisible": "checkbox", + "factions": "multitext" } } \ No newline at end of file diff --git a/chrono.docs/.obsidian/workspace.json b/chrono.docs/.obsidian/workspace.json index 7139a43..fee89da 100644 --- a/chrono.docs/.obsidian/workspace.json +++ b/chrono.docs/.obsidian/workspace.json @@ -3,27 +3,6 @@ "id": "e897f013d92d2cb5", "type": "split", "children": [ - { - "id": "6cbb242d281fac81", - "type": "tabs", - "children": [ - { - "id": "16145cbe588c14bd", - "type": "leaf", - "pinned": true, - "state": { - "type": "bases", - "state": { - "file": "_Timeline.base", - "viewName": "Table" - }, - "pinned": true, - "icon": "lucide-table", - "title": "_Timeline" - } - } - ] - }, { "id": "02cc741a0e7b6b27", "type": "tabs", @@ -206,9 +185,10 @@ }, "active": "c90153d5f925b0d5", "lastOpenFiles": [ + "Swashbuckling Diehard.md", + "Decks/Big Energy.md", "_Decks.base", "Decks/Rewind Me.canvas", - "Decks/Big Energy.md", "Decks/Rewind Me.md", "Overpower.md", "_Keyword.base", diff --git a/chrono.docs/Decks/Big Energy.md b/chrono.docs/Decks/Big Energy.md index d1642b2..be05b1b 100644 --- a/chrono.docs/Decks/Big Energy.md +++ b/chrono.docs/Decks/Big Energy.md @@ -26,20 +26,36 @@ keycards: - "[[Hidden Locus]]" - "[[Debris Collector]]" - "[[Lumbering Starseeker]]" -description: |- - The idea of this deck is just go heavy on stat efficient cards. Like 2 for a 2/3 [[Kinetic Absorber]] or 5 for a 5/6 [[Lumbering Starseeker]]. [[Debris Collector]] is also quite insane, and will Immortalize into a [[Living Comet]] 10/10 with [[Overpower]] pretty reliably, but it's also your only stat [[Mute]] target, so might be a bit trash if that's popular in the meta. + - "[[Lightsteel Colossus]]" +description: "The idea of this deck is to go heavy on stat-efficient cards. Like 2 for a 2/3 [[Kinetic Absorber]], 5/6 [[Lumbering Starseeker]], and 8 for a 9/14 [[Lightsteel Colossus]]. [[Debris Collector]] is also quite powerful and will Immortalize into a [[Living Comet]] 10/10 with [[Overpower]] pretty reliably, but it's also your only stat [[Mute]] target, so it might be a bit trash if that's popular in the meta.\r - You got [[Devourer Spawn]] to help push for lethal. It's going to have a bunch of keywords on it given how popular timelines are in the meta. + \r - You need to Overflow your mana once in awhile or some of your [[Supernova]] removal will be useless. Be pass heavy. + You got [[Devourer Spawn]] to help push for lethal. It's going to have a bunch of keywords on it, given how popular timelines are in the meta.\r - Your going to win on very low health if you win. Such is life. + \r - No card draw, so [[Army of the Sun]] is your hope if things go long. At the worst, it's a spell that summons a big unit right away if it goes off. + You need to Overflow your mana once in a while, or some of your [[Supernova]] removal will be useless. Be pass heavy.\r - [[Starfueled Medics]] and [[Swashbuckling Diehard]] are just there to take up space and be mid range drops. And technically they can be win conditions I suppose. + \r - Attack aggressively with [[Brilliant Martyr]]. You really want [[Star Siphon]] to ramp. + You're going to win on very low health if you win. Such is life.\r - As divers, [[Peaceful Synthesizer]] is a 1 drop that will quickly turn into a 3/3. Can't get more stat efficient then that. And [[Limit Breaker]] is an early game growing threat that will demand an answer from the enemy. + \r + + No card draw, so [[Army of the Sun]] is your hope if things go long. At the worst, it's a spell that summons a big unit right away if it goes off.\r + + \r + + [[Starfueled Medics]] and [[Swashbuckling Diehard]] are just there to take up space and be mid-range drops. And technically, they can be win conditions, I suppose.\r + + \r + + Attack aggressively with [[Brilliant Martyr]]. You really want [[Star Siphon]] to ramp.\r + + \r + + As divers, [[Peaceful Synthesizer]] is a 1 drop that will quickly turn into a 3/3. Can't get more stat efficient than that. And [[Limit Breaker]] is an early-game growing threat that will demand an answer from the enemy." +factions: + - "[[Sungrace]]" ---