501 lines
18 KiB
Plaintext
501 lines
18 KiB
Plaintext
|
|
package components
|
||
|
|
|
||
|
|
type TxType string
|
||
|
|
|
||
|
|
const (
|
||
|
|
TxTypeSend TxType = "send"
|
||
|
|
TxTypeReceive TxType = "receive"
|
||
|
|
TxTypeSwap TxType = "swap"
|
||
|
|
TxTypeApprove TxType = "approve"
|
||
|
|
TxTypeContract TxType = "contract"
|
||
|
|
)
|
||
|
|
|
||
|
|
type TxStatus string
|
||
|
|
|
||
|
|
const (
|
||
|
|
TxStatusConfirmed TxStatus = "confirmed"
|
||
|
|
TxStatusPending TxStatus = "pending"
|
||
|
|
TxStatusFailed TxStatus = "failed"
|
||
|
|
)
|
||
|
|
|
||
|
|
type TransactionDetail struct {
|
||
|
|
Type TxType
|
||
|
|
Title string
|
||
|
|
Description string
|
||
|
|
Asset string
|
||
|
|
AssetColor string
|
||
|
|
ToAsset string
|
||
|
|
ToAssetColor string
|
||
|
|
Amount string
|
||
|
|
AmountUSD string
|
||
|
|
IsPositive bool
|
||
|
|
Time string
|
||
|
|
Date string
|
||
|
|
Status TxStatus
|
||
|
|
Hash string
|
||
|
|
ExplorerURL string
|
||
|
|
}
|
||
|
|
|
||
|
|
type TxStats struct {
|
||
|
|
TotalReceived string
|
||
|
|
ReceivedCount int
|
||
|
|
TotalSent string
|
||
|
|
SentCount int
|
||
|
|
SwapCount int
|
||
|
|
SwapVolume string
|
||
|
|
GasSpent string
|
||
|
|
}
|
||
|
|
|
||
|
|
type TxFilter struct {
|
||
|
|
Types []string
|
||
|
|
Assets []string
|
||
|
|
Accounts []string
|
||
|
|
}
|
||
|
|
|
||
|
|
type TxDateGroup struct {
|
||
|
|
Date string
|
||
|
|
Count int
|
||
|
|
Transactions []TransactionDetail
|
||
|
|
}
|
||
|
|
|
||
|
|
func DefaultTxStats() TxStats {
|
||
|
|
return TxStats{
|
||
|
|
TotalReceived: "15420.50",
|
||
|
|
ReceivedCount: 42,
|
||
|
|
TotalSent: "8234.18",
|
||
|
|
SentCount: 28,
|
||
|
|
SwapCount: 12,
|
||
|
|
SwapVolume: "$4,892.00",
|
||
|
|
GasSpent: "127.45",
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func DefaultTxFilter() TxFilter {
|
||
|
|
return TxFilter{
|
||
|
|
Types: []string{"send", "receive", "swap", "approve", "contract"},
|
||
|
|
Assets: []string{"ETH", "SNR", "USDC", "AVAX"},
|
||
|
|
Accounts: []string{"Main Wallet", "Trading", "Savings"},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func DefaultTxGroups() []TxDateGroup {
|
||
|
|
return []TxDateGroup{
|
||
|
|
{
|
||
|
|
Date: "Today",
|
||
|
|
Count: 3,
|
||
|
|
Transactions: []TransactionDetail{
|
||
|
|
{Type: TxTypeReceive, Title: "Received ETH", Description: "From: 0x742d...35Cb", Asset: "ETH", AssetColor: "#627eea", Amount: "+0.25 ETH", AmountUSD: "$586.25", IsPositive: true, Time: "2:34 PM", Status: TxStatusConfirmed, Hash: "0x8f4a2b1c..."},
|
||
|
|
{Type: TxTypeSwap, Title: "Swapped SNR → USDC", Description: "via Uniswap V3", Asset: "SNR", AssetColor: "linear-gradient(135deg, #17c2ff, #0090ff)", ToAsset: "USDC", ToAssetColor: "#2775ca", Amount: "500 SNR → 248.50 USDC", AmountUSD: "$248.50", IsPositive: false, Time: "11:22 AM", Status: TxStatusConfirmed, Hash: "0x7d3e2a1b..."},
|
||
|
|
{Type: TxTypeApprove, Title: "Approved USDC", Description: "Spender: Uniswap V3 Router", Asset: "USDC", AssetColor: "#2775ca", Amount: "Unlimited", AmountUSD: "—", IsPositive: false, Time: "11:20 AM", Status: TxStatusConfirmed, Hash: "0x1a2b3c4d..."},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Date: "Yesterday",
|
||
|
|
Count: 2,
|
||
|
|
Transactions: []TransactionDetail{
|
||
|
|
{Type: TxTypeSend, Title: "Sent SNR", Description: "To: sonr1k4m...9p3q", Asset: "SNR", AssetColor: "linear-gradient(135deg, #17c2ff, #0090ff)", Amount: "-500 SNR", AmountUSD: "$250.00", IsPositive: false, Time: "4:18 PM", Status: TxStatusConfirmed, Hash: "0x9f8e7d6c..."},
|
||
|
|
{Type: TxTypeReceive, Title: "Received AVAX", Description: "From: 0xaBcD...1234", Asset: "AVAX", AssetColor: "#e84142", Amount: "+10.00 AVAX", AmountUSD: "$281.50", IsPositive: true, Time: "9:45 AM", Status: TxStatusConfirmed, Hash: "0x2b3c4d5e..."},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Date: "January 1, 2026",
|
||
|
|
Count: 4,
|
||
|
|
Transactions: []TransactionDetail{
|
||
|
|
{Type: TxTypeSwap, Title: "Swapped ETH → USDC", Description: "via Uniswap V3", Asset: "ETH", AssetColor: "#627eea", ToAsset: "USDC", ToAssetColor: "#2775ca", Amount: "0.5 ETH → 1,172.50 USDC", AmountUSD: "$1,172.50", IsPositive: false, Time: "9:15 AM", Status: TxStatusConfirmed, Hash: "0x3c4d5e6f..."},
|
||
|
|
{Type: TxTypeContract, Title: "Contract Interaction", Description: "Staking Contract: 0x5f3c...8a2b", Asset: "SNR", AssetColor: "linear-gradient(135deg, #17c2ff, #0090ff)", Amount: "-1,000 SNR", AmountUSD: "$500.00 staked", IsPositive: false, Time: "8:30 AM", Status: TxStatusConfirmed, Hash: "0x4d5e6f7a..."},
|
||
|
|
{Type: TxTypeSend, Title: "Sent ETH", Description: "To: 0x9f8e...7d6c", Asset: "ETH", AssetColor: "#627eea", Amount: "-0.15 ETH", AmountUSD: "$351.85", IsPositive: false, Time: "7:22 AM", Status: TxStatusConfirmed, Hash: "0x5e6f7a8b..."},
|
||
|
|
{Type: TxTypeReceive, Title: "Received SNR", Description: "From: sonr1abc...xyz9 (Airdrop)", Asset: "SNR", AssetColor: "linear-gradient(135deg, #17c2ff, #0090ff)", Amount: "+2,500 SNR", AmountUSD: "$1,250.00", IsPositive: true, Time: "12:00 AM", Status: TxStatusConfirmed, Hash: "0x6f7a8b9c..."},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func assetInitial(asset string) string {
|
||
|
|
if len(asset) > 0 {
|
||
|
|
return string(asset[0])
|
||
|
|
}
|
||
|
|
return "?"
|
||
|
|
}
|
||
|
|
|
||
|
|
templ TxIcon(txType TxType) {
|
||
|
|
<div class={ "tx-icon", string(txType) }>
|
||
|
|
switch txType {
|
||
|
|
case TxTypeSend:
|
||
|
|
<wa-icon name="arrow-up"></wa-icon>
|
||
|
|
case TxTypeReceive:
|
||
|
|
<wa-icon name="arrow-down"></wa-icon>
|
||
|
|
case TxTypeSwap:
|
||
|
|
<wa-icon name="arrow-right-arrow-left"></wa-icon>
|
||
|
|
case TxTypeApprove:
|
||
|
|
<wa-icon name="check"></wa-icon>
|
||
|
|
case TxTypeContract:
|
||
|
|
<wa-icon name="file-code"></wa-icon>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
templ TxAssetBadge(asset string, color string, toAsset string, toColor string) {
|
||
|
|
<div class="tx-asset">
|
||
|
|
<wa-avatar initials={ assetInitial(asset) } style={ "--size: 24px; background: " + color + ";" }></wa-avatar>
|
||
|
|
if toAsset != "" {
|
||
|
|
<wa-icon name="arrow-right" style="font-size: 10px; color: var(--wa-color-neutral-400);"></wa-icon>
|
||
|
|
<wa-avatar initials={ assetInitial(toAsset) } style={ "--size: 24px; background: " + toColor + ";" }></wa-avatar>
|
||
|
|
} else {
|
||
|
|
<span>{ asset }</span>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
templ TxAmount(amount string, usd string, isPositive bool) {
|
||
|
|
<div class={ "tx-amount", templ.KV("positive", isPositive), templ.KV("negative", !isPositive) }>
|
||
|
|
<div class="value">{ amount }</div>
|
||
|
|
<div class="usd">{ usd }</div>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
templ TxTime(time string, status TxStatus) {
|
||
|
|
<div class="tx-time">
|
||
|
|
<div class="time">{ time }</div>
|
||
|
|
switch status {
|
||
|
|
case TxStatusConfirmed:
|
||
|
|
<wa-badge variant="success" size="small">Confirmed</wa-badge>
|
||
|
|
case TxStatusPending:
|
||
|
|
<wa-badge variant="warning" size="small">Pending</wa-badge>
|
||
|
|
case TxStatusFailed:
|
||
|
|
<wa-badge variant="danger" size="small">Failed</wa-badge>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
templ TxActions(hash string) {
|
||
|
|
<div class="tx-actions">
|
||
|
|
<wa-tooltip content="View on Explorer">
|
||
|
|
<wa-icon-button name="arrow-up-right-from-square" label="Explorer"></wa-icon-button>
|
||
|
|
</wa-tooltip>
|
||
|
|
<wa-copy-button value={ hash } copy-label="Copy hash"></wa-copy-button>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
templ TxRow(tx TransactionDetail) {
|
||
|
|
<div class="tx-row">
|
||
|
|
@TxIcon(tx.Type)
|
||
|
|
<div class="tx-details">
|
||
|
|
<div class="tx-type-label">{ tx.Title }</div>
|
||
|
|
<div class="tx-address">{ tx.Description }</div>
|
||
|
|
</div>
|
||
|
|
@TxAssetBadge(tx.Asset, tx.AssetColor, tx.ToAsset, tx.ToAssetColor)
|
||
|
|
@TxAmount(tx.Amount, tx.AmountUSD, tx.IsPositive)
|
||
|
|
@TxTime(tx.Time, tx.Status)
|
||
|
|
@TxActions(tx.Hash)
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
templ TxDateGroupComponent(group TxDateGroup) {
|
||
|
|
<div class="date-group">
|
||
|
|
<div class="date-group-header">
|
||
|
|
<h3>{ group.Date }</h3>
|
||
|
|
<span class="tx-count">{ formatTxCount(group.Count) }</span>
|
||
|
|
</div>
|
||
|
|
for _, tx := range group.Transactions {
|
||
|
|
@TxRow(tx)
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
func formatTxCount(count int) string {
|
||
|
|
if count == 1 {
|
||
|
|
return "1 transaction"
|
||
|
|
}
|
||
|
|
return string(rune('0'+count/10)) + string(rune('0'+count%10)) + " transactions"
|
||
|
|
}
|
||
|
|
|
||
|
|
templ TxStatsGrid(stats TxStats) {
|
||
|
|
<div class="wa-grid stats-grid">
|
||
|
|
<wa-card class="stat-card">
|
||
|
|
<div class="wa-stack wa-gap-3xs">
|
||
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Total Received</span>
|
||
|
|
<span class="wa-heading-l change-positive">
|
||
|
|
+<wa-format-number type="currency" currency="USD" value={ stats.TotalReceived } lang="en-US"></wa-format-number>
|
||
|
|
</span>
|
||
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ formatTxCount(stats.ReceivedCount) }</span>
|
||
|
|
</div>
|
||
|
|
</wa-card>
|
||
|
|
<wa-card class="stat-card">
|
||
|
|
<div class="wa-stack wa-gap-3xs">
|
||
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Total Sent</span>
|
||
|
|
<span class="wa-heading-l change-negative">
|
||
|
|
-<wa-format-number type="currency" currency="USD" value={ stats.TotalSent } lang="en-US"></wa-format-number>
|
||
|
|
</span>
|
||
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ formatTxCount(stats.SentCount) }</span>
|
||
|
|
</div>
|
||
|
|
</wa-card>
|
||
|
|
<wa-card class="stat-card">
|
||
|
|
<div class="wa-stack wa-gap-3xs">
|
||
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Swaps</span>
|
||
|
|
<span class="wa-heading-l">{ formatSwapCount(stats.SwapCount) }</span>
|
||
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ stats.SwapVolume } volume</span>
|
||
|
|
</div>
|
||
|
|
</wa-card>
|
||
|
|
<wa-card class="stat-card">
|
||
|
|
<div class="wa-stack wa-gap-3xs">
|
||
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Gas Spent</span>
|
||
|
|
<span class="wa-heading-l">
|
||
|
|
<wa-format-number type="currency" currency="USD" value={ stats.GasSpent } lang="en-US"></wa-format-number>
|
||
|
|
</span>
|
||
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">All time</span>
|
||
|
|
</div>
|
||
|
|
</wa-card>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
func formatSwapCount(count int) string {
|
||
|
|
if count < 10 {
|
||
|
|
return string(rune('0' + count))
|
||
|
|
}
|
||
|
|
return string(rune('0'+count/10)) + string(rune('0'+count%10))
|
||
|
|
}
|
||
|
|
|
||
|
|
templ TxFilterBar(filter TxFilter) {
|
||
|
|
<div class="filter-bar">
|
||
|
|
<wa-select placeholder="All Types" size="small" style="min-width: 140px;" clearable name="filter-type">
|
||
|
|
<wa-option value="send">
|
||
|
|
<wa-icon slot="prefix" name="arrow-up" style="color: var(--wa-color-danger);"></wa-icon>
|
||
|
|
Send
|
||
|
|
</wa-option>
|
||
|
|
<wa-option value="receive">
|
||
|
|
<wa-icon slot="prefix" name="arrow-down" style="color: var(--wa-color-success);"></wa-icon>
|
||
|
|
Receive
|
||
|
|
</wa-option>
|
||
|
|
<wa-option value="swap">
|
||
|
|
<wa-icon slot="prefix" name="arrow-right-arrow-left" style="color: var(--wa-color-primary);"></wa-icon>
|
||
|
|
Swap
|
||
|
|
</wa-option>
|
||
|
|
<wa-option value="approve">
|
||
|
|
<wa-icon slot="prefix" name="check"></wa-icon>
|
||
|
|
Approve
|
||
|
|
</wa-option>
|
||
|
|
<wa-option value="contract">
|
||
|
|
<wa-icon slot="prefix" name="file-code"></wa-icon>
|
||
|
|
Contract
|
||
|
|
</wa-option>
|
||
|
|
</wa-select>
|
||
|
|
<wa-select placeholder="All Assets" size="small" style="min-width: 140px;" clearable name="filter-asset">
|
||
|
|
for _, asset := range filter.Assets {
|
||
|
|
<wa-option value={ asset }>{ asset }</wa-option>
|
||
|
|
}
|
||
|
|
</wa-select>
|
||
|
|
<wa-select placeholder="All Accounts" size="small" style="min-width: 160px;" clearable name="filter-account">
|
||
|
|
for _, account := range filter.Accounts {
|
||
|
|
<wa-option value={ account }>{ account }</wa-option>
|
||
|
|
}
|
||
|
|
</wa-select>
|
||
|
|
<wa-input type="date" size="small" style="width: 150px;" placeholder="From date" name="filter-from"></wa-input>
|
||
|
|
<span style="color: var(--wa-color-neutral-400);">—</span>
|
||
|
|
<wa-input type="date" size="small" style="width: 150px;" placeholder="To date" name="filter-to"></wa-input>
|
||
|
|
<div style="flex: 1;"></div>
|
||
|
|
<wa-input placeholder="Search by hash or address..." size="small" style="width: 240px;" name="filter-search">
|
||
|
|
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
|
||
|
|
</wa-input>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
templ TxPagination(page int, total int, perPage int) {
|
||
|
|
<div class="pagination">
|
||
|
|
<div class="pagination-info">
|
||
|
|
Showing <strong>{ formatRange(page, perPage) }</strong> of <strong>{ formatTotal(total) }</strong> transactions
|
||
|
|
</div>
|
||
|
|
<div class="pagination-controls">
|
||
|
|
<wa-select value={ formatPerPage(perPage) } size="small" style="width: 100px;" name="limit">
|
||
|
|
<wa-option value="10">10 / page</wa-option>
|
||
|
|
<wa-option value="25">25 / page</wa-option>
|
||
|
|
<wa-option value="50">50 / page</wa-option>
|
||
|
|
<wa-option value="100">100 / page</wa-option>
|
||
|
|
</wa-select>
|
||
|
|
<div class="page-numbers">
|
||
|
|
<button class={ "page-btn", templ.KV("disabled", page == 1) } disabled?={ page == 1 }>
|
||
|
|
<wa-icon name="chevron-left"></wa-icon>
|
||
|
|
</button>
|
||
|
|
@pageButtons(page, totalPages(total, perPage))
|
||
|
|
<button class={ "page-btn", templ.KV("disabled", page >= totalPages(total, perPage)) } disabled?={ page >= totalPages(total, perPage) }>
|
||
|
|
<wa-icon name="chevron-right"></wa-icon>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
func formatRange(page int, perPage int) string {
|
||
|
|
start := (page-1)*perPage + 1
|
||
|
|
end := page * perPage
|
||
|
|
return formatTotal(start) + "-" + formatTotal(end)
|
||
|
|
}
|
||
|
|
|
||
|
|
func formatTotal(n int) string {
|
||
|
|
if n < 10 {
|
||
|
|
return string(rune('0' + n))
|
||
|
|
}
|
||
|
|
if n < 100 {
|
||
|
|
return string(rune('0'+n/10)) + string(rune('0'+n%10))
|
||
|
|
}
|
||
|
|
return string(rune('0'+n/100)) + string(rune('0'+(n%100)/10)) + string(rune('0'+n%10))
|
||
|
|
}
|
||
|
|
|
||
|
|
func formatPerPage(n int) string {
|
||
|
|
return formatTotal(n)
|
||
|
|
}
|
||
|
|
|
||
|
|
func totalPages(total int, perPage int) int {
|
||
|
|
return (total + perPage - 1) / perPage
|
||
|
|
}
|
||
|
|
|
||
|
|
templ pageButtons(current int, total int) {
|
||
|
|
if total <= 5 {
|
||
|
|
for i := 1; i <= total; i++ {
|
||
|
|
<button class={ "page-btn", templ.KV("active", i == current) }>{ formatTotal(i) }</button>
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
<button class={ "page-btn", templ.KV("active", current == 1) }>1</button>
|
||
|
|
if current > 3 {
|
||
|
|
<span style="padding: 0 var(--wa-space-xs); color: var(--wa-color-neutral-400);">...</span>
|
||
|
|
}
|
||
|
|
if current > 2 && current < total-1 {
|
||
|
|
<button class="page-btn">{ formatTotal(current - 1) }</button>
|
||
|
|
<button class="page-btn active">{ formatTotal(current) }</button>
|
||
|
|
<button class="page-btn">{ formatTotal(current + 1) }</button>
|
||
|
|
} else if current <= 2 {
|
||
|
|
<button class={ "page-btn", templ.KV("active", current == 2) }>2</button>
|
||
|
|
<button class={ "page-btn", templ.KV("active", current == 3) }>3</button>
|
||
|
|
} else {
|
||
|
|
<button class={ "page-btn", templ.KV("active", current == total-2) }>{ formatTotal(total - 2) }</button>
|
||
|
|
<button class={ "page-btn", templ.KV("active", current == total-1) }>{ formatTotal(total - 1) }</button>
|
||
|
|
}
|
||
|
|
if current < total-2 {
|
||
|
|
<span style="padding: 0 var(--wa-space-xs); color: var(--wa-color-neutral-400);">...</span>
|
||
|
|
}
|
||
|
|
<button class={ "page-btn", templ.KV("active", current == total) }>{ formatTotal(total) }</button>
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
templ TxStyles() {
|
||
|
|
<style>
|
||
|
|
.tx-icon {
|
||
|
|
width: 40px;
|
||
|
|
height: 40px;
|
||
|
|
border-radius: 50%;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
.tx-icon.send { background: var(--wa-color-danger-subtle); color: var(--wa-color-danger); }
|
||
|
|
.tx-icon.receive { background: var(--wa-color-success-subtle); color: var(--wa-color-success); }
|
||
|
|
.tx-icon.swap { background: var(--wa-color-primary-subtle); color: var(--wa-color-primary); }
|
||
|
|
.tx-icon.approve { background: var(--wa-color-neutral-100); color: var(--wa-color-neutral-600); }
|
||
|
|
.tx-icon.contract { background: var(--wa-color-warning-subtle); color: var(--wa-color-warning); }
|
||
|
|
|
||
|
|
.tx-asset {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--wa-space-xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.tx-row {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: auto 1fr auto auto auto auto;
|
||
|
|
gap: var(--wa-space-m);
|
||
|
|
align-items: center;
|
||
|
|
padding: var(--wa-space-m);
|
||
|
|
background: var(--wa-color-surface);
|
||
|
|
border-radius: var(--wa-radius-m);
|
||
|
|
margin-bottom: var(--wa-space-s);
|
||
|
|
transition: box-shadow 0.15s;
|
||
|
|
}
|
||
|
|
.tx-row:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); }
|
||
|
|
|
||
|
|
.tx-details { min-width: 0; }
|
||
|
|
.tx-type-label { font-weight: 500; margin-bottom: 2px; }
|
||
|
|
.tx-address {
|
||
|
|
font-family: var(--wa-font-mono);
|
||
|
|
font-size: var(--wa-font-size-xs);
|
||
|
|
color: var(--wa-color-neutral-500);
|
||
|
|
white-space: nowrap;
|
||
|
|
overflow: hidden;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tx-amount { text-align: right; }
|
||
|
|
.tx-amount .value { font-weight: 500; }
|
||
|
|
.tx-amount .usd { font-size: var(--wa-font-size-xs); color: var(--wa-color-neutral-500); }
|
||
|
|
.tx-amount.positive .value { color: var(--wa-color-success); }
|
||
|
|
.tx-amount.negative .value { color: var(--wa-color-danger); }
|
||
|
|
|
||
|
|
.tx-time { text-align: right; min-width: 80px; }
|
||
|
|
.tx-time .time { font-size: var(--wa-font-size-s); margin-bottom: 2px; }
|
||
|
|
|
||
|
|
.tx-actions { display: flex; gap: var(--wa-space-2xs); }
|
||
|
|
|
||
|
|
.date-group { margin-bottom: var(--wa-space-xl); }
|
||
|
|
.date-group-header {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--wa-space-s);
|
||
|
|
padding: var(--wa-space-s) 0;
|
||
|
|
margin-bottom: var(--wa-space-s);
|
||
|
|
border-bottom: 1px solid var(--wa-color-neutral-200);
|
||
|
|
}
|
||
|
|
.date-group-header h3 {
|
||
|
|
margin: 0;
|
||
|
|
font-size: var(--wa-font-size-s);
|
||
|
|
font-weight: 600;
|
||
|
|
color: var(--wa-color-neutral-700);
|
||
|
|
}
|
||
|
|
.tx-count { font-size: var(--wa-font-size-xs); color: var(--wa-color-neutral-500); }
|
||
|
|
|
||
|
|
.filter-bar {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--wa-space-m);
|
||
|
|
margin-bottom: var(--wa-space-l);
|
||
|
|
flex-wrap: wrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.pagination {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
padding: var(--wa-space-m) 0;
|
||
|
|
margin-top: var(--wa-space-l);
|
||
|
|
border-top: 1px solid var(--wa-color-neutral-200);
|
||
|
|
}
|
||
|
|
.pagination-info { font-size: var(--wa-font-size-s); color: var(--wa-color-neutral-500); }
|
||
|
|
.pagination-controls { display: flex; align-items: center; gap: var(--wa-space-xs); }
|
||
|
|
.page-numbers { display: flex; gap: var(--wa-space-2xs); }
|
||
|
|
.page-btn {
|
||
|
|
min-width: 32px;
|
||
|
|
height: 32px;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
border-radius: var(--wa-radius-s);
|
||
|
|
font-size: var(--wa-font-size-s);
|
||
|
|
cursor: pointer;
|
||
|
|
border: 1px solid var(--wa-color-neutral-200);
|
||
|
|
background: var(--wa-color-surface);
|
||
|
|
color: var(--wa-color-neutral-700);
|
||
|
|
transition: all 0.15s;
|
||
|
|
}
|
||
|
|
.page-btn:hover:not(:disabled) { background: var(--wa-color-surface-alt); }
|
||
|
|
.page-btn.active { background: var(--wa-color-primary); border-color: var(--wa-color-primary); color: white; }
|
||
|
|
.page-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||
|
|
|
||
|
|
@media (max-width: 1100px) {
|
||
|
|
.tx-row { grid-template-columns: auto 1fr auto auto auto; }
|
||
|
|
.tx-asset { display: none; }
|
||
|
|
}
|
||
|
|
@media (max-width: 900px) {
|
||
|
|
.tx-row { grid-template-columns: auto 1fr auto; }
|
||
|
|
.tx-time { display: none; }
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
}
|