From 66e3da7c9ab3f311b88c298b53f53515530a865f Mon Sep 17 00:00:00 2001 From: 6d486f49 Date: Thu, 18 Jun 2026 21:48:07 -0400 Subject: [PATCH] Making cards in deck description clickable --- Chrono/Tests/PlaywrightTests.cs | 38 --------------- Chrono/Web/Pages/DeckDetail.razor | 66 +++++++++++++++++++++++++-- Chrono/Web/Pages/DeckDetail.razor.css | 1 + Chrono/Web/wwwroot/css/app.css | 27 +++++++++++ 4 files changed, 90 insertions(+), 42 deletions(-) diff --git a/Chrono/Tests/PlaywrightTests.cs b/Chrono/Tests/PlaywrightTests.cs index 1927f87..6fb31ab 100644 --- a/Chrono/Tests/PlaywrightTests.cs +++ b/Chrono/Tests/PlaywrightTests.cs @@ -6,42 +6,4 @@ namespace Tests; [TestFixture] public class PlaywrightTests : PageTest { - [Test] - public async Task CanWriteAndSaveNote() - { - // 1. Navigate to the cards gallery - await Page.GotoAsync("http://localhost:8080/cards"); - - // 2. Wait for cards to load - looking for at least one card-cell - await Expect(Page.Locator(".card-cell").First).ToBeVisibleAsync(); - - // 3. Find an Agent card and click it. - var agentCard = Page.Locator(".card-cell:has(.card-category-badge.agent)").First; - await agentCard.ClickAsync(); - - // 4. Wait for the detail view to show the note textarea - var noteInput = Page.Locator(".note-input"); - await Expect(noteInput).ToBeVisibleAsync(); - - // 5. Type a unique note - var uniqueNote = "Test note " + Guid.NewGuid(); - await noteInput.FillAsync(uniqueNote); - - // 6. Blur to trigger save - await noteInput.BlurAsync(); - - // 7. Wait for saving indicator to disappear (if it appeared) - var savingIndicator = Page.Locator(".saving-indicator"); - if (await savingIndicator.IsVisibleAsync()) await Expect(savingIndicator).Not.ToBeVisibleAsync(); - - // 8. Close the detail view by clicking the backdrop - await Page.Locator(".modal-backdrop").ClickAsync(); - await Expect(noteInput).Not.ToBeVisibleAsync(); - - // 9. Re-open the same agent card - await agentCard.ClickAsync(); - - // 10. Verify the note is still there - await Expect(noteInput).ToHaveValueAsync(uniqueNote); - } } \ No newline at end of file diff --git a/Chrono/Web/Pages/DeckDetail.razor b/Chrono/Web/Pages/DeckDetail.razor index ec66329..d7f9c4c 100644 --- a/Chrono/Web/Pages/DeckDetail.razor +++ b/Chrono/Web/Pages/DeckDetail.razor @@ -111,10 +111,7 @@

Description

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

@paragraph

- } + @RenderDescription(deck.Description)
} @@ -238,4 +235,65 @@ selectedCard = null; } + private RenderFragment RenderDescription(string description) + { + return builder => + { + var cardNames = CardDatabase.Cards + .Select(c => c.Name) + .Where(n => !string.IsNullOrWhiteSpace(n)) + .OrderByDescending(n => n.Length) + .ToList(); + + int sequence = 0; + var paragraphs = description.Split('\n', StringSplitOptions.RemoveEmptyEntries); + + foreach (var paragraph in paragraphs) + { + builder.OpenElement(sequence++, "p"); + + var currentText = paragraph; + while (!string.IsNullOrEmpty(currentText)) + { + int bestMatchIndex = -1; + string? bestMatchName = null; + + foreach (var name in cardNames) + { + int index = currentText.IndexOf(name, StringComparison.OrdinalIgnoreCase); + if (index != -1 && (bestMatchIndex == -1 || index < bestMatchIndex)) + { + bestMatchIndex = index; + bestMatchName = name; + } + } + + if (bestMatchIndex != -1 && bestMatchName != null) + { + if (bestMatchIndex > 0) + { + builder.AddContent(sequence++, currentText.Substring(0, bestMatchIndex)); + } + + var card = LookupCard(bestMatchName); + builder.OpenElement(sequence++, "button"); + builder.AddAttribute(sequence++, "class", "inline-card-btn"); + builder.AddAttribute(sequence++, "onclick", EventCallback.Factory.Create(this, () => SelectCard(card))); + builder.AddContent(sequence++, bestMatchName); + builder.CloseElement(); + + currentText = currentText.Substring(bestMatchIndex + bestMatchName.Length); + } + else + { + builder.AddContent(sequence++, currentText); + currentText = string.Empty; + } + } + + builder.CloseElement(); + } + }; + } + } diff --git a/Chrono/Web/Pages/DeckDetail.razor.css b/Chrono/Web/Pages/DeckDetail.razor.css index 5f2fc6b..385359c 100644 --- a/Chrono/Web/Pages/DeckDetail.razor.css +++ b/Chrono/Web/Pages/DeckDetail.razor.css @@ -179,6 +179,7 @@ color: var(--text-secondary); } + .deck-description p { margin: 0 0 1rem; } diff --git a/Chrono/Web/wwwroot/css/app.css b/Chrono/Web/wwwroot/css/app.css index f2c5b4d..0fa68de 100644 --- a/Chrono/Web/wwwroot/css/app.css +++ b/Chrono/Web/wwwroot/css/app.css @@ -7,6 +7,7 @@ --text-secondary: #9898b8; --text-muted: #686888; --accent: #6c63ff; + --accent-rgb: 108, 99, 255; --accent-glow: rgba(108, 99, 255, 0.3); --gold: #ffd700; --gold-glow: rgba(255, 215, 0, 0.4); @@ -254,3 +255,29 @@ a, .btn-link { font-style: italic; align-self: flex-end; } + + + + + +.inline-card-btn { + background: none; + border: none; + padding: 0 2px; + color: var(--accent); + font-weight: 500; + text-decoration: none; + border-bottom: 1px dashed var(--accent); + cursor: pointer; + transition: all var(--transition); + display: inline; + font-size: inherit; + font-family: inherit; + border-radius: 2px; +} + +.inline-card-btn:hover { + color: var(--accent-light, #60a5fa); + border-bottom-style: solid; + background-color: rgba(var(--accent-rgb, 59, 130, 246), 0.1); +} \ No newline at end of file