Release v1.2.1 - Add quick-add mint list to Cashu wallet

- Add suggested mints list (Minibits, Coinos, 21Mint, Macadamia, Stablenut)
- Show quick-add menu on empty Cashu page with + icon and descriptions
- Add collapsible "Quick Add" disclosure when mints exist
- Hide already-added mints from the list
- Closes #6

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
woikos
2026-01-06 18:00:48 +01:00
parent 58e9053867
commit cdc98f23e5
8 changed files with 582 additions and 3 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "plebeian-signer",
"version": "1.2.0",
"version": "1.2.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "plebeian-signer",
"version": "1.2.0",
"version": "1.2.1",
"dependencies": {
"@angular/animations": "^19.0.0",
"@angular/common": "^19.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "plebeian-signer",
"version": "1.2.0",
"version": "1.2.1",
"custom": {
"chrome": {
"version": "v1.1.6"

View File

@@ -122,6 +122,31 @@
<button class="show-info-btn" (click)="showCashuInfo = true">
Show storage info
</button>
<!-- Suggested mints for quick-add -->
<div class="quick-add-section">
<div class="quick-add-label">Quick Add a Mint</div>
<div class="quick-add-menu">
@for (mint of suggestedMints; track mint.url) {
@if (!isMintAlreadyAdded(mint.url)) {
<button
class="quick-add-item"
[disabled]="addingMint"
(click)="quickAddMint(mint)"
>
<span class="mint-row">
<span class="add-icon">+</span>
<span class="mint-name">{{ mint.name }}</span>
</span>
<span class="mint-desc">{{ mint.description }}</span>
</button>
}
}
</div>
@if (mintError) {
<div class="error-message small">{{ mintError }}</div>
}
</div>
</div>
}
</div>
@@ -134,6 +159,33 @@
</button>
}
</div>
<!-- Quick add disclosure when mints exist -->
@if (hasUnavailableMints()) {
<details class="quick-add-disclosure">
<summary>Quick Add</summary>
<div class="quick-add-menu">
@for (mint of suggestedMints; track mint.url) {
@if (!isMintAlreadyAdded(mint.url)) {
<button
class="quick-add-item"
[disabled]="addingMint"
(click)="quickAddMint(mint)"
>
<span class="mint-row">
<span class="add-icon">+</span>
<span class="mint-name">{{ mint.name }}</span>
</span>
<span class="mint-desc">{{ mint.description }}</span>
</button>
}
}
</div>
@if (mintError) {
<div class="error-message small">{{ mintError }}</div>
}
</details>
}
}
<button class="add-wallet-btn" (click)="showAddMint()">
<span class="emoji">+</span>
@@ -210,6 +262,31 @@
<!-- Cashu add mint form -->
@else if (activeSection === 'cashu-add') {
<div class="add-wallet-form">
<!-- Suggested mints -->
<div class="suggested-mints">
<div class="suggested-label">Quick Add</div>
<div class="suggested-list">
@for (mint of suggestedMints; track mint.url) {
<button
class="suggested-mint-btn"
[class.already-added]="isMintAlreadyAdded(mint.url)"
[disabled]="isMintAlreadyAdded(mint.url) || addingMint"
(click)="selectSuggestedMint(mint)"
[title]="mint.description"
>
<span class="mint-name">{{ mint.name }}</span>
@if (isMintAlreadyAdded(mint.url)) {
<span class="added-badge"></span>
}
</button>
}
</div>
</div>
<div class="form-divider">
<span>or enter manually</span>
</div>
<div class="form-group">
<label for="mintName">Mint Name</label>
<input

View File

@@ -1134,3 +1134,182 @@
padding: var(--size);
}
}
// Suggested mints quick-add
.suggested-mints {
display: flex;
flex-direction: column;
gap: var(--size-h);
&.centered {
align-items: center;
margin-top: var(--size);
.suggested-list {
justify-content: center;
}
}
.suggested-label {
font-size: 0.75rem;
color: var(--muted-foreground);
text-transform: uppercase;
font-weight: 500;
}
.suggested-list {
display: flex;
flex-wrap: wrap;
gap: var(--size-h);
}
.suggested-mint-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
background: var(--background-light);
border: 1px solid var(--border);
border-radius: var(--radius-md);
cursor: pointer;
font-size: 0.8rem;
color: var(--foreground);
transition: all 0.15s ease;
&:hover:not(:disabled) {
background: var(--background-light-hover);
border-color: var(--primary);
}
&:disabled {
cursor: not-allowed;
}
&.already-added {
opacity: 0.5;
background: transparent;
.added-badge {
color: var(--success, #22c55e);
font-size: 0.7rem;
}
}
.mint-name {
font-weight: 500;
}
}
}
.form-divider {
display: flex;
align-items: center;
gap: var(--size);
color: var(--muted-foreground);
font-size: 0.75rem;
&::before,
&::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
}
// Quick add section (empty state)
.quick-add-section {
width: 100%;
margin-top: var(--size);
.quick-add-label {
font-size: 0.75rem;
color: var(--muted-foreground);
text-transform: uppercase;
font-weight: 500;
text-align: center;
margin-bottom: var(--size-h);
}
}
// Quick add disclosure (when mints exist)
.quick-add-disclosure {
margin-top: var(--size-h);
summary {
font-size: 0.8rem;
color: var(--muted-foreground);
cursor: pointer;
padding: var(--size-h);
text-align: center;
user-select: none;
&:hover {
color: var(--foreground);
}
&::marker {
color: var(--muted-foreground);
}
}
&[open] summary {
margin-bottom: var(--size-h);
}
}
// Quick add menu (shared by both)
.quick-add-menu {
display: flex;
flex-direction: column;
gap: 2px;
background: var(--background-light);
border-radius: var(--radius-md);
overflow: hidden;
}
.quick-add-item {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
padding: var(--size-h) var(--size);
background: transparent;
border: none;
cursor: pointer;
text-align: left;
transition: background-color 0.15s ease;
&:hover:not(:disabled) {
background: var(--background-light-hover);
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.mint-row {
display: flex;
align-items: center;
gap: var(--size-h);
}
.add-icon {
font-size: 1rem;
font-weight: 600;
color: var(--success, #22c55e);
}
.mint-name {
font-size: 0.875rem;
font-weight: 500;
color: var(--foreground);
}
.mint-desc {
font-size: 0.75rem;
color: var(--muted-foreground);
padding-left: calc(1rem + var(--size-h));
}
}

View File

@@ -123,6 +123,15 @@ export class WalletComponent extends NavComponent implements OnInit, OnDestroy {
refreshingMint = false;
refreshError = '';
// Suggested mints for quick-add
readonly suggestedMints = [
{ name: 'Minibits', url: 'https://mint.minibits.cash', description: 'Well-established mobile wallet mint' },
{ name: 'Coinos', url: 'https://mint.coinos.io', description: 'Lightning wallet with Cashu integration' },
{ name: '21Mint', url: 'https://21mint.me', description: 'Community mint' },
{ name: 'Macadamia', url: 'https://mint.macadamia.cash', description: 'Reliable community mint' },
{ name: 'Stablenut (USD)', url: 'https://stablenut.umint.cash', unit: 'usd', description: 'USD-denominated mint' },
];
get title(): string {
switch (this.activeSection) {
case 'cashu':
@@ -499,6 +508,35 @@ export class WalletComponent extends NavComponent implements OnInit, OnDestroy {
}
}
selectSuggestedMint(mint: { name: string; url: string }) {
this.newMintName = mint.name;
this.newMintUrl = mint.url;
this.mintError = '';
this.mintTestResult = '';
}
isMintAlreadyAdded(mintUrl: string): boolean {
return this.mints.some(m => m.mintUrl === mintUrl);
}
hasUnavailableMints(): boolean {
return this.suggestedMints.some(m => !this.isMintAlreadyAdded(m.url));
}
async quickAddMint(mint: { name: string; url: string }) {
this.addingMint = true;
this.mintError = '';
try {
await this.cashuService.addMint(mint.name, mint.url);
} catch (error) {
this.mintError =
error instanceof Error ? error.message : 'Failed to add mint';
} finally {
this.addingMint = false;
}
}
async deleteMint() {
if (!this.selectedMintId) return;

View File

@@ -122,6 +122,31 @@
<button class="show-info-btn" (click)="showCashuInfo = true">
Show storage info
</button>
<!-- Suggested mints for quick-add -->
<div class="quick-add-section">
<div class="quick-add-label">Quick Add a Mint</div>
<div class="quick-add-menu">
@for (mint of suggestedMints; track mint.url) {
@if (!isMintAlreadyAdded(mint.url)) {
<button
class="quick-add-item"
[disabled]="addingMint"
(click)="quickAddMint(mint)"
>
<span class="mint-row">
<span class="add-icon">+</span>
<span class="mint-name">{{ mint.name }}</span>
</span>
<span class="mint-desc">{{ mint.description }}</span>
</button>
}
}
</div>
@if (mintError) {
<div class="error-message small">{{ mintError }}</div>
}
</div>
</div>
}
</div>
@@ -134,6 +159,33 @@
</button>
}
</div>
<!-- Quick add disclosure when mints exist -->
@if (hasUnavailableMints()) {
<details class="quick-add-disclosure">
<summary>Quick Add</summary>
<div class="quick-add-menu">
@for (mint of suggestedMints; track mint.url) {
@if (!isMintAlreadyAdded(mint.url)) {
<button
class="quick-add-item"
[disabled]="addingMint"
(click)="quickAddMint(mint)"
>
<span class="mint-row">
<span class="add-icon">+</span>
<span class="mint-name">{{ mint.name }}</span>
</span>
<span class="mint-desc">{{ mint.description }}</span>
</button>
}
}
</div>
@if (mintError) {
<div class="error-message small">{{ mintError }}</div>
}
</details>
}
}
<button class="add-wallet-btn" (click)="showAddMint()">
<span class="emoji">+</span>
@@ -210,6 +262,31 @@
<!-- Cashu add mint form -->
@else if (activeSection === 'cashu-add') {
<div class="add-wallet-form">
<!-- Suggested mints -->
<div class="suggested-mints">
<div class="suggested-label">Quick Add</div>
<div class="suggested-list">
@for (mint of suggestedMints; track mint.url) {
<button
class="suggested-mint-btn"
[class.already-added]="isMintAlreadyAdded(mint.url)"
[disabled]="isMintAlreadyAdded(mint.url) || addingMint"
(click)="selectSuggestedMint(mint)"
[title]="mint.description"
>
<span class="mint-name">{{ mint.name }}</span>
@if (isMintAlreadyAdded(mint.url)) {
<span class="added-badge"></span>
}
</button>
}
</div>
</div>
<div class="form-divider">
<span>or enter manually</span>
</div>
<div class="form-group">
<label for="mintName">Mint Name</label>
<input

View File

@@ -1134,3 +1134,173 @@
padding: var(--size);
}
}
// Suggested mints quick-add
.suggested-mints {
display: flex;
flex-direction: column;
gap: var(--size-h);
.suggested-label {
font-size: 0.75rem;
color: var(--muted-foreground);
text-transform: uppercase;
font-weight: 500;
}
.suggested-list {
display: flex;
flex-wrap: wrap;
gap: var(--size-h);
}
.suggested-mint-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
background: var(--background-light);
border: 1px solid var(--border);
border-radius: var(--radius-md);
cursor: pointer;
font-size: 0.8rem;
color: var(--foreground);
transition: all 0.15s ease;
&:hover:not(:disabled) {
background: var(--background-light-hover);
border-color: var(--primary);
}
&:disabled {
cursor: not-allowed;
}
&.already-added {
opacity: 0.5;
background: transparent;
.added-badge {
color: var(--success, #22c55e);
font-size: 0.7rem;
}
}
.mint-name {
font-weight: 500;
}
}
}
.form-divider {
display: flex;
align-items: center;
gap: var(--size);
color: var(--muted-foreground);
font-size: 0.75rem;
&::before,
&::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
}
// Quick add section (empty state)
.quick-add-section {
width: 100%;
margin-top: var(--size);
.quick-add-label {
font-size: 0.75rem;
color: var(--muted-foreground);
text-transform: uppercase;
font-weight: 500;
text-align: center;
margin-bottom: var(--size-h);
}
}
// Quick add disclosure (when mints exist)
.quick-add-disclosure {
margin-top: var(--size-h);
summary {
font-size: 0.8rem;
color: var(--muted-foreground);
cursor: pointer;
padding: var(--size-h);
text-align: center;
user-select: none;
&:hover {
color: var(--foreground);
}
&::marker {
color: var(--muted-foreground);
}
}
&[open] summary {
margin-bottom: var(--size-h);
}
}
// Quick add menu (shared by both)
.quick-add-menu {
display: flex;
flex-direction: column;
gap: 2px;
background: var(--background-light);
border-radius: var(--radius-md);
overflow: hidden;
}
.quick-add-item {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
padding: var(--size-h) var(--size);
background: transparent;
border: none;
cursor: pointer;
text-align: left;
transition: background-color 0.15s ease;
&:hover:not(:disabled) {
background: var(--background-light-hover);
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.mint-row {
display: flex;
align-items: center;
gap: var(--size-h);
}
.add-icon {
font-size: 1rem;
font-weight: 600;
color: var(--success, #22c55e);
}
.mint-name {
font-size: 0.875rem;
font-weight: 500;
color: var(--foreground);
}
.mint-desc {
font-size: 0.75rem;
color: var(--muted-foreground);
padding-left: calc(1rem + var(--size-h));
}
}

View File

@@ -123,6 +123,15 @@ export class WalletComponent extends NavComponent implements OnInit, OnDestroy {
refreshingMint = false;
refreshError = '';
// Suggested mints for quick-add
readonly suggestedMints = [
{ name: 'Minibits', url: 'https://mint.minibits.cash', description: 'Well-established mobile wallet mint' },
{ name: 'Coinos', url: 'https://mint.coinos.io', description: 'Lightning wallet with Cashu integration' },
{ name: '21Mint', url: 'https://21mint.me', description: 'Community mint' },
{ name: 'Macadamia', url: 'https://mint.macadamia.cash', description: 'Reliable community mint' },
{ name: 'Stablenut (USD)', url: 'https://stablenut.umint.cash', unit: 'usd', description: 'USD-denominated mint' },
];
get title(): string {
switch (this.activeSection) {
case 'cashu':
@@ -499,6 +508,35 @@ export class WalletComponent extends NavComponent implements OnInit, OnDestroy {
}
}
selectSuggestedMint(mint: { name: string; url: string }) {
this.newMintName = mint.name;
this.newMintUrl = mint.url;
this.mintError = '';
this.mintTestResult = '';
}
isMintAlreadyAdded(mintUrl: string): boolean {
return this.mints.some(m => m.mintUrl === mintUrl);
}
hasUnavailableMints(): boolean {
return this.suggestedMints.some(m => !this.isMintAlreadyAdded(m.url));
}
async quickAddMint(mint: { name: string; url: string }) {
this.addingMint = true;
this.mintError = '';
try {
await this.cashuService.addMint(mint.name, mint.url);
} catch (error) {
this.mintError =
error instanceof Error ? error.message : 'Failed to add mint';
} finally {
this.addingMint = false;
}
}
async deleteMint() {
if (!this.selectedMintId) return;