refine(wallet): the balance readout scans as one column, not a barcode
Demo images / Build & push demo images (push) Failing after 2m7s
Demo images / Build & push demo images (push) Failing after 2m7s
Seeing it on the node settled the shape. Two rows of per-cell delays read as a dense flicker — closer to a progress bar than a display, and short enough against the row's text to look like an underline. Three rows laid out column-first fixes both: the three cells of a column now share a delay, so the lit column travels across as a single scan line, and at 11px the matrix sits with the text rather than under it. Verified in a real browser against the live node with the balance RPCs held open: five placeholders, five distinct rail colours (white, orange, yellow, purple, blue), 42 cells each, all animating, each announcing what it is waiting for — and no "0 sats" anywhere on screen while the calls were in flight. They gave way to real figures on arrival, with Lightning's genuine 0 correctly shown as a figure rather than left shimmering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
fa6fe32ef9
commit
212e349b19
@@ -196,8 +196,8 @@
|
|||||||
not a loading state, and it is the one number that frightens
|
not a loading state, and it is the one number that frightens
|
||||||
people. It inherits currentColor, so each tile shimmers in its own
|
people. It inherits currentColor, so each tile shimmers in its own
|
||||||
rail colour. */
|
rail colour. */
|
||||||
.bal-pixels { display: inline-grid; grid-template-columns: repeat(14, 4px); grid-auto-rows: 4px; gap: 1px; vertical-align: 0.15em; }
|
.bal-pixels { display: inline-grid; grid-auto-flow: column; grid-template-rows: repeat(3, 4px); grid-auto-columns: 4px; gap: 1px; vertical-align: 0.1em; }
|
||||||
.bal-pixels i { width: 4px; height: 4px; border-radius: 0.5px; background: currentColor; opacity: 0.16; animation: bal-pixel-scan 1.4s ease-in-out infinite; }
|
.bal-pixels i { width: 4px; height: 4px; border-radius: 0.5px; background: currentColor; opacity: 0.16; animation: bal-pixel-scan 1.6s ease-in-out infinite; }
|
||||||
@keyframes bal-pixel-scan { 0%, 70%, 100% { opacity: 0.16; } 25% { opacity: 1; } 45% { opacity: 0.42; } }
|
@keyframes bal-pixel-scan { 0%, 70%, 100% { opacity: 0.16; } 25% { opacity: 1; } 45% { opacity: 0.42; } }
|
||||||
@media (prefers-reduced-motion: reduce) { .bal-pixels i { animation: none; opacity: 0.35; } }
|
@media (prefers-reduced-motion: reduce) { .bal-pixels i { animation: none; opacity: 0.35; } }
|
||||||
.bg-green { background: #4ade80; } .bg-yellow { background: #facc15; } .bg-red { background: #f87171; }
|
.bg-green { background: #4ade80; } .bg-yellow { background: #facc15; } .bg-red { background: #f87171; }
|
||||||
@@ -1083,9 +1083,11 @@
|
|||||||
|
|
||||||
// 28 cells = 14 columns x 2 rows, delays staggered so the lit column
|
// 28 cells = 14 columns x 2 rows, delays staggered so the lit column
|
||||||
// travels across the matrix.
|
// travels across the matrix.
|
||||||
|
// 14 columns x 3 rows, laid out column-first so the three cells of a
|
||||||
|
// column share a delay and the lit column scans across as one line.
|
||||||
const BAL_PIXELS = '<span class="bal-pixels" role="status" aria-label="Loading balance">' +
|
const BAL_PIXELS = '<span class="bal-pixels" role="status" aria-label="Loading balance">' +
|
||||||
Array.from({ length: 28 }, function (_, i) {
|
Array.from({ length: 42 }, function (_, i) {
|
||||||
return '<i style="animation-delay:' + (i * 45) + 'ms"></i>';
|
return '<i style="animation-delay:' + (Math.floor(i / 3) * 55) + 'ms"></i>';
|
||||||
}).join('') + '</span>';
|
}).join('') + '</span>';
|
||||||
|
|
||||||
// Render a balance, or the pixel readout when it is not known yet.
|
// Render a balance, or the pixel readout when it is not known yet.
|
||||||
|
|||||||
@@ -34,9 +34,18 @@ const props = withDefaults(
|
|||||||
{ suffix: 'sats', label: 'balance' },
|
{ suffix: 'sats', label: 'balance' },
|
||||||
)
|
)
|
||||||
|
|
||||||
// 14 columns × 2 rows. Enough to read as a matrix rather than a spinner, and
|
// 14 columns × 3 rows, laid out column-first so all three cells of a column
|
||||||
// close to the width of a five-figure sat amount.
|
// share a delay and the lit column travels across as a single scan line —
|
||||||
const CELLS = 28
|
// that is what makes it read as a readout rather than a progress bar.
|
||||||
|
const COLUMNS = 14
|
||||||
|
const ROWS = 3
|
||||||
|
const CELLS = COLUMNS * ROWS
|
||||||
|
const STEP_MS = 55
|
||||||
|
|
||||||
|
/** Delay for cell `i` (1-based), constant within a column. */
|
||||||
|
function cellDelay(i: number): string {
|
||||||
|
return `${Math.floor((i - 1) / ROWS) * STEP_MS}ms`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Built as one string rather than interpolated around a `<template>`, so the
|
* Built as one string rather than interpolated around a `<template>`, so the
|
||||||
@@ -59,7 +68,7 @@ const display = computed(() => {
|
|||||||
:aria-label="`Loading ${props.label}`"
|
:aria-label="`Loading ${props.label}`"
|
||||||
:title="`Loading ${props.label}…`"
|
:title="`Loading ${props.label}…`"
|
||||||
>
|
>
|
||||||
<span v-for="i in CELLS" :key="i" class="balance-pixel" :style="{ '--i': i }" />
|
<span v-for="i in CELLS" :key="i" class="balance-pixel" :style="{ animationDelay: cellDelay(i) }" />
|
||||||
</span>
|
</span>
|
||||||
<span v-else>{{ display }}</span>
|
<span v-else>{{ display }}</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -67,12 +76,15 @@ const display = computed(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.balance-pixels {
|
.balance-pixels {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
grid-template-columns: repeat(14, 3px);
|
/* Column-first: children fill top-to-bottom, then across, so consecutive
|
||||||
grid-auto-rows: 3px;
|
cells share a column and the delay below scans horizontally. */
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-template-rows: repeat(3, 3px);
|
||||||
|
grid-auto-columns: 3px;
|
||||||
gap: 1px;
|
gap: 1px;
|
||||||
/* Sit on the text baseline so a row's height doesn't change when the real
|
/* Centred on the text it stands in for, so the row height is unchanged when
|
||||||
figure replaces this. */
|
the real figure replaces it. */
|
||||||
vertical-align: -1px;
|
vertical-align: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.balance-pixel {
|
.balance-pixel {
|
||||||
@@ -81,10 +93,7 @@ const display = computed(() => {
|
|||||||
border-radius: 0.5px;
|
border-radius: 0.5px;
|
||||||
background: currentColor;
|
background: currentColor;
|
||||||
opacity: 0.16;
|
opacity: 0.16;
|
||||||
/* The wave runs left-to-right across columns; the two rows of a column are
|
animation: balance-pixel-scan 1.6s ease-in-out infinite;
|
||||||
offset slightly so it reads as a scan rather than a marching block. */
|
|
||||||
animation: balance-pixel-scan 1.4s ease-in-out infinite;
|
|
||||||
animation-delay: calc(var(--i) * 45ms);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes balance-pixel-scan {
|
@keyframes balance-pixel-scan {
|
||||||
|
|||||||
@@ -59,9 +59,17 @@ describe('BalanceAmount', () => {
|
|||||||
expect(w.html()).not.toMatch(/background:\s*#|rgb\(/)
|
expect(w.html()).not.toMatch(/background:\s*#|rgb\(/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders enough cells to read as a matrix', () => {
|
it('renders a 14x3 matrix scanned column by column', () => {
|
||||||
|
// Column-first layout is what makes the lit column travel across as one
|
||||||
|
// scan line; per-cell delays would make it crawl diagonally instead.
|
||||||
const w = mount(BalanceAmount, { props: { sats: null } })
|
const w = mount(BalanceAmount, { props: { sats: null } })
|
||||||
expect(w.findAll('.balance-pixel').length).toBe(28)
|
const cells = w.findAll('.balance-pixel')
|
||||||
|
expect(cells.length).toBe(42)
|
||||||
|
// The three cells of a column share a delay; the next column steps on.
|
||||||
|
const delay = (i: number) => cells[i]?.attributes('style') ?? ''
|
||||||
|
expect(delay(0)).toBe(delay(1))
|
||||||
|
expect(delay(1)).toBe(delay(2))
|
||||||
|
expect(delay(3)).not.toBe(delay(2))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user