improve cluster diagram mobile UX

This commit is contained in:
mononaut 2026-05-14 07:25:16 +00:00
parent 95eb1372f4
commit a4b46c2b48
No known key found for this signature in database
GPG key ID: BFD16BE592A9CD8D
6 changed files with 142 additions and 28 deletions

View file

@ -48,10 +48,12 @@
[class.inactive]="outline.chunkIndex !== effectiveActiveChunk"
[attr.d]="outline.path"
[ngbTooltip]="chunkLabelTooltip"
[disableTooltip]="isMobile"
container="body"
placement="top"
(mouseenter)="onChunkEnter(outline.chunkIndex)"
(mouseleave)="onChunkLeave()" />
(mouseleave)="onChunkLeave()"
(click)="onChunkClick(outline.chunkIndex, $event)" />
<text *ngFor="let outline of chunkOutlines; trackBy: trackByChunkIndex"
class="chunk-label"
@ -59,10 +61,12 @@
[attr.x]="outline.labelX" [attr.y]="outline.labelY"
text-anchor="middle"
[ngbTooltip]="chunkLabelTooltip"
[disableTooltip]="isMobile"
container="body"
placement="top"
(mouseenter)="onChunkEnter(outline.chunkIndex)"
(mouseleave)="onChunkLeave()">
(mouseleave)="onChunkLeave()"
(click)="onChunkClick(outline.chunkIndex, $event)">
{{ outline.feerate | feeRounding }} sat/vB
</text>
</ng-container>
@ -76,7 +80,8 @@
stroke-width="12"
(mouseenter)="onEdgeEnter(i, $event)"
(mousemove)="onEdgeMove($event)"
(mouseleave)="onEdgeLeave()" />
(mouseleave)="onEdgeLeave()"
(click)="onEdgeClick(i, $event)" />
<path class="edge-line"
[class.highlighted]="edge.highlighted"
[class.ancestor]="edge.highlightKind === 'ancestor'"
@ -96,7 +101,7 @@
(mouseenter)="onNodeEnter(node, $event)"
(mousemove)="onNodeMove($event)"
(mouseleave)="onNodeLeave()"
(click)="onNodeClick(node)">
(click)="onNodeClick(node, $event)">
<rect class="node-rect"
[class.current]="node.isCurrent"
[class.hovered]="node.hovered"
@ -120,7 +125,7 @@
<div #tooltip
class="cluster-tooltip"
*ngIf="hoverNode || hoverEdge"
*ngIf="!isMobile && (hoverNode || hoverEdge)"
[style.left.px]="tooltipPosition.x"
[style.top.px]="tooltipPosition.y">
<ng-container *ngIf="hoverNode">
@ -154,3 +159,39 @@
<div><span class="swatch descendant"></span><span i18n="cluster.child|child">child</span></div>
</div>
</div>
<div
class="cluster-mobile-panel"
*ngIf="isMobile && (hoverNode || hoverEdge)"
(click)="$event.stopPropagation()">
<ng-container *ngIf="hoverNode">
<div class="tx-id-row">
<a [routerLink]="['/tx/' | relativeUrl, hoverNode.tx.txid]">{{ hoverNode.tx.txid | shortenString }}</a>
<span class="this-tx-badge" *ngIf="hoverNode.isCurrent" i18n="cluster.this-transaction|this transaction">this transaction</span>
</div>
<div class="tooltip-body">
<table class="stats">
<tr>
<th i18n="transaction.fee|Transaction fee">Fee</th>
<td>{{ hoverNode.tx.fee | number }} <span class="unit" i18n="shared.sats">sats</span></td>
</tr>
<tr>
<th i18n="transaction.vsize|Virtual size">Size</th>
<td><span [innerHTML]="hoverNode.tx.weight / 4 | vbytes: 2"></span></td>
</tr>
<tr>
<th i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
<td>{{ hoverNode.feerate | feeRounding }} <span class="unit">sat/vB</span></td>
</tr>
</table>
<div class="legend">
<div><span class="swatch ancestor"></span><span i18n="cluster.parents|parents">parents</span></div>
<div><span class="swatch descendant"></span><span i18n="cluster.children|children">children</span></div>
</div>
</div>
</ng-container>
<div class="legend edge-legend" *ngIf="!hoverNode && hoverEdge">
<div><span class="swatch ancestor"></span><span i18n="cluster.parent|parent">parent</span></div>
<div><span class="swatch descendant"></span><span i18n="cluster.child|child">child</span></div>
</div>
</div>

View file

@ -144,17 +144,14 @@
font-weight: 700;
}
.cluster-tooltip {
position: absolute;
.cluster-tooltip,
.cluster-mobile-panel {
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.5);
color: var(--tooltip-grey);
padding: 8px 12px;
text-align: left;
pointer-events: none;
max-width: 360px;
white-space: nowrap;
.tx-id-row {
display: flex;
@ -225,3 +222,15 @@
&.descendant { background: var(--cluster-descendant-color); }
}
}
.cluster-tooltip {
position: absolute;
pointer-events: none;
max-width: 360px;
white-space: nowrap;
}
.cluster-mobile-panel {
margin-top: 8px;
white-space: normal;
}

View file

@ -22,6 +22,7 @@ export class ClusterDiagramComponent implements OnChanges, AfterViewInit, OnDest
@Input() cluster: { txs: CpfpClusterTx[]; chunks: CpfpClusterChunk[]; chunkIndex: number };
@Input() txid: string;
@Input() preview = false;
@Input() isMobile = false;
@ViewChild('graphContainer', { static: true }) graphContainer: ElementRef;
@ViewChild('tooltip') tooltipElement: ElementRef;
@ -125,8 +126,17 @@ export class ClusterDiagramComponent implements OnChanges, AfterViewInit, OnDest
}
onNodeEnter(node: RenderedNode, event: MouseEvent): void {
if (this.preview) { return; }
if (this.preview || this.isMobile) { return; }
this.applyNodeHighlight(node);
this.updateTooltipPosition(event);
this.cd.markForCheck();
}
private applyNodeHighlight(node: RenderedNode): void {
this.hoverNode = node;
this.hoverEdge = null;
this.hoverChunkIndex = null;
this.applyEffectiveChunk();
this.clearHighlights();
node.hovered = true;
for (const edge of this.edges) {
@ -140,58 +150,63 @@ export class ClusterDiagramComponent implements OnChanges, AfterViewInit, OnDest
edge.highlightKind = 'ancestor';
}
}
this.updateTooltipPosition(event);
this.cd.markForCheck();
}
onNodeMove(event: MouseEvent): void {
if (this.preview) { return; }
if (this.preview || this.isMobile) { return; }
this.updateTooltipPosition(event);
this.cd.markForCheck();
}
onNodeLeave(): void {
if (this.preview) { return; }
if (this.preview || this.isMobile) { return; }
this.hoverNode = null;
this.clearHighlights();
this.cd.markForCheck();
}
onEdgeEnter(edgeIndex: number, event: MouseEvent): void {
if (this.preview) { return; }
if (this.preview || this.isMobile) { return; }
this.applyEdgeHighlight(edgeIndex);
this.updateTooltipPosition(event);
this.cd.markForCheck();
}
private applyEdgeHighlight(edgeIndex: number): void {
this.clearHighlights();
this.hoverNode = null;
this.hoverChunkIndex = null;
this.applyEffectiveChunk();
const edge = this.edges[edgeIndex];
edge.highlighted = true;
edge.highlightKind = 'direct';
this.nodes[edge.parentIndex].relation = 'ancestor';
this.nodes[edge.childIndex].relation = 'descendant';
this.hoverEdge = edge;
this.updateTooltipPosition(event);
this.cd.markForCheck();
}
onEdgeMove(event: MouseEvent): void {
if (this.preview) { return; }
if (this.preview || this.isMobile) { return; }
this.updateTooltipPosition(event);
this.cd.markForCheck();
}
onEdgeLeave(): void {
if (this.preview) { return; }
if (this.preview || this.isMobile) { return; }
this.hoverEdge = null;
this.clearHighlights();
this.cd.markForCheck();
}
onChunkEnter(chunkIndex: number): void {
if (this.preview) { return; }
if (this.preview || this.isMobile) { return; }
this.hoverChunkIndex = chunkIndex;
this.applyEffectiveChunk();
this.cd.markForCheck();
}
onChunkLeave(): void {
if (this.preview) { return; }
if (this.preview || this.isMobile) { return; }
this.hoverChunkIndex = null;
this.applyEffectiveChunk();
this.cd.markForCheck();
@ -212,13 +227,61 @@ export class ClusterDiagramComponent implements OnChanges, AfterViewInit, OnDest
}
}
onNodeClick(node: RenderedNode): void {
onNodeClick(node: RenderedNode, event: MouseEvent): void {
if (this.preview) { return; }
if (this.isMobile) {
event.stopPropagation();
if (this.hoverNode?.index === node.index) {
this.clearMobileSelection();
} else {
this.applyNodeHighlight(node);
this.cd.markForCheck();
}
return;
}
const network = this.stateService.network;
const prefix = network && network !== 'mainnet' ? `/${network}` : '';
this.router.navigate([prefix + '/tx/', node.tx.txid]);
}
onEdgeClick(edgeIndex: number, event: MouseEvent): void {
if (this.preview || !this.isMobile) { return; }
event.stopPropagation();
const edge = this.edges[edgeIndex];
if (this.hoverEdge === edge) {
this.clearMobileSelection();
} else {
this.applyEdgeHighlight(edgeIndex);
this.cd.markForCheck();
}
}
onChunkClick(chunkIndex: number, event: MouseEvent): void {
if (this.preview || !this.isMobile) { return; }
event.stopPropagation();
this.hoverNode = null;
this.hoverEdge = null;
this.clearHighlights();
this.hoverChunkIndex = chunkIndex;
this.applyEffectiveChunk();
this.cd.markForCheck();
}
@HostListener('click')
onBackgroundClick(): void {
if (this.preview || !this.isMobile) { return; }
this.clearMobileSelection();
}
private clearMobileSelection(): void {
this.hoverNode = null;
this.hoverEdge = null;
this.hoverChunkIndex = null;
this.clearHighlights();
this.applyEffectiveChunk();
this.cd.markForCheck();
}
private clearHighlights(): void {
for (const node of this.nodes) {
node.hovered = false;

View file

@ -248,13 +248,14 @@
<ng-template #clusterPreviewButton let-cluster="cluster" let-txid="txid">
<div class="cluster-preview-inline" role="button" tabindex="0"
[ngbTooltip]="clusterPreviewTooltip"
[disableTooltip]="isMobile"
placement="bottom"
tooltipClass="cluster-preview-ngb-tooltip"
container="body"
(click)="toggleCpfp()"
(keydown.enter)="toggleCpfp()"
(keydown.space)="toggleCpfp(); $event.preventDefault()">
<app-cluster-diagram class="cluster-preview-diagram" [cluster]="cluster" [txid]="txid" [preview]="true"></app-cluster-diagram>
<app-cluster-diagram class="cluster-preview-diagram" [cluster]="cluster" [txid]="txid" [preview]="true" [isMobile]="isMobile"></app-cluster-diagram>
<fa-icon class="cluster-preview-expand" [icon]="['fas', cpfpMode ? 'compress' : 'expand']" [fixedWidth]="true"></fa-icon>
</div>
<ng-template #clusterPreviewTooltip>

View file

@ -104,7 +104,7 @@
<ng-container *ngIf="cpfpInfo?.cluster; else legacyCpfpRaw">
<div class="box">
<app-cluster-diagram
[cluster]="cpfpInfo.cluster" [txid]="transaction.txid">
[cluster]="cpfpInfo.cluster" [txid]="transaction.txid" [isMobile]="isMobile">
</app-cluster-diagram>
</div>
</ng-container>
@ -248,4 +248,4 @@
</h3>
</div>
}
</div>
</div>

View file

@ -92,7 +92,7 @@
<ng-container *ngIf="cpfpInfo?.cluster && !isAcceleration; else legacyCpfp">
<div class="box">
<app-cluster-diagram
[cluster]="cpfpInfo.cluster" [txid]="tx.txid">
[cluster]="cpfpInfo.cluster" [txid]="tx.txid" [isMobile]="isMobile">
</app-cluster-diagram>
</div>
</ng-container>
@ -393,4 +393,4 @@
</ng-template>
</ng-template>
</div>
</div>