refactor(libra): encode status on border, type on signed amount
Original styling used the left border for entry-type (green=income,
red=expense), which clashed visually with the status icons: a red
border on a pending expense suggested "rejected". Split the visual
channels so each conveys one thing:
- Left border + status icon = workflow state (green accepted,
yellow pending, red rejected/voided)
- Signed/tinted amount = type (+green income, -red expense)
- Strike-through + muted amount = voided
- Badges = user-added tags only; income-entry / expense-entry
suppressed (redundant with the amount), Voided kept as a
high-signal status badge
Follows the conventional bank-statement / personal-finance encoding
(Wise, Mint, YNAB), where amount sign carries direction and chrome
carries state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4af220adda
commit
5db2dbe8a8
1 changed files with 58 additions and 24 deletions
|
|
@ -123,7 +123,7 @@ function getStatusInfo(transaction: Transaction) {
|
||||||
case '*':
|
case '*':
|
||||||
return { icon: CheckCircle2, color: 'text-green-600', label: 'Cleared' }
|
return { icon: CheckCircle2, color: 'text-green-600', label: 'Cleared' }
|
||||||
case '!':
|
case '!':
|
||||||
return { icon: Clock, color: 'text-orange-600', label: 'Pending' }
|
return { icon: Clock, color: 'text-yellow-500', label: 'Pending' }
|
||||||
case '#':
|
case '#':
|
||||||
return { icon: Flag, color: 'text-red-600', label: 'Flagged' }
|
return { icon: Flag, color: 'text-red-600', label: 'Flagged' }
|
||||||
default:
|
default:
|
||||||
|
|
@ -131,6 +131,39 @@ function getStatusInfo(transaction: Transaction) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Left-border color encodes status (accepted / pending / rejected).
|
||||||
|
// Entry-type is communicated separately via the signed/colored amount.
|
||||||
|
function getStatusBorderClass(t: Transaction): string {
|
||||||
|
if (isVoided(t)) return 'border-l-red-600'
|
||||||
|
if (t.flag === '*') return 'border-l-green-600'
|
||||||
|
if (t.flag === '!') return 'border-l-yellow-500'
|
||||||
|
return 'border-l-transparent'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Income gets a leading '+', expense a leading '-'.
|
||||||
|
function getAmountSign(t: Transaction): string {
|
||||||
|
if (isIncome(t)) return '+'
|
||||||
|
if (isExpense(t)) return '-'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color tint for the amount text. Voided entries drop to muted regardless
|
||||||
|
// of type since the strike-through carries the "ignore this" signal.
|
||||||
|
function getAmountColorClass(t: Transaction): string {
|
||||||
|
if (isVoided(t)) return 'line-through text-muted-foreground'
|
||||||
|
if (isIncome(t)) return 'text-green-600 dark:text-green-400'
|
||||||
|
if (isExpense(t)) return 'text-red-600 dark:text-red-400'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags that drive other visual channels (border / sign / strike-through) —
|
||||||
|
// suppressed from the badge row so it only carries user-added tags.
|
||||||
|
const TYPE_TAGS = new Set(['income-entry', 'expense-entry', 'voided'])
|
||||||
|
|
||||||
|
function getDisplayTags(t: Transaction): string[] {
|
||||||
|
return (t.tags ?? []).filter(tag => !TYPE_TAGS.has(tag))
|
||||||
|
}
|
||||||
|
|
||||||
// Load transactions
|
// Load transactions
|
||||||
async function loadTransactions() {
|
async function loadTransactions() {
|
||||||
if (!walletKey.value) {
|
if (!walletKey.value) {
|
||||||
|
|
@ -324,9 +357,8 @@ onMounted(() => {
|
||||||
v-for="transaction in transactionsToDisplay"
|
v-for="transaction in transactionsToDisplay"
|
||||||
:key="transaction.id"
|
:key="transaction.id"
|
||||||
:class="[
|
:class="[
|
||||||
'border-b md:border md:rounded-lg p-4 hover:bg-muted/50 transition-colors',
|
'border-b md:border md:rounded-lg p-4 hover:bg-muted/50 transition-colors border-l-4',
|
||||||
isIncome(transaction) && 'border-l-4 border-l-green-600',
|
getStatusBorderClass(transaction)
|
||||||
isExpense(transaction) && 'border-l-4 border-l-red-600'
|
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<!-- Transaction Header -->
|
<!-- Transaction Header -->
|
||||||
|
|
@ -358,22 +390,17 @@ onMounted(() => {
|
||||||
|
|
||||||
<!-- Amount -->
|
<!-- Amount -->
|
||||||
<div class="text-right flex-shrink-0">
|
<div class="text-right flex-shrink-0">
|
||||||
<p
|
<p :class="['font-semibold text-sm sm:text-base', getAmountColorClass(transaction)]">
|
||||||
:class="[
|
{{ getAmountSign(transaction) }}{{ formatAmount(transaction.amount) }} sats
|
||||||
'font-semibold text-sm sm:text-base',
|
|
||||||
isVoided(transaction) && 'line-through text-muted-foreground'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ formatAmount(transaction.amount) }} sats
|
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
v-if="transaction.fiat_amount"
|
v-if="transaction.fiat_amount"
|
||||||
:class="[
|
:class="[
|
||||||
'text-xs text-muted-foreground',
|
'text-xs',
|
||||||
isVoided(transaction) && 'line-through'
|
getAmountColorClass(transaction) || 'text-muted-foreground'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ transaction.fiat_amount.toFixed(2) }} {{ transaction.fiat_currency }}
|
{{ getAmountSign(transaction) }}{{ transaction.fiat_amount.toFixed(2) }} {{ transaction.fiat_currency }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -390,20 +417,27 @@ onMounted(() => {
|
||||||
<span class="font-medium">Ref:</span> {{ transaction.reference }}
|
<span class="font-medium">Ref:</span> {{ transaction.reference }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tags -->
|
<!-- Tags: Voided badge (status) + any user-added tags. Type
|
||||||
<div v-if="transaction.tags && transaction.tags.length > 0" class="flex flex-wrap gap-1 mt-2">
|
tags (income-entry / expense-entry) are intentionally
|
||||||
|
suppressed — they're encoded by the signed amount. -->
|
||||||
|
<div
|
||||||
|
v-if="isVoided(transaction) || getDisplayTags(transaction).length > 0"
|
||||||
|
class="flex flex-wrap gap-1 mt-2"
|
||||||
|
>
|
||||||
<Badge
|
<Badge
|
||||||
v-for="tag in transaction.tags"
|
v-if="isVoided(transaction)"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-xs bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-300"
|
||||||
|
>
|
||||||
|
Voided
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-for="tag in getDisplayTags(transaction)"
|
||||||
:key="tag"
|
:key="tag"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
:class="[
|
class="text-xs"
|
||||||
'text-xs',
|
|
||||||
tag === 'income-entry' && 'bg-green-100 dark:bg-green-900/20 text-green-700 dark:text-green-300',
|
|
||||||
tag === 'expense-entry' && 'bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-300',
|
|
||||||
tag === 'voided' && 'bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-300'
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
{{ tag === 'voided' ? 'Voided' : tag }}
|
{{ tag }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue