diff --git a/client/src/views/block-details-card.js b/client/src/components/block-details-card.js similarity index 66% rename from client/src/views/block-details-card.js rename to client/src/components/block-details-card.js index 34f1a4a1..af2a6671 100644 --- a/client/src/views/block-details-card.js +++ b/client/src/components/block-details-card.js @@ -1,17 +1,25 @@ -import { BlockGrid } from "../components/block-grid"; -import { InfoStat } from "../components/info-stat"; -import { StatusBadge } from "../components/status-badge"; -import { ElapsedTime } from "../components/elapsed-time"; +import { BlockGrid } from "./block-grid"; +import { InfoStat } from "./info-stat"; +import { StatusBadge } from "./status-badge"; +import { ElapsedTime } from "./elapsed-time"; +import { Tooltip } from "./tooltip"; import { formatTime, formatVMB, getBlockPercentageUsed, -} from "./util"; +} from "../views/util"; + +const staticRoot = process.env.STATIC_ROOT || ""; const formatInteger = (value) => Number.isFinite(value) ? value.toLocaleString() : "N/A"; -const BlockDetailsCard = ({ className, block, confirmed }) => { +const BlockDetailsCard = ({ + className, + block, + statusText, + statusVariant = "success", +}) => { const percentage = block ? Math.min(Math.max(getBlockPercentageUsed(block.weight), 0), 100) : 0; @@ -38,16 +46,14 @@ const BlockDetailsCard = ({ className, block, confirmed }) => { title={block ? formatTime(block.timestamp) : ""} > {block ? ( - - ago - + ) : ( "Loading block..." )}

- {confirmed ? ( - Confirmed + {statusText ? ( + {statusText} ) : null} @@ -60,11 +66,25 @@ const BlockDetailsCard = ({ className, block, confirmed }) => { title="SIZE" value={block ? formatVMB(block.size, "MB") : "N/A"} /> +
-

BLOCK FILLING

+
+

BLOCK FILLING

+ +

{block ? `${percentage}%` : "N/A"}

diff --git a/client/src/components/block-signatures.js b/client/src/components/block-signatures.js new file mode 100644 index 00000000..8995c947 --- /dev/null +++ b/client/src/components/block-signatures.js @@ -0,0 +1,37 @@ +import { parseBlockSignatures } from "../lib/block-signatures"; + +let lastWarningKey; + +const warnOnce = (block, error) => { + if (!process.browser) return; + + const source = error.source || "challenge"; + const warningKey = `${block && block.id}:${source}`; + if (warningKey === lastWarningKey) return; + + lastWarningKey = warningKey; + console.warn(`Failed to parse ${source} script`, error); +}; + +const BlockSignatures = ({ block }) => { + const result = parseBlockSignatures(block); + + if (result && result.error) { + warnOnce(block, result.error); + } + + const signatures = result && !result.error ? result : null; + + return signatures ? ( +
+

{`${signatures.signatureCount} of ${signatures.totalSigners}`}

+

+ {`${signatures.requiredSignatures} required`} +

+
+ ) : ( +

N/A

+ ); +}; + +export default BlockSignatures; diff --git a/client/src/components/elapsed-time.js b/client/src/components/elapsed-time.js index 407fca9e..df6a0df7 100644 --- a/client/src/components/elapsed-time.js +++ b/client/src/components/elapsed-time.js @@ -3,7 +3,7 @@ const MINUTES_PER_DAY = 24 * 60; const MINUTES_PER_YEAR = 365 * MINUTES_PER_DAY; const MINUTES_PER_MONTH = MINUTES_PER_YEAR / 12; -const formatElapsedTime = (timestamp) => { +const formatElapsedTime = (timestamp, compact) => { const fromDate = timestamp < 1e12 ? new Date(timestamp * 1000) : new Date(timestamp); const diffMinutes = Math.max( @@ -17,26 +17,41 @@ const formatElapsedTime = (timestamp) => { minutesAfterYears - months * MINUTES_PER_MONTH, ); const units = [ - ["y", years], - ["mo", months], - ["d", Math.floor(minutesAfterMonths / MINUTES_PER_DAY)], - ["h", Math.floor((minutesAfterMonths % MINUTES_PER_DAY) / 60)], - ["m", minutesAfterMonths % 60], + ["YEAR", "YEARS", "y", years], + ["MONTH", "MONTHS", "mo", months], + ["DAY", "DAYS", "d", Math.floor(minutesAfterMonths / MINUTES_PER_DAY)], + [ + "HOUR", + "HOURS", + "h", + Math.floor((minutesAfterMonths % MINUTES_PER_DAY) / 60), + ], + ["MINUTE", "MINUTES", "m", minutesAfterMonths % 60], ]; const parts = units - .filter(([_, value]) => value > 0) + .filter((unit) => unit[3] > 0) .slice(0, 2) - .map(([unit, value]) => `${value}${unit}`); + .map(([singular, plural, abbreviation, value]) => + compact + ? `${value}${abbreviation}` + : `${value} ${value === 1 ? singular : plural}`, + ); - return parts.length ? parts.join(" ") : "< 1m"; + if (compact) return parts.length ? parts.join(" ") : "< 1m"; + + return parts.length ? `${parts.join(" ")} AGO` : "< 1 MINUTE AGO"; }; const updateElapsedTime = (element) => { - element.textContent = formatElapsedTime(element.elapsedTimeTimestamp); + element.textContent = formatElapsedTime( + element.elapsedTimeTimestamp, + element.elapsedTimeCompact, + ); }; -const startElapsedTime = (vnode, timestamp) => { +const startElapsedTime = (vnode, timestamp, compact) => { vnode.elm.elapsedTimeTimestamp = timestamp; + vnode.elm.elapsedTimeCompact = compact; updateElapsedTime(vnode.elm); vnode.elm.elapsedTimeInterval = window.setInterval( () => updateElapsedTime(vnode.elm), @@ -44,8 +59,9 @@ const startElapsedTime = (vnode, timestamp) => { ); }; -const patchElapsedTime = (_, vnode, timestamp) => { +const patchElapsedTime = (_, vnode, timestamp, compact) => { vnode.elm.elapsedTimeTimestamp = timestamp; + vnode.elm.elapsedTimeCompact = compact; updateElapsedTime(vnode.elm); }; @@ -53,14 +69,14 @@ const stopElapsedTime = (vnode) => { window.clearInterval(vnode.elm.elapsedTimeInterval); }; -export const ElapsedTime = ({ timestamp } = {}) => ( +export const ElapsedTime = ({ timestamp, compact = false } = {}) => ( startElapsedTime(vnode, timestamp)} + hook-insert={(vnode) => startElapsedTime(vnode, timestamp, compact)} hook-postpatch={(oldVnode, vnode) => - patchElapsedTime(oldVnode, vnode, timestamp) + patchElapsedTime(oldVnode, vnode, timestamp, compact) } hook-destroy={stopElapsedTime} > - {formatElapsedTime(timestamp)} + {formatElapsedTime(timestamp, compact)} ); diff --git a/client/src/components/icons.js b/client/src/components/icons.js index d5486491..2589c034 100644 --- a/client/src/components/icons.js +++ b/client/src/components/icons.js @@ -59,6 +59,18 @@ export const ChevronRightIcon = ({ className } = {}) => +export const CaretCircleLeftIcon = ({ className } = {}) => + + +export const CaretCircleRightIcon = ({ className } = {}) => + + export const ChevronDownIcon = ({ className } = {}) =>
-
-
-

{t`Block ${b.height}`}

-
{b.id} - { process.browser &&
-
-
} -
-
-
- { b.previousblockhash && - -
-
-
{t`Previous`}
-
-
- } +import layout from "./layout"; +import { txBox } from "./tx"; +import { formatHex } from "./util"; +import loader from "../components/loading"; +import { + BlockIcon, + CaretCircleLeftIcon, + CaretCircleRightIcon, +} from "../components/icons"; +import BlockDetailsCard from "../components/block-details-card"; + +// Require behind env conditional so it gets removed by `envify` on non-elements builds +const BlockSignatures = + process.env.IS_ELEMENTS && + require("../components/block-signatures").default; + +const staticRoot = process.env.STATIC_ROOT || ""; + +const makeStatus = (b) => + b && { confirmed: true, block_height: b.height, block_hash: b.id }; + +export default ({ + t, + block: b, + blockStatus: status, + blockTxs, + openTx, + spends, + goBlock, + tipHeight, + loading, + page, + txsStatus = makeStatus(b), + ...S +}) => + b && + layout( + [ +
+
+
+
+
-
- { (status && status.next_best) && - -
-
{t`Next`}
-
-
+
+ {b.previousblockhash && ( + + + + )} +

{b.height}

+ + {status && status.next_best && ( + + - } + )}
-
-
-
, -
- {btnDetails(b.id, openBlock == b.id, page.query, t)} - -
-
-
{t`Height`}
- -
-
-
{t`Status`}
-
{!status ? '' : !status.in_best_chain ? t`Orphaned` : tipHeight ? t`In best chain (${tipHeight-b.height+1} confirmations)` : t`In best chain`}
-
-
-
{t`Timestamp`}
-
{formatTime(b.timestamp)}
-
-
-
{t`Size`}
-
{`${formatNumber(b.size/1000)} KB`}
-
-
-
{t`Virtual size`}
-
{`${Math.ceil(b.weight/4/1000)} vKB`}
-
-
-
{t`Weight units`}
-
{`${formatNumber(b.weight/1000)} KWU`}
+
- { /* advanced details */ } - { openBlock == b.id && [ +
+
+
+
+

VERSION

+
+

{formatHex(b.version)}

+
+
-
-
{t`Version`}
-
{formatHex(b.version)}
-
- ,
-
{t`Merkle root`}
-
{b.merkle_root}
+
+

BLOCK HASH

+
+

{b.id}

+
+
- /* PoW chains */ - , b.bits ? [ -
-
{t`Bits`}
-
{formatHex(b.bits)}
-
- ,
-
{t`Difficulty`}
-
{formatNumber(b.difficulty)}
-
- ,
-
{t`Nonce`}
-
{formatHex(b.nonce)}
+
+
+

MERKLE ROOT

+
+

{b.merkle_root}

+
- ] - /* Federated chains */ - /* TODO: support for dynafed blocks */ - : b.ext && b.ext.challenge ? [ -
-
{t`Block Challenge`}
-
{b.ext.challenge}
+
+

+ {process.env.IS_ELEMENTS ? "BLOCK SIGNATURES" : "NONCE"} +

+
+ {process.env.IS_ELEMENTS ? ( + + ) : ( +

{b.nonce}

+ )} +
- ,
-
{t`Block Solution`}
-
{b.ext.solution}
-
- ] - : null - ] } - - -
+
+
+
-
- { blockTxs ? blockTxs.map((tx, index) => txBox( - { ...tx, status: txsStatus }, - { - openTx, - tipHeight, - t, - spends, - listingIndex: +goBlock.start_index + index + 1, - listingTotal: b.tx_count - } - )) - : loader() } -
+
+ {blockTxs + ? blockTxs.map((tx, index) => + txBox( + { ...tx, status: txsStatus }, + { + openTx, + tipHeight, + t, + spends, + listingIndex: +goBlock.start_index + index + 1, + listingTotal: b.tx_count, + }, + ), + ) + : loader()} +
-
-
- { loading ?
{t`Loading...`}
{loader("small")}
- : pagingNav(b, { ...S, t }) } +
+
+ {loading ? ( +
+ {t`Loading...`} +
{loader("small")}
+
+ ) : ( + pagingNav(b, { ...S, t }) + )} +
-
-
- ] -, { t, page, activeTab: 'recentBlocks', ...S }) +
, + ], + { t, page, activeTab: "recentBlocks", ...S }, + ); const pagingNav = (block, { nextBlockTxs, prevBlockTxs, t }) => process.browser - -? nextBlockTxs && -
- {t`Load more`} -
- -: [ - prevBlockTxs != null && - -
- {t`Prev`} -
- , nextBlockTxs != null && - - {t`Next`} -
-
- ] - -const btnDetails = (blockhash, isOpen, query, t) => process.browser - // dynamic button in browser env - ?
{btnDetailsContent(isOpen, t)}
- // or a plain link in server-side rendered env - : {btnDetailsContent(isOpen, t)} - -const btnDetailsContent = (isOpen, t) => -
-

{t`Details`}

- {isOpen ? : } -
+ ? nextBlockTxs && ( +
+ {t`Load more`} +
+ ) + : [ + prevBlockTxs != null && ( + +
+ +
+ {t`Prev`} +
+ ), + nextBlockTxs != null && ( + + {t`Next`} +
+ +
+
+ ), + ]; diff --git a/client/src/views/overview.js b/client/src/views/overview.js index dc5a353a..98752bb2 100644 --- a/client/src/views/overview.js +++ b/client/src/views/overview.js @@ -107,7 +107,11 @@ export const overview = ({ text: "Elapsed time since the last block confirmed. Bitcoin targets one every ~10 minutes.", }} value={ - latestBlock ? : "" + latestBlock ? ( + + ) : ( + "" + ) } footer={ latestBlock ? `BLOCK #${latestBlock.height.toLocaleString()}` : "" diff --git a/client/src/views/tx.js b/client/src/views/tx.js index c597d230..44090a17 100644 --- a/client/src/views/tx.js +++ b/client/src/views/tx.js @@ -21,7 +21,7 @@ import { } from "../components/icons"; import { InfoStat } from "../components/info-stat"; import { StatusBadge } from "../components/status-badge"; -import BlockDetailsCard from "./block-details-card"; +import BlockDetailsCard from "../components/block-details-card"; // Require behind env conditional so it gets removed by `envify` on non-elements builds const deduceBlinded = @@ -61,7 +61,7 @@ export default ({ return layout( [ -
+
{txHeader(tx, { t, tipHeight, ...S })} {unblinded && unblinded.error ? (
@@ -91,7 +91,7 @@ export default ({
) : null} @@ -292,19 +292,19 @@ const txHeader = ( ) : null}
-
-
+
+
{isConfirmed ? ( -
-

CONFIRMATION TIME

-
+
+

CONFIRMATION TIME

+

{confirmationTime}

) : ( -
-

ETA

-
+
+

ETA

+

{etaLabel}

@@ -327,16 +327,16 @@ const txHeader = (
)} -
-

TRANSACTION FEES

-
+
+

TRANSACTION FEES

+
{feerate != null ? (
-

+

{formatSat(tx.fee)}

-

+

{`(${feerate.toFixed(2)} sat/vB)`}

@@ -360,17 +360,17 @@ const txHeader = (
-
-
-

PRIVACY ANALYSIS

-
+
+
+

PRIVACY ANALYSIS

+
{privacyAnalysisView(privacyAnalysis, t)}
-
-

SEGWIT FEE SAVINGS

-
+
+

SEGWIT FEE SAVINGS

+
{segwitSavings || "N/A"}
diff --git a/flavors/liquid/extras.css b/flavors/liquid/extras.css index 4aa4ce5d..4ff4b947 100644 --- a/flavors/liquid/extras.css +++ b/flavors/liquid/extras.css @@ -1,5 +1,6 @@ :root { - --accent-color: #22E1C9; + --accent-color-rgb: 34, 225, 201; + --accent-color: rgb(var(--accent-color-rgb)); --background-color: #0E012D; --surface-primary-color: #0C1537; --surface-secondary-color: #172146; diff --git a/www/style.css b/www/style.css index a5950ecc..10c6b607 100644 --- a/www/style.css +++ b/www/style.css @@ -117,15 +117,21 @@ html { } :root { - --accent-color: #FA8A00; + --blockstream-primary-brand-color-rgb: 0, 195, 255; + --blockstream-primary-brand-color: rgb(var(--blockstream-primary-brand-color-rgb)); + --accent-color-rgb: 250, 138, 0; + --accent-color: rgb(var(--accent-color-rgb)); --background-color: #181818; --surface-primary-color: #1C1C1C; --surface-primary-alt-color: #26221D; --surface-secondary-color: #262626; --page-gap: 24px; - --success-color: #17C964; - --warning-color: #E2BA1B; - --danger-color: #DB3B3E; + --success-color-rgb: 23, 201, 100; + --success-color: rgb(var(--success-color-rgb)); + --warning-color-rgb: 226, 186, 27; + --warning-color: rgb(var(--warning-color-rgb)); + --danger-color-rgb: 219, 59, 62; + --danger-color: rgb(var(--danger-color-rgb)); --foreground-link-color: #00C3FF; --foreground-muted: #B5BDC2; --radius-rounded-xl: 12px; @@ -1201,9 +1207,16 @@ table th { } .transactions { + display: flex; + flex-direction: column; + gap: var(--page-gap); margin-top: 84px; } +.block-page-container > .transactions { + margin-top: 0; +} + .transactions > h3, .transactions > img { display: inline-block; } @@ -1221,7 +1234,6 @@ table th { background-color: var(--surface-primary-color); padding: 24px; border-radius: 5px; - margin-top: 24px; gap: 24px; } @@ -2114,6 +2126,10 @@ body::after{ } @media only screen and (max-width: 500px) { + .explorer-container > .footer { + height: auto; + } + .footer_container_content_row_onion_container { margin-top: 10px; } @@ -3671,7 +3687,6 @@ a.back-link img{ } .transaction-table { - margin-top: var(--page-gap); background-color: var(--surface-primary-color); padding: 24px; border-radius: var(--radius-rounded-xl); @@ -3861,7 +3876,7 @@ a.back-link img{ } .status-badge.warning { - background-color: rgb(from var(--warning-color) r g b / 20%); + background-color: rgba(var(--warning-color-rgb), .2); color: var(--warning-color); } @@ -3902,7 +3917,7 @@ a.back-link img{ } .status-badge.success { - background-color: rgb(from var(--success-color) r g b / 20%); + background-color: rgba(var(--success-color-rgb), .2); color: var(--success-color); } @@ -3926,7 +3941,7 @@ a.back-link img{ border-radius: 9999px; } -.transaction-details { +.detail-grid { display: flex; flex-direction: column; gap: 12px; @@ -4148,12 +4163,12 @@ a.back-link img{ cursor: pointer; } -.transaction-details-row { +.detail-grid-row { display: flex; gap: 12px; } -.tx-overview-panel { +.detail-field { display: flex; align-items: center; padding: 12px; @@ -4163,20 +4178,33 @@ a.back-link img{ gap: 24px; } -.tx-overview-panel-label { +.detail-field-label { font-size: 10px; color: #B5BDC2; text-transform: capitalize; white-space: nowrap; } -.tx-overview-panel-details { +.detail-field-content { display: flex; margin-left: auto; text-align: right; gap: 12px; align-items: center; font-size: 14px; + flex-wrap: wrap; + line-break: anywhere; +} + +.block-signatures { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.block-signatures-required { + color: var(--foreground-muted); + font-size: 10px; } .eta-blocks { @@ -4195,12 +4223,12 @@ a.back-link img{ align-items: center; } -.tx-overview-panel-fee { +.transaction-fee-value { font-size: 14px; font-weight: 600; } -.tx-overview-panel-fee-p-vb { +.transaction-fee-rate { color: var(--foreground-muted); font-size: 11px; font-weight: 400; @@ -4211,7 +4239,6 @@ a.back-link img{ display: flex; flex-direction: column; width: 100%; - margin-top: 24px; padding: 24px; background-color: var(--surface-primary-color); border-radius: 12px; @@ -4231,7 +4258,7 @@ a.back-link img{ --block-grid-filled-color: var(--accent-color); flex: 0 0 auto; padding: 10px; - border: 2px solid rgb(from var(--accent-color) r g b / 0.2); + border: 2px solid rgba(var(--accent-color-rgb), .2); border-radius: 4px; background-color: var(--surface-primary-color); width: fit-content; @@ -4264,6 +4291,7 @@ a.back-link img{ color: var(--foreground-muted); font-size: 14px; font-weight: 400; + font-size: 10px; } .block-details-card-summary { @@ -4282,6 +4310,11 @@ a.back-link img{ font-size: 11px; } +.block-details-card-progress-title { + display: flex; + align-items: center; +} + .block-details-card-progress-header .usage-number { color: #fff; } @@ -4310,6 +4343,44 @@ a.back-link img{ margin-top: 12px; } +.block-box { + background-color: var(--surface-primary-color); + border-radius: var(--radius-rounded-xl); + padding: 24px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.block-page-container, .tx-page-container { + display: flex; + flex-direction: column; + gap: var(--page-gap); + margin-top: var(--page-gap); +} + +.block-navigation { + display: flex; + align-items: center; + margin-left: 15px; + gap: 0; +} + +.block-navigation a { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 44px; + min-height: 44px; +} + +.block-navigation-block-number { + font-size: 28px; + font-weight: bold; + font-family: 'Rigid Square'; + color: var(--blockstream-primary-brand-color); +} + @media only screen and (max-width: 820px) { .block-details-card-summary { flex-direction: column; @@ -4325,7 +4396,7 @@ a.back-link img{ } } -@media only screen and (max-width: 1100px) { +@media only screen and (max-width: 1328px) { .explorer-container { box-sizing: border-box; } @@ -4629,7 +4700,7 @@ a.back-link img{ line-break: anywhere; } - .transaction-details-row { + .detail-grid-row { flex-direction: column; } @@ -4638,7 +4709,7 @@ a.back-link img{ } } -@media only screen and (max-width: 1100px) and (prefers-reduced-motion: reduce) { +@media only screen and (max-width: 1328px) and (prefers-reduced-motion: reduce) { .tooltip-dialogue { animation: none; }