From a1eae36cdc7af850968dd78d90263409eccf8057 Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Tue, 30 Jun 2026 19:41:00 -0500 Subject: [PATCH] Declutter status UI: dot-only state, click-to-open characters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Session header: remove the redundant "Connected" state pill from the top-right. Each tab already shows a coloured connection-state dot next to the character name (SessionTabs), so the pill duplicated it. Dropped the now-unused StateClass/StateLabel helpers. Worlds page character rows: the badge + name + a single status dot are now the primary click target — clicking opens the live session if there is one, else connects (PrimaryActionAsync). Removed the separate "Open" button and the "Connected" text pill (now a coloured .sc-state-dot), and folded the "Connect" button into the label click. Edit/delete icons remain. Updated WorldManagerTests to drive connect via the label (.sc-char-main) instead of .sc-connect-btn. Also removed dead --sc-cols-width JS that a merge re-introduced into measureGrid (nothing consumes it since the min-width column track was dropped). Web + Android build clean; UI bUnit suite 46/46. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp --- .../Components/SessionScreen.razor | 27 +------------ .../Components/WorldManager.razor | 40 ++++++++++--------- src/SharpClient.UI/wwwroot/app.css | 5 ++- src/SharpClient.UI/wwwroot/sc-interop.js | 9 ----- .../SharpClient.UI.Tests/WorldManagerTests.cs | 7 ++-- 5 files changed, 32 insertions(+), 56 deletions(-) diff --git a/src/SharpClient.UI/Components/SessionScreen.razor b/src/SharpClient.UI/Components/SessionScreen.razor index a52f198..10503bc 100644 --- a/src/SharpClient.UI/Components/SessionScreen.razor +++ b/src/SharpClient.UI/Components/SessionScreen.razor @@ -12,13 +12,8 @@
- @if (Vm.Active is not null) - { - - - @StateLabel(Vm.Active.State) - - } + @* Connection state is already shown as a coloured dot on each tab (SessionTabs), so the + redundant "Connected" state pill has been removed from the header. *@ @if (ProtocolVm is not null) { - - @activeSession.State - - } - else - { - - } +
+
@Initial(character.Name)
+
@character.Name
+ +
@@ -225,6 +216,19 @@ private async Task ConnectAsync(World world, Character character) => await Vm.ConnectAsync(world, character); + // Clicking a character's label switches to its live session if one exists, otherwise connects it. + private async Task PrimaryActionAsync(World world, Character character) + { + if (Vm.ActiveSessionFor(character) is not null) + { + OpenSession(character); + } + else + { + await ConnectAsync(world, character); + } + } + private async Task OpenRulesAsync(Guid worldId) => await OnOpenRules.InvokeAsync(worldId); private void DismissError() => Vm.ClearError(); diff --git a/src/SharpClient.UI/wwwroot/app.css b/src/SharpClient.UI/wwwroot/app.css index 938c6a8..04a0223 100644 --- a/src/SharpClient.UI/wwwroot/app.css +++ b/src/SharpClient.UI/wwwroot/app.css @@ -547,8 +547,11 @@ body { .sc-chev-open { transform: rotate(180deg); } .sc-char-row { display: flex; align-items: center; gap: 11px; padding: 10px 14px; border-top: 1px solid var(--bd); } +/* The badge + name + status dot form the primary click target (open the session, or connect). */ +.sc-char-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: 11px; cursor: pointer; } +.sc-char-main:hover .sc-char-name { color: var(--acc2); } .sc-char-badge { flex: none; width: 26px; height: 26px; border-radius: 7px; background: var(--outbg); border: 1px solid var(--bd2); display: flex; align-items: center; justify-content: center; font-family: var(--mono); font-size: 11px; color: var(--dim); } -.sc-char-name { flex: 1; min-width: 0; font-size: 14px; color: #e7ecf2; } +.sc-char-name { flex: 1; min-width: 0; font-size: 14px; color: #e7ecf2; transition: color .12s ease; } .sc-connect-btn { flex: none; font-size: 12.5px; font-weight: 600; color: #0a0c10; background: var(--acc2); border: none; border-radius: 8px; padding: 8px 14px; cursor: pointer; } .sc-icon-btn { flex: none; width: 30px; height: 30px; border-radius: 8px; background: transparent; border: 1px solid var(--bd); color: var(--faint); display: flex; align-items: center; justify-content: center; cursor: pointer; } .sc-icon-btn-danger:hover { border-color: rgba(224, 108, 117, .5); color: #e06c75; } diff --git a/src/SharpClient.UI/wwwroot/sc-interop.js b/src/SharpClient.UI/wwwroot/sc-interop.js index 58be02d..83e381e 100644 --- a/src/SharpClient.UI/wwwroot/sc-interop.js +++ b/src/SharpClient.UI/wwwroot/sc-interop.js @@ -164,17 +164,8 @@ export function measureGrid(element, targetCols, minFont, maxFont) { cols = clampGrid(Math.floor(contentW / (advanceRatio * fontPx))); } - // Publish the column-track width in PIXELS, measured on the same basis the font was fitted to - // (a real run of `cols` glyphs). The CSS `ch` unit can't be used for the track: its single-glyph - // advance rounds a couple px wider than this averaged run, which pushed `cols * 1ch` past the - // content box and produced a spurious horizontal scrollbar even though the columns fit. Measured - // px keeps the track <= the box in the fitting case (no scroll, lines wrap at the column edge) and - // only exceeds it when the screen genuinely can't fit `cols` at the min font (then it scrolls). - const trackW = runWidth(fontPx, '0'.repeat(cols)); - element.style.setProperty('--out-fs', fontPx + 'px'); element.style.setProperty('--sc-cols', String(cols)); - element.style.setProperty('--sc-cols-width', trackW + 'px'); return { cols, rows: clampGrid(Math.floor(contentH / (lineRatio * fontPx))) }; } diff --git a/tests/SharpClient.UI.Tests/WorldManagerTests.cs b/tests/SharpClient.UI.Tests/WorldManagerTests.cs index 2589098..d2eb8d8 100644 --- a/tests/SharpClient.UI.Tests/WorldManagerTests.cs +++ b/tests/SharpClient.UI.Tests/WorldManagerTests.cs @@ -32,7 +32,7 @@ public async Task RendersWorldName() } [Test] - public async Task ExpandingWorldShowsCharacterAndConnectButton() + public async Task ExpandingWorldShowsClickableCharacterLabel() { var vm = await BuildSeededVmAsync(); using var ctx = new BunitContext(); @@ -41,7 +41,8 @@ public async Task ExpandingWorldShowsCharacterAndConnectButton() cut.Find(".sc-world-row").Click(); await Assert.That(cut.Markup).Contains("Vesper"); - await Assert.That(cut.FindAll(".sc-connect-btn")).IsNotEmpty(); + // Connecting is now driven by clicking the character's label area, not a separate button. + await Assert.That(cut.FindAll(".sc-char-main")).IsNotEmpty(); } [Test] @@ -60,7 +61,7 @@ public async Task FailedConnectRendersDismissibleErrorBanner() var cut = ctx.Render(p => p.Add(c => c.Vm, vm)); cut.Find(".sc-world-row").Click(); - cut.Find(".sc-connect-btn").Click(); + cut.Find(".sc-char-main").Click(); // The failure surfaces as a banner instead of throwing. await Assert.That(cut.FindAll(".sc-wm-error")).IsNotEmpty();