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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Pre-commit (husky) runs `yarn test` + `yarn lint`. Lint-staged additionally runs

**Default:** build UI with `@obolnetwork/obol-ui` primitives — `Box`, `Text`, `Button`, `Link` — and the project’s Stitches tokens (`$bg01`, `$body`, `$textMiddle`, `var(--theme-brand)`, etc.). Bridge tokens live in `styles/theme-tokens.css` and `styles/obol-bridge.css`.

- Prefer the **`css` prop** on `Box` / `Text` (see `components/layout/Navbar.tsx`, `components/landing/Landing.tsx`) or **reuse styled exports** from `components/assessment/stitches.ts` (`Card`, `Eyebrow`, `BrandLink`, `TopNavLink`, `risk`, …) instead of new page-specific CSS files or BEM class strings.
- Prefer the **`css` prop** on `Box` / `Text` (see `components/layout/SiteHeader.tsx`, `components/landing/Landing.tsx`) or **reuse styled exports** from `components/assessment/stitches.ts` (`Card`, `Eyebrow`, `BrandLink`, `TopNavLink`, `risk`, …) instead of new page-specific CSS files or BEM class strings.
- Use **`VbButton`** (`components/ui/VbButton.tsx`) for primary actions in the assessment flow.
- **Do not** add separate per-page CSS files (e.g. a dedicated `landing.css`) unless there is a strong reason; the landing page was migrated off that pattern.

Expand All @@ -96,7 +96,7 @@ Pre-commit (husky) runs `yarn test` + `yarn lint`. Lint-staged additionally runs
- **SVG visuals** tied to the product (`Pizza`, brand mark) — not generic UI icons; use `@radix-ui/react-icons` (already in the tree via obol-ui) for arrows, external-link, etc. (`components/landing/icons.tsx`).
- **Methodology** — still uses `styles/methodology.css` for now; new work should still prefer obol-ui where practical.

**Site chrome (future):** landing and assessment currently use different headers/footers. Planned UX is a shared slim header + compact assess footer — see **Future UX** in [`README.md`](./README.md). Do not implement unless asked.
**Site chrome:** the landing, assessment, and methodology share one header and footer — `components/layout/SiteHeader.tsx` and `SiteFooter.tsx` (the landing nav, generalized). `SiteHeader` reads the route to adapt: it hides the "Assess your validator" CTA on `/assess` and points "How it works" at `/#how` from inner pages. Both take a `contentWidth` prop to align the inner content with the page's main column (landing/methodology `1140`, assessment `1440`). The assessment renders them as the top/bottom rows of the 100vh `Shell` (`MainGrid` is `flex:1`).

## Deferred work (don't build yet unless asked)

Expand Down
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,15 @@ Every push to `main` runs CI and, on success, deploys the static `out/` folder t
- `NEXT_PUBLIC_SITE_URL=https://obolnetwork.github.io/validator-beat` — share links and OG URLs
3. **Custom domain** (`validatorbeat.com`): point DNS at GitHub Pages, then rebuild with `NEXT_PUBLIC_BASE_PATH=` (empty) and `NEXT_PUBLIC_SITE_URL=https://validatorbeat.com`.

**Theming:** edit [`styles/theme-tokens.css`](styles/theme-tokens.css) to change colors (Lido forum–aligned by default). If you change pizza slice colors, also update [`lib/theme/tokens.ts`](lib/theme/tokens.ts).
**Theming:** edit [`styles/theme-tokens.css`](styles/theme-tokens.css) to change colors (warm cream, ecosystem-neutral by default; an Obol-branded dark palette under `[data-theme="dark"]`). If you change pizza slice colors, also update [`lib/theme/tokens.ts`](lib/theme/tokens.ts).

## Future UX: shared chrome across landing and assessment
## Site chrome: shared header and footer

Today the **landing** (`/`) and **assessment** (`/assess/`) use different shells: the marketing page has a full nav + footer; the assessment uses a slim `TopBar` and a one-line `Footnote` inside a `100vh` focus layout (`AssessmentApp`). That is intentional for the six-question wizard (less distraction, pizza always visible) but can feel like leaving the site when users click “Assess your validator.”
The **landing** (`/`), **assessment** (`/assess/`), and **methodology** (`/methodology/`) share one header and footer — [`components/layout/SiteHeader.tsx`](components/layout/SiteHeader.tsx) and [`SiteFooter.tsx`](components/layout/SiteFooter.tsx) (the landing nav, generalized).

**Recommended direction (not implemented yet):**

1. **One shared slim header** on `/`, `/assess/`, and `/methodology/` — logo (home), Methodology, valOS (external), theme toggle; primary CTA on marketing pages only.
2. **Compact footer on assess** — keep the trust line (“nothing submitted or stored”) plus links (Methodology · valOS · Home), not the full landing footer.
3. **Keep focus mode during Q1–Q6** — do not wrap the wizard in the full marketing layout; optional richer links on intro/results only if needed later.

**Avoid:** pasting the full landing nav + footer around the assessment grid — it costs too much vertical space on mobile and fights the locked viewport.

See `components/assessment/AssessmentApp.tsx` (`TopBar`, `Footnote`, `Shell`) and `components/landing/Landing.tsx` (`LNav`, `LFooter`) for the current split.
- **`SiteHeader`** adapts to the route: it hides the "Assess your validator" CTA on `/assess` (you're already there) and points "How it works" at `/#how` from inner pages. The logo mark reads the `--vb-*` tokens, so it follows the theme.
- Both take a **`contentWidth`** prop so the inner content lines up with the page's main column (landing/methodology `1140`, assessment `1440`).
- The assessment renders them as the top/bottom rows of the `100vh` `Shell`; `MainGrid` is `flex:1` and absorbs the remaining height, so the focus layout still fits one screen.

## v0.1 routes

Expand Down
26 changes: 5 additions & 21 deletions components/assessment/AssessmentApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@ import {
} from "./Blockers";
import { Intro } from "./Intro";
import { Question } from "./Question";
import { ThemeToggle } from "./ThemeToggle";
import { LevelUp, ResultHero, ShareModal } from "./Results";
import { SiteHeader } from "@components/layout/SiteHeader";
import { SiteFooter } from "@components/layout/SiteFooter";
import {
BrandAccent,
BrandLink,
Footnote,
LeftCard,
MainGrid,
PizzaWrap,
Expand All @@ -31,10 +29,6 @@ import {
RightPanel,
SectionLabel,
Shell,
TagPill,
TopBar,
TopNavLink,
TopSpacer,
} from "./stitches";

type AssessmentAppProps = {
Expand Down Expand Up @@ -111,15 +105,7 @@ export function AssessmentApp({ initialShareCode }: AssessmentAppProps) {

return (
<Shell>
<TopBar>
<BrandLink href="/">
Validator <BrandAccent>Beat</BrandAccent>
</BrandLink>
<TagPill>v0.1 · self-assessment</TagPill>
<TopSpacer />
<TopNavLink href="/methodology/">Methodology</TopNavLink>
<ThemeToggle />
</TopBar>
<SiteHeader contentWidth={1440} />

<MainGrid>
<LeftCard>
Expand Down Expand Up @@ -175,10 +161,8 @@ export function AssessmentApp({ initialShareCode }: AssessmentAppProps) {
</RightCard>
</MainGrid>

<Footnote>
A neutral, public-good resource · nothing is submitted or stored · runs entirely in your
browser
</Footnote>
<SiteFooter contentWidth={1440} />


{share && a.stage != null && (
<ShareModal
Expand Down
59 changes: 57 additions & 2 deletions components/assessment/stitches.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Text, styled } from "@obolnetwork/obol-ui";
import { Box, Text, keyframes, styled } from "@obolnetwork/obol-ui";
import type { SliceColor } from "@lib/rubric/types";
import NextLink from "next/link";

Expand Down Expand Up @@ -59,6 +59,61 @@ export const BrandAccent = styled("span", {
color: "var(--theme-brand)",
});

/* "beat" — a heartbeat that cycles the three signal colors (green → yellow → red),
with a pulsing halo behind it. Reads from the --vb-* tokens, so it follows the
light/dark theme automatically. */
const beatColor = keyframes({
"0%": { color: "var(--vb-green)" },
"10%": { color: "var(--vb-green)" },
"33%": { color: "var(--vb-yellow)" },
"43%": { color: "var(--vb-yellow)" },
"66%": { color: "var(--vb-red)" },
"76%": { color: "var(--vb-red)" },
"100%": { color: "var(--vb-green)" },
});
const beatRing = keyframes({
"0%": { opacity: 0, transform: "translate(-50%,-50%) scale(0.6)", boxShadow: "0 0 0 0 var(--vb-green)" },
"6%": { opacity: 0.5, transform: "translate(-50%,-50%) scale(0.92)", boxShadow: "0 0 22px 5px var(--vb-green)" },
"14%": { opacity: 0.18, transform: "translate(-50%,-50%) scale(1.04)" },
"20%": { opacity: 0.55, transform: "translate(-50%,-50%) scale(1.0)", boxShadow: "0 0 26px 7px var(--vb-green)" },
"30%": { opacity: 0, transform: "translate(-50%,-50%) scale(1.12)", boxShadow: "0 0 0 0 var(--vb-yellow)" },
"39%": { opacity: 0.5, transform: "translate(-50%,-50%) scale(0.92)", boxShadow: "0 0 22px 5px var(--vb-yellow)" },
"47%": { opacity: 0.18, transform: "translate(-50%,-50%) scale(1.04)" },
"53%": { opacity: 0.55, transform: "translate(-50%,-50%) scale(1.0)", boxShadow: "0 0 26px 7px var(--vb-yellow)" },
"63%": { opacity: 0, transform: "translate(-50%,-50%) scale(1.12)", boxShadow: "0 0 0 0 var(--vb-red)" },
"72%": { opacity: 0.5, transform: "translate(-50%,-50%) scale(0.92)", boxShadow: "0 0 22px 5px var(--vb-red)" },
"80%": { opacity: 0.18, transform: "translate(-50%,-50%) scale(1.04)" },
"86%": { opacity: 0.55, transform: "translate(-50%,-50%) scale(1.0)", boxShadow: "0 0 26px 7px var(--vb-red)" },
"100%": { opacity: 0, transform: "translate(-50%,-50%) scale(1.12)", boxShadow: "0 0 0 0 var(--vb-green)" },
});

export const Beat = styled("span", {
position: "relative",
display: "inline-block",
fontWeight: 800,
isolation: "isolate", // contain the negative-z pulse above the section background
animation: `${beatColor} 3s cubic-bezier(.16,1,.3,1) infinite`,
"@media (prefers-reduced-motion: reduce)": {
animation: "none",
color: "var(--theme-brand)",
},
});

export const BeatPulse = styled("span", {
position: "absolute",
left: "50%",
top: "54%",
width: "1.55em",
height: "1.55em",
transform: "translate(-50%, -50%) scale(0.6)",
borderRadius: "50%",
zIndex: -1,
opacity: 0,
pointerEvents: "none",
animation: `${beatRing} 3s cubic-bezier(.16,1,.3,1) infinite`,
"@media (prefers-reduced-motion: reduce)": { animation: "none", display: "none" },
});

export const TagPill = styled(Box, {
fontSize: "$1",
fontWeight: "$semibold",
Expand Down Expand Up @@ -100,7 +155,7 @@ export const MainGrid = styled(Box, {
display: "grid",
gridTemplateColumns: "1fr 440px",
gap: 22,
padding: "4px 28px 16px",
padding: "22px 28px 16px",
alignItems: "stretch",
"@media (max-width: 880px)": {
gridTemplateColumns: "1fr",
Expand Down
Loading
Loading