feat(profiles): switch profile from the account menu
Switching was reachable only from Settings, which is the wrong place for it: on a shared device changing who is watching is a frequent, front-door action, not a configuration change buried three screens deep. It sits directly under the identity block rather than among the destinations (Downloads/Settings/Display), because it answers "who is this?" and not "where do I go?". Shown even with one account, since the picker is also where a second is added -- gating it on a second profile existing would leave no way in from here. The picker also gains a Back affordance when a session is already live. Reaching it from the menu and changing your mind -- or failing a PIN on someone else's tile -- previously had no way back to the session you still had. At startup there is nothing behind it, so it stays hidden.
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
Settings, Display) and Sign out. Available on every authenticated,
|
||||
non-immersive screen via the shared AppHeader.
|
||||
|
||||
TRACES: UR-054 | DR-075
|
||||
TRACES: UR-054, UR-082 | DR-075, DR-276
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { tick } from "svelte";
|
||||
@@ -105,6 +105,32 @@
|
||||
|
||||
<div class="border-t border-gray-700 my-1"></div>
|
||||
|
||||
<!--
|
||||
Grouped with the identity block above rather than with the destinations
|
||||
below: this changes *who* the app is, not where it goes. Shown even on a
|
||||
device with one account, because the picker is also where a second one is
|
||||
added — hiding it until a second profile exists would leave no way in
|
||||
from here. (DR-276)
|
||||
-->
|
||||
<a
|
||||
href="/profiles"
|
||||
role="menuitem"
|
||||
class="flex items-center gap-3 px-4 py-3 text-sm text-gray-300 hover:bg-gray-700 transition-colors"
|
||||
onclick={() => close(false)}
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
Switch profile
|
||||
</a>
|
||||
|
||||
<div class="border-t border-gray-700 my-1"></div>
|
||||
|
||||
<a
|
||||
href="/downloads"
|
||||
role="menuitem"
|
||||
|
||||
@@ -73,7 +73,22 @@ describe("AccountMenu", () => {
|
||||
render(AccountMenu);
|
||||
openMenu();
|
||||
const items = screen.getAllByRole("menuitem").map((el) => el.textContent?.trim());
|
||||
expect(items).toEqual(["Downloads", "Settings", "Display", "Sign out"]);
|
||||
expect(items).toEqual(["Switch profile", "Downloads", "Settings", "Display", "Sign out"]);
|
||||
});
|
||||
|
||||
/**
|
||||
* "Switch profile" sits directly under the identity block, before the
|
||||
* destinations. It answers "who is this?", not "where do I go?", and burying
|
||||
* it in Settings is what this entry exists to undo.
|
||||
*
|
||||
* TRACES: UR-082 | DR-276
|
||||
*/
|
||||
it("puts Switch profile first, next to the identity block", async () => {
|
||||
render(AccountMenu);
|
||||
openMenu();
|
||||
const items = screen.getAllByRole("menuitem");
|
||||
expect(items[0].textContent?.trim()).toBe("Switch profile");
|
||||
expect(items[0].getAttribute("href")).toBe("/profiles");
|
||||
});
|
||||
|
||||
it("shows the identity block with name and server host", async () => {
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { profiles } from "$lib/stores/profiles";
|
||||
import { isAuthenticated } from "$lib/stores/auth";
|
||||
import { navigateBack } from "$lib/utils/navigation";
|
||||
import type { Profile, UnlockOutcome } from "$lib/api/bindings";
|
||||
import PinPad from "$lib/components/PinPad.svelte";
|
||||
import { orderProfiles, initialsFor, tileColour, lockoutMessage } from "$lib/utils/profileTiles";
|
||||
@@ -152,6 +154,30 @@
|
||||
|
||||
<div class="min-h-full flex items-center justify-center p-6">
|
||||
<div class="w-full max-w-3xl">
|
||||
<!--
|
||||
Reached from the account menu, the picker must not be a one-way door: a
|
||||
signed-in viewer who opens it and changes their mind (or fails a PIN on
|
||||
someone else's tile) needs a way back to the session they still have. At
|
||||
startup there is no session behind it, so there is nothing to go back to
|
||||
and this stays hidden. (DR-276)
|
||||
-->
|
||||
{#if $isAuthenticated && mode !== "pin" && mode !== "password"}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => navigateBack("/")}
|
||||
class="mb-6 text-sm text-gray-400 hover:text-white flex items-center gap-1"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
Back
|
||||
</button>
|
||||
{/if}
|
||||
{#if mode === "picker" || mode === "manage"}
|
||||
<h1 class="text-3xl font-semibold text-center mb-2">
|
||||
{mode === "manage" ? "Manage profiles" : "Who's watching?"}
|
||||
|
||||
Reference in New Issue
Block a user