Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/screenshots/hover-card-aggregated.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/legend-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/legend-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@floating-ui/react": "0.26.28",
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@headlessui/react": "2.2.10",
Expand Down
3 changes: 3 additions & 0 deletions web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

193 changes: 94 additions & 99 deletions web/src/components/AggregatedNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,10 @@ import { memo } from 'react';
import { EyeIcon, CheckIcon, FlagIcon } from '@heroicons/react/20/solid';
import classNames from 'clsx';

import HoverCard from '@components/HoverCard';
import { AggregatedNodeCard } from '@components/NodeCards';
import ProgressCircle from '@components/ProgressCircle';

/* Mirrors the WeightedNode status recipe: dim core, bright rim, soft glow. */
const NODE_STYLES: Record<
string,
{ fill: string; arc: string; track: string; label: string; glow: string }
> = {
canonical: {
fill: 'bg-canonical-deep',
arc: 'text-canonical-ring',
track: 'text-canonical-edge',
label: 'text-canonical',
glow: 'shadow-[0_0_90px_-15px_var(--color-canonical-ring)]',
},
fork: {
fill: 'bg-fork-deep',
arc: 'text-fork-ring',
track: 'text-fork-edge',
label: 'text-fork',
glow: 'shadow-[0_0_90px_-15px_var(--color-fork-ring)]',
},
finalized: {
fill: 'bg-finalized-deep',
arc: 'text-finalized-ring',
track: 'text-finalized-edge',
label: 'text-finalized',
glow: 'shadow-[0_0_90px_-15px_var(--color-finalized-ring)]',
},
justified: {
fill: 'bg-justified-deep',
arc: 'text-justified-ring',
track: 'text-justified-edge',
label: 'text-justified',
glow: 'shadow-[0_0_90px_-15px_var(--color-justified-ring)]',
},
invalid: {
fill: 'bg-invalid-deep',
arc: 'text-invalid-ring',
track: 'text-invalid-edge',
label: 'text-invalid',
glow: 'shadow-[0_0_90px_-15px_var(--color-invalid-ring)]',
},
optimistic: {
fill: 'bg-optimistic-deep',
arc: 'text-optimistic-ring',
track: 'text-optimistic-edge',
label: 'text-optimistic',
glow: 'shadow-[0_0_90px_-15px_var(--color-optimistic-ring)]',
},
};
import { NODE_STYLES, NodeStyleKey } from '@utils/nodeStyles';

function AggregatedNode({
id,
Expand All @@ -69,6 +23,8 @@ function AggregatedNode({
x,
y,
radius,
slot,
epoch,
className,
onClick,
}: {
Expand All @@ -86,10 +42,12 @@ function AggregatedNode({
x: number;
y: number;
radius: number;
slot?: number;
epoch?: number;
className?: string;
onClick?: (hash: string) => void;
}) {
const [styleKey, title] = ((): [string, string] => {
const [styleKey, title] = ((): [NodeStyleKey, string] => {
if (valid !== seen) return ['invalid', `${seen - valid} NOT VALID`];
if (orphans > 0) return ['invalid', `${orphans} DETACHED`];
if (finalizedCheckpoints > 0) return ['finalized', 'FINALIZED'];
Expand All @@ -105,60 +63,97 @@ function AggregatedNode({
return ['canonical', type];
}
})();
const styles = NODE_STYLES[styleKey] ?? NODE_STYLES.canonical;
const styles = NODE_STYLES[styleKey];

const handleActivate = () => onClick?.(hash);

return (
<div
id={id}
className={classNames(
'absolute flex cursor-pointer flex-col items-center justify-center gap-3 rounded-full transition-[filter] duration-150 hover:brightness-110',
styles.fill,
styles.glow,
className,
)}
style={{
left: `${x}px`,
top: `${y}px`,
width: `${radius * 2}px`,
height: `${radius * 2}px`,
<HoverCard
accent={styleKey}
content={
<AggregatedNodeCard
styleKey={styleKey}
status={title}
blockRoot={hash}
slot={slot}
epoch={epoch}
seen={seen}
canonical={canonical}
finalizedCheckpoints={finalizedCheckpoints}
justifiedCheckpoints={justifiedCheckpoints}
orphans={orphans}
valid={valid}
optimistic={optimistic}
total={total}
/>
}
referenceProps={{
onClick: handleActivate,
onKeyDown: event => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleActivate();
}
},
}}
onClick={() => onClick?.(hash)}
>
<ProgressCircle
progress={(canonical / total) * 100}
radius={radius}
className="absolute"
color={styles.arc}
backgroundColor={styles.track}
/>
<p
className={classNames(
'h-16 pt-6 font-mono text-xl font-semibold uppercase tracking-widest',
styles.label,
)}
>
{title}
</p>
<p className="font-mono text-3xl font-semibold text-foreground-strong">
{hash.substring(0, 6)}…{hash.substring(hash.length - 4)}
</p>
<p className="flex h-16 gap-6 pt-2 font-mono text-xl tabular-nums text-foreground/70">
<span className="flex flex-col items-center gap-1">
{finalizedCheckpoints > 0 || justifiedCheckpoints > 0 ? (
<>
<FlagIcon className="size-5" /> {finalizedCheckpoints || justifiedCheckpoints}/{total}
</>
) : (
<>
<CheckIcon className="size-5" /> {canonical}/{total}
</>
{referenceProps => (
<div
id={id}
role="button"
tabIndex={0}
{...referenceProps}
className={classNames(
'absolute flex cursor-pointer flex-col items-center justify-center gap-3 rounded-full transition-[filter] duration-150 hover:brightness-110',
styles.fill,
styles.glow,
className,
)}
</span>
<span className="flex flex-col items-center gap-1">
<EyeIcon className="size-5" /> {seen}/{total}
</span>
</p>
</div>
style={{
left: `${x}px`,
top: `${y}px`,
width: `${radius * 2}px`,
height: `${radius * 2}px`,
}}
>
<ProgressCircle
progress={(canonical / total) * 100}
radius={radius}
className="absolute"
color={styles.arc}
backgroundColor={styles.track}
/>
<p
className={classNames(
'h-16 pt-6 font-mono text-xl font-semibold uppercase tracking-widest',
styles.label,
)}
>
{title}
</p>
<p className="font-mono text-3xl font-semibold text-foreground-strong">
{hash.substring(0, 6)}…{hash.substring(hash.length - 4)}
</p>
<p className="flex h-16 gap-6 pt-2 font-mono text-xl tabular-nums text-foreground/70">
<span className="flex flex-col items-center gap-1">
{finalizedCheckpoints > 0 || justifiedCheckpoints > 0 ? (
<>
<FlagIcon className="size-5" /> {finalizedCheckpoints || justifiedCheckpoints}/
{total}
</>
) : (
<>
<CheckIcon className="size-5" /> {canonical}/{total}
</>
)}
</span>
<span className="flex flex-col items-center gap-1">
<EyeIcon className="size-5" /> {seen}/{total}
</span>
</p>
</div>
)}
</HoverCard>
);
}

Expand Down
Loading
Loading