mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
profile modal: always-visible "Acting as" identity card (#43)
Identity pill top-center on every pane, straddling the header divider, so users always see which profile they're acting as (e.g. before uploading). Populated in renderSavedProfiles (same resolveMeta path as the Account pane card, so it refreshes on profile switch), hidden when logged out, and click-through jumps to the Account pane. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
27be0301c5
commit
d1a578f3a2
4 files changed
+186
No files matched your search
@@ -62,6 +62,35 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Active-identity card (#43): visible on EVERY pane, straddling the
|
||||
header divider, so users always see which profile they're acting as
|
||||
(e.g. before uploading). Clicking it jumps to the Account pane.
|
||||
-->
|
||||
<button
|
||||
id="profileModalIdentityCard"
|
||||
type="button"
|
||||
class="profile-modal__identity focus-ring hidden"
|
||||
aria-hidden="true"
|
||||
aria-label="Active profile — open account settings"
|
||||
>
|
||||
<span class="profile-modal__identity-avatar" aria-hidden="true">
|
||||
<img
|
||||
id="profileModalIdentityAvatar"
|
||||
src="/assets/svg/default-profile.svg"
|
||||
alt=""
|
||||
/>
|
||||
</span>
|
||||
<span class="profile-modal__identity-copy">
|
||||
<span class="profile-modal__identity-kicker">Acting as</span>
|
||||
<span
|
||||
id="profileModalIdentityName"
|
||||
class="profile-modal__identity-name"
|
||||
>No active profile</span
|
||||
>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="profile-modal__body">
|
||||
<div
|
||||
|
||||
@@ -2904,6 +2904,79 @@
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
/* #43: always-visible active-identity pill. Rendered after the header in
|
||||
flow and pulled up with a negative block-start margin so it sits half-on /
|
||||
half-off the header divider — every pane shows who you're acting as. */
|
||||
.profile-modal__identity {
|
||||
align-self: center;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
max-inline-size: min(80%, calc(var(--profile-modal-active-min-width) * 2));
|
||||
padding: var(--space-3xs);
|
||||
padding-inline-end: var(--space-md);
|
||||
border-radius: var(--radius-full);
|
||||
border: var(--border-width-hairline) solid var(--color-border-translucent);
|
||||
background-color: var(--color-panel);
|
||||
color: var(--color-text);
|
||||
box-shadow: var(--shadow-md);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-block-start: calc(-1 * var(--space-sm));
|
||||
margin-block-end: calc(-1 * var(--space-3xs));
|
||||
cursor: pointer;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.profile-modal__identity.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile-modal__identity:hover {
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.profile-modal__identity-avatar {
|
||||
display: flex;
|
||||
inline-size: var(--space-xl);
|
||||
block-size: var(--space-xl);
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-full);
|
||||
border: var(--border-width-hairline) solid var(--color-accent);
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
.profile-modal__identity-avatar img {
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.profile-modal__identity-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.profile-modal__identity-kicker {
|
||||
font-size: var(--font-size-3xs);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-extra-wide);
|
||||
color: var(--color-accent);
|
||||
line-height: var(--line-height-tight);
|
||||
}
|
||||
|
||||
.profile-modal__identity-name {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
line-height: var(--line-height-tight);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-modal__active-profile {
|
||||
@apply focus-ring flex flex-wrap items-center gap-4 p-4;
|
||||
}
|
||||
|
||||
@@ -669,6 +669,13 @@ export class ProfileModalController {
|
||||
this.profileAvatar = document.getElementById("profileModalAvatar") || null;
|
||||
this.profileName = document.getElementById("profileModalName") || null;
|
||||
this.profileNpub = document.getElementById("profileModalNpub") || null;
|
||||
// #43: always-visible identity card straddling the header divider.
|
||||
this.identityCard =
|
||||
document.getElementById("profileModalIdentityCard") || null;
|
||||
this.identityCardAvatar =
|
||||
document.getElementById("profileModalIdentityAvatar") || null;
|
||||
this.identityCardName =
|
||||
document.getElementById("profileModalIdentityName") || null;
|
||||
this.switcherList = document.getElementById("profileSwitcherList") || null;
|
||||
|
||||
this.profileModalAvatar = this.profileAvatar;
|
||||
@@ -1745,6 +1752,12 @@ export class ProfileModalController {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.identityCard instanceof HTMLElement) {
|
||||
this.identityCard.addEventListener("click", () => {
|
||||
this.selectPane("account");
|
||||
});
|
||||
}
|
||||
|
||||
if (this.permissionPromptCtaButton instanceof HTMLElement) {
|
||||
this.permissionPromptCtaButton.addEventListener("click", () => {
|
||||
if (this.permissionPromptCtaButton?.dataset?.action === "retry-auth-sync") {
|
||||
@@ -2564,6 +2577,24 @@ export class ProfileModalController {
|
||||
: this.globalProfileAvatar.alt || "Profile avatar";
|
||||
}
|
||||
|
||||
// #43: keep the always-visible identity card in sync (hidden when logged
|
||||
// out so the modal doesn't claim an identity that isn't active).
|
||||
if (this.identityCard instanceof HTMLElement) {
|
||||
this.identityCard.classList.toggle("hidden", !hasActiveProfile);
|
||||
this.identityCard.setAttribute(
|
||||
"aria-hidden",
|
||||
hasActiveProfile ? "false" : "true",
|
||||
);
|
||||
}
|
||||
if (this.identityCardName instanceof HTMLElement) {
|
||||
this.identityCardName.textContent = activeDisplayName;
|
||||
}
|
||||
if (this.identityCardAvatar instanceof HTMLImageElement) {
|
||||
if (this.identityCardAvatar.src !== activeAvatarSrc) {
|
||||
this.identityCardAvatar.src = activeAvatarSrc;
|
||||
}
|
||||
}
|
||||
|
||||
const listEl = this.switcherList;
|
||||
if (listEl instanceof HTMLElement) {
|
||||
listEl.textContent = "";
|
||||
|
||||
@@ -726,6 +726,59 @@ for (const _ of [0]) {
|
||||
assert.equal(switcherNpubEl.textContent, expectedSwitcher);
|
||||
});
|
||||
|
||||
// #43: the always-visible identity card mirrors the active profile on every
|
||||
// pane (populated by renderSavedProfiles, so it also refreshes on account
|
||||
// switch), hides when logged out, and click-through opens the Account pane.
|
||||
test('identity card shows the acting profile, hides logged out, opens Account', async () => {
|
||||
const sampleProfiles = [
|
||||
{
|
||||
pubkey: defaultActorHex,
|
||||
npub: 'npub1abcdefghijkmnopqrstuvwxyz1234567890example',
|
||||
name: 'Primary Account',
|
||||
picture: '',
|
||||
providerId: 'nip07',
|
||||
authType: 'nip07',
|
||||
},
|
||||
];
|
||||
|
||||
const controller = createController({
|
||||
services: {
|
||||
formatShortNpub,
|
||||
},
|
||||
});
|
||||
|
||||
await controller.load();
|
||||
|
||||
controller.state.setSavedProfiles(sampleProfiles);
|
||||
controller.state.setActivePubkey(defaultActorHex);
|
||||
controller.renderSavedProfiles();
|
||||
|
||||
assert.ok(controller.identityCard, 'identity card element is cached');
|
||||
assert.equal(
|
||||
controller.identityCard.classList.contains('hidden'),
|
||||
false,
|
||||
'visible while a profile is active',
|
||||
);
|
||||
assert.equal(controller.identityCard.getAttribute('aria-hidden'), 'false');
|
||||
assert.equal(controller.identityCardName.textContent, 'Primary Account');
|
||||
|
||||
controller.identityCard.click();
|
||||
assert.equal(
|
||||
controller.getActivePane(),
|
||||
'account',
|
||||
'click-through jumps to the Account pane',
|
||||
);
|
||||
|
||||
controller.state.setActivePubkey(null);
|
||||
controller.renderSavedProfiles();
|
||||
assert.equal(
|
||||
controller.identityCard.classList.contains('hidden'),
|
||||
true,
|
||||
'hidden when logged out (no claimed identity)',
|
||||
);
|
||||
assert.equal(controller.identityCard.getAttribute('aria-hidden'), 'true');
|
||||
});
|
||||
|
||||
test('renderSavedProfiles applies provider metadata', async () => {
|
||||
const sampleProfiles = [
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user