Release v0.0.7 - Identity list UI redesign with avatars
- Redesign identities list to show avatar and display name from Nostr profile - Replace star icon selection with clickable row for identity switching - Add gear icon for direct access to identity settings - Highlight selected identity with primary color border - Remove credits box from information tab - Add rebuild instruction to CLAUDE.md - Clean up unused imports in info components - Replace HTML autofocus with programmatic focus for accessibility Files modified: - CLAUDE.md - package.json - projects/chrome/src/app/components/home/identities/* - projects/chrome/src/app/components/home/info/* - projects/chrome/src/app/components/vault-login/* - projects/firefox/src/app/components/home/identities/* - projects/firefox/src/app/components/home/info/* - projects/firefox/src/app/components/vault-login/* 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,11 @@ npm test # Run unit tests with Karma
|
||||
npm run lint # Run ESLint
|
||||
```
|
||||
|
||||
**Important:** After making any code changes, rebuild both extensions before testing:
|
||||
```bash
|
||||
npm run build:chrome && npm run build:firefox
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Monorepo Structure
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "plebeian-signer",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"custom": {
|
||||
"chrome": {
|
||||
"version": "0.0.6"
|
||||
"version": "0.0.7"
|
||||
},
|
||||
"firefox": {
|
||||
"version": "0.0.6"
|
||||
"version": "0.0.7"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "Plebeian Signer - Nostr Identity Manager & Signer",
|
||||
"description": "Manage and switch between multiple identities while interacting with Nostr apps",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"homepage_url": "https://git.mleku.dev/mleku/plebeian-signer",
|
||||
"options_page": "options.html",
|
||||
"permissions": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->
|
||||
<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->
|
||||
<div class="custom-header" style="position: sticky; top: 0">
|
||||
<span class="text">Identities </span>
|
||||
<span class="text">Identities</span>
|
||||
|
||||
<button class="button btn btn-primary btn-sm" (click)="onClickNewIdentity()">
|
||||
<div class="sam-flex-row gap-h">
|
||||
@@ -12,67 +12,37 @@
|
||||
</div>
|
||||
|
||||
@let sessionData = storage.getBrowserSessionHandler().browserSessionData;
|
||||
<!-- - -->
|
||||
@let identities = sessionData?.identities ?? []; @if(identities.length === 0) {
|
||||
<div
|
||||
style="
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
@let identities = sessionData?.identities ?? [];
|
||||
|
||||
@if(identities.length === 0) {
|
||||
<div class="empty-state">
|
||||
<span class="sam-text-muted">
|
||||
Create your first identity by clicking on the button in the upper right
|
||||
corner.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
} @for(identity of identities; track identity) {
|
||||
<div
|
||||
class="identity"
|
||||
style="overflow: hidden"
|
||||
(click)="onClickEditIdentity(identity)"
|
||||
>
|
||||
@for(identity of identities; track identity.id) {
|
||||
@let isSelected = identity.id === sessionData?.selectedIdentityId;
|
||||
|
||||
<span
|
||||
class="no-select"
|
||||
style="overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap"
|
||||
[class.not-active]="!isSelected"
|
||||
<div
|
||||
class="identity"
|
||||
[class.selected]="isSelected"
|
||||
(click)="onClickSelectIdentity(identity.id)"
|
||||
>
|
||||
{{ identity.nick }}
|
||||
</span>
|
||||
|
||||
<div class="sam-flex-grow"></div>
|
||||
|
||||
@if(isSelected) {
|
||||
<lib-icon-button
|
||||
icon="star-fill"
|
||||
title="Edit identity"
|
||||
style="pointer-events: none; color: var(--bs-pink)"
|
||||
></lib-icon-button>
|
||||
}
|
||||
|
||||
<div class="buttons sam-flex-row gap-h">
|
||||
@if(!isSelected) {
|
||||
<img
|
||||
class="avatar"
|
||||
[src]="getAvatarUrl(identity)"
|
||||
alt=""
|
||||
(error)="$any($event.target).src = 'assets/person-fill.svg'"
|
||||
/>
|
||||
<span class="name">{{ getDisplayName(identity) }}</span>
|
||||
<lib-icon-button
|
||||
icon="star-fill"
|
||||
title="Select identity"
|
||||
(click)="
|
||||
onClickSwitchIdentity(identity.id, $event);
|
||||
toast.show('Identity changed')
|
||||
"
|
||||
icon="gear"
|
||||
title="Identity settings"
|
||||
(click)="onClickEditIdentity(identity.id, $event)"
|
||||
></lib-icon-button>
|
||||
}
|
||||
</div>
|
||||
<lib-icon-button
|
||||
icon="arrow-right"
|
||||
title="Edit identity"
|
||||
style="pointer-events: none"
|
||||
></lib-icon-button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<lib-toast #toast></lib-toast>
|
||||
|
||||
@@ -35,34 +35,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.identity {
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
height: 56px;
|
||||
min-height: 56px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
gap: 12px;
|
||||
padding-left: 12px;
|
||||
padding-right: 8px;
|
||||
background: var(--background-light);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
.not-active {
|
||||
//color: #525b6a;
|
||||
opacity: 0.4;
|
||||
}
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--background-light-hover);
|
||||
|
||||
.buttons {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
visibility: hidden;
|
||||
&.selected {
|
||||
background: rgba(255, 62, 181, 0.15);
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
IconButtonComponent,
|
||||
Identity_DECRYPTED,
|
||||
NostrHelper,
|
||||
ProfileMetadata,
|
||||
ProfileMetadataService,
|
||||
StorageService,
|
||||
ToastComponent,
|
||||
} from '@common';
|
||||
@@ -13,21 +16,48 @@ import {
|
||||
styleUrl: './identities.component.scss',
|
||||
imports: [IconButtonComponent, ToastComponent],
|
||||
})
|
||||
export class IdentitiesComponent {
|
||||
export class IdentitiesComponent implements OnInit {
|
||||
readonly storage = inject(StorageService);
|
||||
|
||||
readonly #router = inject(Router);
|
||||
readonly #profileMetadata = inject(ProfileMetadataService);
|
||||
|
||||
// Cache of pubkey -> profile for quick lookup
|
||||
#profileCache = new Map<string, ProfileMetadata | null>();
|
||||
|
||||
async ngOnInit() {
|
||||
await this.#profileMetadata.initialize();
|
||||
this.#loadProfiles();
|
||||
}
|
||||
|
||||
#loadProfiles() {
|
||||
const identities = this.storage.getBrowserSessionHandler().browserSessionData?.identities ?? [];
|
||||
for (const identity of identities) {
|
||||
const pubkey = NostrHelper.pubkeyFromPrivkey(identity.privkey);
|
||||
const profile = this.#profileMetadata.getCachedProfile(pubkey);
|
||||
this.#profileCache.set(identity.id, profile);
|
||||
}
|
||||
}
|
||||
|
||||
getAvatarUrl(identity: Identity_DECRYPTED): string {
|
||||
const profile = this.#profileCache.get(identity.id);
|
||||
return profile?.picture || 'assets/person-fill.svg';
|
||||
}
|
||||
|
||||
getDisplayName(identity: Identity_DECRYPTED): string {
|
||||
const profile = this.#profileCache.get(identity.id) ?? null;
|
||||
return this.#profileMetadata.getDisplayName(profile) || identity.nick;
|
||||
}
|
||||
|
||||
onClickNewIdentity() {
|
||||
this.#router.navigateByUrl('/new-identity');
|
||||
}
|
||||
|
||||
onClickEditIdentity(identity: Identity_DECRYPTED) {
|
||||
this.#router.navigateByUrl(`/edit-identity/${identity.id}/home`);
|
||||
onClickEditIdentity(identityId: string, event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
this.#router.navigateByUrl(`/edit-identity/${identityId}/home`);
|
||||
}
|
||||
|
||||
async onClickSwitchIdentity(identityId: string, event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
async onClickSelectIdentity(identityId: string) {
|
||||
await this.storage.switchIdentity(identityId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,3 @@
|
||||
git.mleku.dev/mleku/plebeian-signer
|
||||
</a>
|
||||
|
||||
<div class="sam-flex-grow"></div>
|
||||
|
||||
<div class="sam-card sam-mb" style="align-items: center">
|
||||
<span>
|
||||
Made with <i class="bi bi-heart-fill" style="color: red"></i> by
|
||||
<a href="https://sam-hayes.org" target="_blank">Sam Hayes</a>
|
||||
</span>
|
||||
|
||||
<lib-pubkey
|
||||
class="sam-mt-h"
|
||||
value="npub1tgyjshvelwj73t3jy0n3xllgt03elkapfl3k3n0x2wkunegkgrwssfp0u4"
|
||||
(click)="toast.show('Copied to clipboard')"
|
||||
></lib-pubkey>
|
||||
</div>
|
||||
|
||||
<lib-toast #toast [bottom]="188"></lib-toast>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { PubkeyComponent, ToastComponent } from '@common';
|
||||
import packageJson from '../../../../../../../package.json';
|
||||
|
||||
@Component({
|
||||
selector: 'app-info',
|
||||
imports: [PubkeyComponent, ToastComponent],
|
||||
templateUrl: './info.component.html',
|
||||
styleUrl: './info.component.scss',
|
||||
})
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
placeholder="vault password"
|
||||
[(ngModel)]="loginPassword"
|
||||
(keyup.enter)="loginPassword && loginVault()"
|
||||
autofocus
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { AfterViewInit, Component, ElementRef, inject, ViewChild } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
@@ -16,7 +16,9 @@ import { getNewStorageServiceConfig } from '../../common/data/get-new-storage-se
|
||||
styleUrl: './vault-login.component.scss',
|
||||
imports: [FormsModule, ConfirmComponent],
|
||||
})
|
||||
export class VaultLoginComponent {
|
||||
export class VaultLoginComponent implements AfterViewInit {
|
||||
@ViewChild('passwordInputElement') passwordInput!: ElementRef<HTMLInputElement>;
|
||||
|
||||
loginPassword = '';
|
||||
showInvalidPasswordAlert = false;
|
||||
|
||||
@@ -25,6 +27,10 @@ export class VaultLoginComponent {
|
||||
readonly #startup = inject(StartupService);
|
||||
readonly #profileMetadata = inject(ProfileMetadataService);
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.passwordInput.nativeElement.focus();
|
||||
}
|
||||
|
||||
toggleType(element: HTMLInputElement) {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "Plebeian Signer",
|
||||
"description": "Nostr Identity Manager & Signer",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"homepage_url": "https://git.mleku.dev/mleku/plebeian-signer",
|
||||
"options_page": "options.html",
|
||||
"permissions": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->
|
||||
<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->
|
||||
<div class="custom-header" style="position: sticky; top: 0">
|
||||
<span class="text">Identities </span>
|
||||
<span class="text">Identities</span>
|
||||
|
||||
<button class="button btn btn-primary btn-sm" (click)="onClickNewIdentity()">
|
||||
<div class="sam-flex-row gap-h">
|
||||
@@ -12,67 +12,37 @@
|
||||
</div>
|
||||
|
||||
@let sessionData = storage.getBrowserSessionHandler().browserSessionData;
|
||||
<!-- - -->
|
||||
@let identities = sessionData?.identities ?? []; @if(identities.length === 0) {
|
||||
<div
|
||||
style="
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
@let identities = sessionData?.identities ?? [];
|
||||
|
||||
@if(identities.length === 0) {
|
||||
<div class="empty-state">
|
||||
<span class="sam-text-muted">
|
||||
Create your first identity by clicking on the button in the upper right
|
||||
corner.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
} @for(identity of identities; track identity) {
|
||||
<div
|
||||
class="identity"
|
||||
style="overflow: hidden"
|
||||
(click)="onClickEditIdentity(identity)"
|
||||
>
|
||||
@for(identity of identities; track identity.id) {
|
||||
@let isSelected = identity.id === sessionData?.selectedIdentityId;
|
||||
|
||||
<span
|
||||
class="no-select"
|
||||
style="overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap"
|
||||
[class.not-active]="!isSelected"
|
||||
<div
|
||||
class="identity"
|
||||
[class.selected]="isSelected"
|
||||
(click)="onClickSelectIdentity(identity.id)"
|
||||
>
|
||||
{{ identity.nick }}
|
||||
</span>
|
||||
|
||||
<div class="sam-flex-grow"></div>
|
||||
|
||||
@if(isSelected) {
|
||||
<lib-icon-button
|
||||
icon="star-fill"
|
||||
title="Edit identity"
|
||||
style="pointer-events: none; color: var(--bs-pink)"
|
||||
></lib-icon-button>
|
||||
}
|
||||
|
||||
<div class="buttons sam-flex-row gap-h">
|
||||
@if(!isSelected) {
|
||||
<img
|
||||
class="avatar"
|
||||
[src]="getAvatarUrl(identity)"
|
||||
alt=""
|
||||
(error)="$any($event.target).src = 'assets/person-fill.svg'"
|
||||
/>
|
||||
<span class="name">{{ getDisplayName(identity) }}</span>
|
||||
<lib-icon-button
|
||||
icon="star-fill"
|
||||
title="Select identity"
|
||||
(click)="
|
||||
onClickSwitchIdentity(identity.id, $event);
|
||||
toast.show('Identity changed')
|
||||
"
|
||||
icon="gear"
|
||||
title="Identity settings"
|
||||
(click)="onClickEditIdentity(identity.id, $event)"
|
||||
></lib-icon-button>
|
||||
}
|
||||
</div>
|
||||
<lib-icon-button
|
||||
icon="arrow-right"
|
||||
title="Edit identity"
|
||||
style="pointer-events: none"
|
||||
></lib-icon-button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<lib-toast #toast></lib-toast>
|
||||
|
||||
@@ -35,34 +35,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.identity {
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
height: 56px;
|
||||
min-height: 56px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
gap: 12px;
|
||||
padding-left: 12px;
|
||||
padding-right: 8px;
|
||||
background: var(--background-light);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
.not-active {
|
||||
//color: #525b6a;
|
||||
opacity: 0.4;
|
||||
}
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--background-light-hover);
|
||||
|
||||
.buttons {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
visibility: hidden;
|
||||
&.selected {
|
||||
background: rgba(255, 62, 181, 0.15);
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
IconButtonComponent,
|
||||
Identity_DECRYPTED,
|
||||
NostrHelper,
|
||||
ProfileMetadata,
|
||||
ProfileMetadataService,
|
||||
StorageService,
|
||||
ToastComponent,
|
||||
} from '@common';
|
||||
@@ -13,21 +16,48 @@ import {
|
||||
templateUrl: './identities.component.html',
|
||||
styleUrl: './identities.component.scss',
|
||||
})
|
||||
export class IdentitiesComponent {
|
||||
export class IdentitiesComponent implements OnInit {
|
||||
readonly storage = inject(StorageService);
|
||||
|
||||
readonly #router = inject(Router);
|
||||
readonly #profileMetadata = inject(ProfileMetadataService);
|
||||
|
||||
// Cache of pubkey -> profile for quick lookup
|
||||
#profileCache = new Map<string, ProfileMetadata | null>();
|
||||
|
||||
async ngOnInit() {
|
||||
await this.#profileMetadata.initialize();
|
||||
this.#loadProfiles();
|
||||
}
|
||||
|
||||
#loadProfiles() {
|
||||
const identities = this.storage.getBrowserSessionHandler().browserSessionData?.identities ?? [];
|
||||
for (const identity of identities) {
|
||||
const pubkey = NostrHelper.pubkeyFromPrivkey(identity.privkey);
|
||||
const profile = this.#profileMetadata.getCachedProfile(pubkey);
|
||||
this.#profileCache.set(identity.id, profile);
|
||||
}
|
||||
}
|
||||
|
||||
getAvatarUrl(identity: Identity_DECRYPTED): string {
|
||||
const profile = this.#profileCache.get(identity.id);
|
||||
return profile?.picture || 'assets/person-fill.svg';
|
||||
}
|
||||
|
||||
getDisplayName(identity: Identity_DECRYPTED): string {
|
||||
const profile = this.#profileCache.get(identity.id) ?? null;
|
||||
return this.#profileMetadata.getDisplayName(profile) || identity.nick;
|
||||
}
|
||||
|
||||
onClickNewIdentity() {
|
||||
this.#router.navigateByUrl('/new-identity');
|
||||
}
|
||||
|
||||
onClickEditIdentity(identity: Identity_DECRYPTED) {
|
||||
this.#router.navigateByUrl(`/edit-identity/${identity.id}/home`);
|
||||
onClickEditIdentity(identityId: string, event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
this.#router.navigateByUrl(`/edit-identity/${identityId}/home`);
|
||||
}
|
||||
|
||||
async onClickSwitchIdentity(identityId: string, event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
async onClickSelectIdentity(identityId: string) {
|
||||
await this.storage.switchIdentity(identityId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,19 +11,3 @@
|
||||
git.mleku.dev/mleku/plebeian-signer
|
||||
</a>
|
||||
|
||||
<div class="sam-flex-grow"></div>
|
||||
|
||||
<div class="sam-card sam-mb" style="align-items: center">
|
||||
<span>
|
||||
Made with <i class="bi bi-heart-fill" style="color: red"></i> by
|
||||
<a href="https://sam-hayes.org" target="_blank">Sam Hayes</a>
|
||||
</span>
|
||||
|
||||
<lib-pubkey
|
||||
class="sam-mt-h"
|
||||
value="npub1tgyjshvelwj73t3jy0n3xllgt03elkapfl3k3n0x2wkunegkgrwssfp0u4"
|
||||
(click)="toast.show('Copied to clipboard')"
|
||||
></lib-pubkey>
|
||||
</div>
|
||||
|
||||
<lib-toast #toast [bottom]="188"></lib-toast>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { PubkeyComponent, ToastComponent } from '@common';
|
||||
import packageJson from '../../../../../../../package.json';
|
||||
|
||||
@Component({
|
||||
selector: 'app-info',
|
||||
imports: [PubkeyComponent, ToastComponent],
|
||||
templateUrl: './info.component.html',
|
||||
styleUrl: './info.component.scss',
|
||||
})
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
placeholder="vault password"
|
||||
[(ngModel)]="loginPassword"
|
||||
(keyup.enter)="loginPassword && loginVault()"
|
||||
autofocus
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { AfterViewInit, Component, ElementRef, inject, ViewChild } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
@@ -16,7 +16,9 @@ import { getNewStorageServiceConfig } from '../../common/data/get-new-storage-se
|
||||
styleUrl: './vault-login.component.scss',
|
||||
imports: [FormsModule, ConfirmComponent],
|
||||
})
|
||||
export class VaultLoginComponent {
|
||||
export class VaultLoginComponent implements AfterViewInit {
|
||||
@ViewChild('passwordInputElement') passwordInput!: ElementRef<HTMLInputElement>;
|
||||
|
||||
loginPassword = '';
|
||||
showInvalidPasswordAlert = false;
|
||||
|
||||
@@ -25,6 +27,10 @@ export class VaultLoginComponent {
|
||||
readonly #startup = inject(StartupService);
|
||||
readonly #profileMetadata = inject(ProfileMetadataService);
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.passwordInput.nativeElement.focus();
|
||||
}
|
||||
|
||||
toggleType(element: HTMLInputElement) {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
|
||||
Reference in New Issue
Block a user