Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 2 additions & 25 deletions src/SharpClient.UI/Components/SessionScreen.razor
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,8 @@
<div class="sc-session-header">
<SessionTabs Vm="Vm" />
<div class="sc-session-actions">
@if (Vm.Active is not null)
{
<span class="sc-state-pill sc-state-@StateClass(Vm.Active.State)">
<span class="sc-state-pill-dot"></span>
@StateLabel(Vm.Active.State)
</span>
}
@* 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)
{
<button class="sc-protocol-toggle @(_showProtocol ? "sc-protocol-toggle-active" : string.Empty)"
Expand Down Expand Up @@ -129,24 +124,6 @@

private void ToggleProtocol() => _showProtocol = !_showProtocol;

private static string StateClass(ConnectionState state) => state switch
{
ConnectionState.Connected => "connected",
ConnectionState.Connecting => "connecting",
ConnectionState.Reconnecting => "reconnecting",
ConnectionState.Error => "error",
_ => "disconnected",
};

private static string StateLabel(ConnectionState state) => state switch
{
ConnectionState.Connected => "Connected",
ConnectionState.Connecting => "Connecting",
ConnectionState.Reconnecting => "Reconnecting",
ConnectionState.Error => "Error",
_ => "Disconnected",
};

public async ValueTask DisposeAsync()
{
Vm.Changed -= OnVmChanged;
Expand Down
40 changes: 22 additions & 18 deletions src/SharpClient.UI/Components/WorldManager.razor
Original file line number Diff line number Diff line change
Expand Up @@ -68,25 +68,16 @@
@foreach (var character in world.Characters)
{
var activeSession = Vm.ActiveSessionFor(character);
var charState = activeSession?.State ?? ConnectionState.Disconnected;
// The label area is the primary click target: open the running session if
// there is one, otherwise connect. A single coloured dot shows the state.
<div class="sc-char-row">
<div class="sc-char-badge">@Initial(character.Name)</div>
<div class="sc-char-name">@character.Name</div>
@if (activeSession is not null)
{
<button type="button" class="sc-connect-btn sc-open-btn"
@onclick="() => OpenSession(character)"
title="Switch to active session">
Open
</button>
<span class="sc-state-pill" style="background:@StateColor(activeSession.State)"
title="@activeSession.State">
@activeSession.State
</span>
}
else
{
<button type="button" class="sc-connect-btn" @onclick="() => ConnectAsync(world, character)">Connect</button>
}
<div class="sc-char-main" @onclick="() => PrimaryActionAsync(world, character)"
title="@(activeSession is not null ? "Open session" : "Connect")">
<div class="sc-char-badge">@Initial(character.Name)</div>
<div class="sc-char-name">@character.Name</div>
<span class="sc-state-dot" style="background:@StateColor(charState)" title="@charState"></span>
</div>
Comment on lines +75 to +80
<button type="button" class="sc-icon-btn" title="Edit character" @onclick="() => OpenEditCharacter(world, character)">✎</button>
<button type="button" class="sc-icon-btn sc-icon-btn-danger" title="Delete character" @onclick="() => DeleteCharacterAsync(world, character)">🗑</button>
</div>
Expand Down Expand Up @@ -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();
Expand Down
5 changes: 4 additions & 1 deletion src/SharpClient.UI/wwwroot/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
9 changes: 0 additions & 9 deletions src/SharpClient.UI/wwwroot/sc-interop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))) };
}
Expand Down
7 changes: 4 additions & 3 deletions tests/SharpClient.UI.Tests/WorldManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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]
Expand All @@ -60,7 +61,7 @@ public async Task FailedConnectRendersDismissibleErrorBanner()
var cut = ctx.Render<WorldManager>(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();
Expand Down
Loading