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
19 changes: 18 additions & 1 deletion components/assessment/AssessmentApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SLICES, allAnswered, computeStage, decodeShareCode, shareCode } from "@
import type { SliceColor, SliceId } from "@lib/rubric/types";
import { VbButton } from "@components/ui/VbButton";
import { getShareUrl, shareNameFromQuery } from "@constants/index";
import { Box } from "@obolnetwork/obol-ui";
import { CONFETTI_BRAND } from "@lib/theme/tokens";
import {
Blockers,
Expand Down Expand Up @@ -107,7 +108,23 @@ export function AssessmentApp({ initialShareCode }: AssessmentAppProps) {
<Shell>
<SiteHeader contentWidth={1440} />

<MainGrid>
<MainGrid as="main" id="main-content">
<Box
as="h1"
css={{
position: "absolute",
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: "hidden",
clip: "rect(0 0 0 0)",
whiteSpace: "nowrap",
border: 0,
}}
>
Validator self-assessment
</Box>
<LeftCard>
{a.atIntro ? (
<Intro onStart={a.start} />
Expand Down
5 changes: 4 additions & 1 deletion components/assessment/Question.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,17 @@ export function Question({
type="button"
selected={sel}
color={o.color}
aria-pressed={sel}
onClick={() => onChoose(sliceId, o.color)}
>
<RiskDot color={o.color} size="lg" />
<OptionBody>
<OptionLabel>{o.label}</OptionLabel>
{o.sub && <OptionSub>{o.sub}</OptionSub>}
</OptionBody>
<OptionCheck selected={sel}>{sel ? "✓" : ""}</OptionCheck>
<OptionCheck selected={sel} aria-hidden="true">
{sel ? "✓" : ""}
</OptionCheck>
</OptionButton>
);
})}
Expand Down
18 changes: 10 additions & 8 deletions components/landing/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -689,14 +689,16 @@ export function Landing() {
return (
<Box css={{ minHeight: "100vh", backgroundColor: "$bg01", color: "$textMiddle" }}>
<SiteHeader />
<LHero />
<LGap />
<LFail />
<LRead />
<LSides />
<LValos />
<LNeutral />
<LClosing />
<Box as="main" id="main-content">
<LHero />
<LGap />
<LFail />
<LRead />
<LSides />
<LValos />
<LNeutral />
<LClosing />
</Box>
<SiteFooter />
</Box>
);
Expand Down
16 changes: 15 additions & 1 deletion components/layout/SiteFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Box, Text } from "@obolnetwork/obol-ui";
import type { CSS } from "@stitches/react";
import { TopNavLink, TopSpacer } from "@components/assessment/stitches";
import { ASSESS_PATH, METHODOLOGY_PATH, VALOS_URL } from "@constants/index";
import {
ASSESS_PATH,
GITHUB_URL,
METHODOLOGY_PATH,
VALOS_URL,
} from "@constants/index";

const navLink: CSS = {
fontSize: "$2",
Expand Down Expand Up @@ -54,6 +59,15 @@ export function SiteFooter({ contentWidth = 1140 }: SiteFooterProps) {
>
valOS
</Box>
<Box
as="a"
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
css={navLink}
>
GitHub
</Box>
<TopNavLink href={ASSESS_PATH}>Assess</TopNavLink>
</Box>
</Box>
Expand Down
19 changes: 18 additions & 1 deletion components/layout/SiteHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,24 @@ export function SiteHeader({ contentWidth = 1140 }: SiteHeaderProps) {
const onAssess = router.pathname === "/assess";

return (
<Box as="nav" css={bar}>
<Box as="nav" aria-label="Main" css={bar}>
<Box
as="a"
href="#main-content"
css={{
position: "absolute",
left: -9999,
zIndex: 10,
padding: "8px 14px",
backgroundColor: "$bg01",
color: "$body",
fontSize: "$2",
fontWeight: "$semibold",
"&:focus-visible": { left: 8, top: 8 },
}}
>
Skip to content
</Box>
<Box
css={{
display: "flex",
Expand Down
49 changes: 49 additions & 0 deletions components/methodology/AnchorHeading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useRef, useState, type ReactNode } from "react";

type AnchorHeadingProps = {
/** Stable slug — becomes the element id and the #fragment. Treat as a public URL. */
id: string;
as?: "h2" | "h3";
children: ReactNode;
};

/**
* Heading with a hover-revealed "#" that navigates to the fragment and copies
* the full section URL to the clipboard, with brief inline feedback.
*/
export function AnchorHeading({
id,
as: Tag = "h2",
children,
}: AnchorHeadingProps) {
const [copied, setCopied] = useState(false);
const timer = useRef<number | undefined>(undefined);

const copy = () => {
const url = `${window.location.origin}${window.location.pathname}#${id}`;
void navigator.clipboard?.writeText(url).then(() => {
setCopied(true);
window.clearTimeout(timer.current);
timer.current = window.setTimeout(() => setCopied(false), 1600);
});
};

return (
<Tag id={id} className="vb-anchor">
{children}
<a
href={`#${id}`}
className="vb-anchor__link"
aria-label="Copy link to this section"
onClick={copy}
>
#
</a>
{copied && (
<span className="vb-anchor__copied" role="status">
Link copied
</span>
)}
</Tag>
);
}
10 changes: 9 additions & 1 deletion components/pizza/Pizza.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,22 @@ export function Pizza({
const circ = 2 * Math.PI * ringR;
const frac = answered / SLICES.length;

const sliceDesc = SLICES.map(
(s) => `${s.short} ${answers[s.id] ?? "unanswered"}`,
).join(", ");
const ariaLabel =
stage != null
? `Validator resilience pizza — Stage ${stage}. ${sliceDesc}.`
: `Validator resilience pizza — ${answered} of ${SLICES.length} slices answered. ${sliceDesc}.`;

return (
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
className="vbpizza"
role="img"
aria-label="Decentralization pizza — six slices"
aria-label={ariaLabel}
>
{big && (
<defs>
Expand Down
1 change: 1 addition & 0 deletions constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const SITE_URL =
export const ASSESS_PATH = "/assess/";
export const METHODOLOGY_PATH = "/methodology/";
export const VALOS_URL = "https://lidofinance.github.io/valos/valos-spec.html";
export const GITHUB_URL = "https://github.com/ObolNetwork/validator-beat";

export {
getShareUrl,
Expand Down
33 changes: 33 additions & 0 deletions lib/share/eth-mark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Ethereum eth-diamond logomark inside a brand-green circular badge — the same
* lockup as public/icon.svg (green disc, light diamond), rendered inline.
* Polygon points are the Wikimedia originals (native viewBox ≈ 256×417); the
* opacity tiers preserve the 3D facets.
*
* @param cx badge centre x, in the parent SVG's user units
* @param cy badge centre y
* @param radius badge radius in px
* @param circleFill disc colour (brand green)
* @param diamondFill diamond colour (light grey / white)
*/
export function ethBadge(
cx: number,
cy: number,
radius: number,
circleFill: string,
diamondFill: string,
): string {
// Diamond height ≈ 57% of the badge diameter (matches icon.svg's 0.7 scale).
const s = (radius * 1.14) / 416.906;
const tx = (cx - 127.96 * s).toFixed(2);
const ty = (cy - 208.45 * s).toFixed(2);
return `<circle cx="${cx}" cy="${cy}" r="${radius}" fill="${circleFill}"/>
<g transform="translate(${tx} ${ty}) scale(${s.toFixed(4)})" fill="${diamondFill}">
<polygon opacity="1" points="127.9611 0 125.1661 9.5 125.1661 285.168 127.9611 287.958 255.9231 212.32"/>
<polygon opacity="0.6" points="127.962 0 0 212.32 127.962 287.959 127.962 154.158"/>
<polygon opacity="1" points="127.9611 312.1866 126.3861 314.1066 126.3861 412.3056 127.9611 416.9066 255.9991 236.5866"/>
<polygon opacity="0.6" points="127.962 416.9052 127.962 312.1852 0 236.5852"/>
<polygon opacity="0.45" points="127.9611 287.9577 255.9211 212.3207 127.9611 154.1587"/>
<polygon opacity="0.8" points="0.0009 212.3208 127.9609 287.9578 127.9609 154.1588"/>
</g>`;
}
45 changes: 45 additions & 0 deletions lib/share/landing-og-svg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { SliceColor } from "@lib/rubric/types";
import {
FONT,
OG_ACCENT,
OG_HEAD,
OG_HUB_IDLE_STROKE,
OG_SUB,
ogCta,
ogHeader,
ogPizza,
ogPulse,
ogShell,
} from "@lib/share/og-card";
import { PIZZA_FILL } from "@lib/theme/tokens";

/**
* Default OG card for the landing page — the "What stage is your validator?"
* hook with a mixed pizza and a STAGE ? hub, so viewers realise they don't
* know their own answer. Rendered to public/og/landing.png at build time.
*/
export function landingOgSvg(
displayHost: string = "validatorbeat.com",
): string {
// Mixed, mostly-fine-with-one-red — "you're probably not as safe as you think".
const colors: SliceColor[] = [
"green",
"yellow",
"red",
"green",
"yellow",
"green",
];

return ogShell(`
${ogHeader(displayHost)}
<text x="64" y="186" font-family="${FONT}" font-size="19" font-weight="600" fill="${OG_ACCENT}" letter-spacing="0.12em">THE STANDARD FOR ETHEREUM STAKING SECURITY</text>
<text x="64" y="262" font-family="${FONT}" font-size="66" font-weight="700" fill="${OG_HEAD}">What stage is</text>
<text x="64" y="340" font-family="${FONT}" font-size="66" font-weight="700" fill="${OG_HEAD}">your validator?</text>
<text x="64" y="402" font-family="${FONT}" font-size="26" fill="${OG_SUB}"><tspan fill="${PIZZA_FILL.red}" font-weight="700">Stage 0</tspan> — one failure can slash it.</text>
<text x="64" y="440" font-family="${FONT}" font-size="26" fill="${OG_SUB}"><tspan fill="${PIZZA_FILL.yellow}" font-weight="700">Stage 1</tspan> — one failure won't slash it.</text>
<text x="64" y="478" font-family="${FONT}" font-size="26" fill="${OG_SUB}"><tspan fill="${PIZZA_FILL.green}" font-weight="700">Stage 2</tspan> — one failure won't cause an outage.</text>
${ogPulse(514)}
${ogCta(`Find your stage → ${displayHost}`, 578)}
${ogPizza({ colors, hubText: "?", hubTextColor: PIZZA_FILL.yellow, hubStroke: OG_HUB_IDLE_STROKE })}`);
}
Loading