Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7dc05a9
style(card): one card header anatomy with aligned insets
ampagent Oct 21, 2026
e4e2329
style(radius): two radius tokens, rounded by default
ampagent Oct 21, 2026
21471b7
style(controls): one 32px control family
ampagent Oct 21, 2026
4ab73a3
style(type): quieter page headers and one micro type size
ampagent Oct 21, 2026
8340022
style(table): one table density that sits flush in cards
ampagent Oct 21, 2026
5dc42ee
Merge branch 'design/2-one-radius' into design/all
ampagent Oct 21, 2026
fee4359
Merge branch 'design/3-controls' into design/all
ampagent Oct 21, 2026
514d5e8
Merge branch 'design/4-type-ramp' into design/all
ampagent Oct 21, 2026
7d543b8
Merge branch 'design/5-tables' into design/all
ampagent Oct 21, 2026
8a43ce4
style(type): sort text-2xs classes
ampagent Oct 21, 2026
1fa771f
Merge branch 'design/4-type-ramp' into design/all
ampagent Oct 21, 2026
1b9991c
style(type): Geist for UI, Geist Mono for identifiers, Space Grotesk …
ampagent Oct 21, 2026
6213384
style(surface): hairline panels and a three-step palette
ampagent Oct 21, 2026
2413d25
style(shell): one-line app frame with neutral navigation
ampagent Oct 21, 2026
ccb8242
style(stats): headline numbers as one KPI strip
ampagent Oct 21, 2026
efa84fb
style(charts): quieter chart chrome for MultiLine
ampagent Oct 21, 2026
8b0a184
style(shell): render the top bar only when there are breadcrumbs
ampagent Oct 21, 2026
0d62b22
style(card): format
ampagent Oct 21, 2026
60b9fa9
style: remove decorative motion, blur and gradients
ampagent Oct 21, 2026
9ab1108
style(consensus): one stack of synced charts with a lead metric
ampagent Oct 21, 2026
06a1c94
style(copy): cut subtitles that restate the title or window, collapse…
ampagent Oct 21, 2026
54da508
feat(lists): page slots and epochs through full history
ampagent Oct 21, 2026
6abe220
feat(entities): page an entity's block proposals through full history
ampagent Oct 21, 2026
cf792f1
style(charts): share MultiLine's chart chrome across chart types
ampagent Oct 21, 2026
2b87e28
style(components): no shadows, one radius scale and one focus style
ampagent Oct 21, 2026
5cc77fd
style(pages): consensus-side pages on the shared card, stats and badge
ampagent Oct 21, 2026
29032f6
style(pages): execution, data availability, xatu and home on shared c…
ampagent Oct 21, 2026
7639b86
refactor(button): share the button look with link-buttons; stats take…
ampagent Oct 21, 2026
a1d7e4d
fix(live): only call column data unavailable after the deadline
ampagent Oct 21, 2026
22e797d
feat(locally-built-blocks): page prepared blocks through indexed history
ampagent Oct 21, 2026
2da5c2e
style(dashboards): stack synced charts with a lead metric
ampagent Oct 21, 2026
c18dfda
feat(charts): label lines at their ends; client series use client col…
ampagent Oct 21, 2026
50c0ffb
style: colour only exceptions
ampagent Oct 21, 2026
552593f
feat(home): a start page with every network's head and indexing state
ampagent Oct 21, 2026
b6082f6
style: tidy end labels and the pager on narrow screens; document colo…
ampagent Oct 21, 2026
aa34991
fix(charts): round axis bounds, keep line and fork labels from overpr…
ampagent Oct 22, 2026
ba08440
feat(home): replace the page list with a live feed of the latest blocks
ampagent Oct 22, 2026
64d0d09
fix(charts): keep fork labels above the data and inside the plot
ampagent Oct 22, 2026
2b33bb1
feat(charts): calendar date ticks, compact axis numbers, quieter anno…
ampagent Oct 22, 2026
15f83f3
feat(charts): one-row legend with mark swatches, shorter merged fork …
ampagent Oct 22, 2026
6604e10
ci: run the Test job on ubuntu-24.04
ampagent Oct 22, 2026
ec84627
fix(charts): start fork labels below the plot's top edge
ampagent Oct 22, 2026
7ba7393
fix(slots): redraw Attestation Arrivals by Block Vote
ampagent Oct 22, 2026
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
17 changes: 11 additions & 6 deletions .claude/rules/charts.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@

## Visual Standards

**Axis Lines:**
**Axes and grid** (set by the core components; wrappers don't override them):

- **ALWAYS** render both y-axis and x-axis lines unless specifically not needed
- Set `axisLine: { show: true }` for both xAxis and yAxis
- The category / time axis draws its axis line; the value axis has no axis line
- Value axes draw hairline split lines in `border`; category axes draw none
- Value-axis numbers are compact from 10,000 up ("40M", "1.2B") via `formatAxisNumber` in `chartChrome.ts`; pass a `formatter` only for units (%, ms, ETH)
- Date category labels ("Jan 15 '24") tick on calendar boundaries — years, quarters, months, weeks or days, picked for the chart's width (`dateCategoryTicks`). Don't thin labels yourself

**Grid Lines:**
**Marks:**

- **NEVER** render grid lines
- Set `splitLine: { show: false }` for both xAxis and yAxis
- Lines default to 1.5px; series over 600 points are LTTB-sampled to the plot's resolution
- Share stacks (every layer filled) render as opaque bands; confidence bands (`createBandSeries`) stay translucent
- Fork and other mark lines draw at half strength with title-case names; crowded marks merge into one label ("Istanbul · Muir Glacier", or "Fusaka +2" for three or more)
- The legend is one wrapping row of groups; swatches match the mark (line, dashed line, or block for areas and bands)
- The zoom track only appears once a chart is zoomed

**Axis Ranges & Intervals:**

Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/vet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ jobs:

test:
name: Test
runs-on: ubuntu-latest
# Playwright 1.58 has no Chromium build for ubuntu-latest's 26.04 image yet
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Expand Down
191 changes: 142 additions & 49 deletions .interface-design/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

**Signature element:** The PopoutCard. Every chart lives in an expandable container that breaks out to fullscreen or exports as a branded image. Fork annotations read like geological strata across synchronized chart groups.

**Typeface:** Space Grotesk Variable — geometric precision with personality. Not sterile, not playful. Technical with warmth.
**Typeface:** Geist for UI text and numbers (`font-sans`), Geist Mono for identifiers — hashes, graffiti, code (`font-mono`). Space Grotesk is the brand display face (`font-display`), used only for the wordmark and the landing hero. Charts get Geist through the registered `lab` ECharts theme (`ECHARTS_THEME`), since canvas text can't inherit CSS fonts.

---

Expand Down Expand Up @@ -60,24 +60,44 @@ Constant across all themes:

---

## Depth Strategy
## Colour Only Exceptions

**Hybrid: shadows (light) + outlines (dark).**
Data values render in `text-foreground` (or `text-muted` when secondary). Colour marks what needs
attention: `warning` and `danger` for thresholds, failures, missed/orphaned/late states and lag.
Good and normal values (participation, Ready, Canonical, Delivered, pass marks) are not green;
the normal state of a multi-state badge is neutral. `success` remains for live/running
indicators and transient feedback ("Copied"). Categorical data colours (chart series, call
types, heatmaps) are data, not judgement. Each Ethereum client has one colour everywhere:
`getClientColor` in `src/utils/ethereum.ts`.

- Light mode: `shadow-sm` on cards
- Dark mode: `outline outline-border` (no shadows — they disappear on dark backgrounds)
- Star mode: follows dark mode pattern
- Interactive hover: `-translate-y-0.5 shadow-xs` (light), no shadow change (dark)
## Depth Strategy

---
**Hairlines only, both themes.** Panels are `bg-surface outline -outline-offset-1 outline-border`;
no shadows in light mode, no lift on hover. The palette has three clearly separated steps —
page (`background`), panel (`surface`), hairline (`border`) — so panels read without shadows.
The sidebar uses the page background, so content panels are the only raised surfaces.

| Token | Light | Dark |
|---|---|---|
| background | sand-100 | #0f0f0e |
| surface | #fbfaf6 | #181817 |
| border | #dcd7c5 | #2c2b29 |
| muted | sand-700 | #a19f9a |
| foreground | sand-950 | #f5f5f4 |

## Border Radius

`rounded-sm` everywhere. Cards, buttons, inputs, navigation items, badges, legends.
Two radii, set as tokens in `src/index.css`:

No mixing of sharp and round. One radius, one feel.
| Class | Value | Use |
|---|---|---|
| `rounded-xs` | 4px | Inline marks: badges, tags, skeleton bars, toggle knobs |
| `rounded-sm` | 6px | Every container and control: cards, buttons, inputs, selects, toggles, menus, nav items |
| `rounded-full` | — | Dots, avatars, progress tracks only |

---
`rounded-md` through `rounded-3xl` alias to `rounded-sm`, so legacy classes can't drift. Card,
Button, ButtonGroup, SelectMenu, Toggle and Checkbox are rounded by default; their `rounded` prop
exists only to opt out (for example a card bleeding into a page edge).

## Spacing

Expand All @@ -87,9 +107,9 @@ No mixing of sharp and round. One radius, one feel.
|---|---|---|
| Container padding | responsive | `p-4 sm:p-6 lg:p-8` |
| Container max-width | responsive | `max-w-screen-2xl 3xl:max-w-screen-3xl 4xl:max-w-screen-4xl` |
| Card header padding | responsive | `px-4 py-5 sm:px-6` |
| Card body padding | responsive | `px-4 py-5 sm:p-6` |
| Card footer padding | responsive | `px-4 py-4 sm:px-6` |
| Card header padding | responsive | `px-4 py-3 sm:px-5` |
| Card body padding | responsive | `p-4 sm:p-5` |
| Card footer padding | responsive | `px-4 py-3 sm:px-5` |
| Page header margin | 32px | `mb-8` |
| Grid gap | 24px | `gap-6` |
| Section dividers | divide-y | `divide-y divide-border` |
Expand All @@ -98,22 +118,38 @@ No mixing of sharp and round. One radius, one feel.

## Typography

All text uses Space Grotesk Variable.
All UI text uses Geist (`font-sans`). Identifiers use Geist Mono (`font-mono`). Space Grotesk (`font-display`) is reserved for the brand.

| Role | Classes |
|---|---|
| Page title (md) | `text-4xl/tight font-bold text-foreground` |
| Page title (sm) | `text-2xl/tight font-bold text-foreground` |
| Page description | `text-base text-muted` |
| Card title | `text-lg/7 font-semibold text-foreground` |
| Card subtitle | `text-sm text-muted` |
| Page title (md) | `text-2xl/8 font-semibold tracking-tight text-foreground` |
| Page title (sm) | `text-lg/7 font-semibold tracking-tight text-foreground` |
| Page description | `text-sm/6 text-muted max-w-3xl` |
| Card title | `text-sm/6 font-semibold text-foreground` (rendered by `Card title`) |
| Card subtitle | `text-xs/5 text-muted` (rendered by `Card subtitle`) |
| Section header | `text-xs font-semibold tracking-wider text-foreground` |
| Subsection header | `text-[10px] font-semibold tracking-wider text-muted/60 uppercase` |
| Subsection header | `text-2xs font-semibold tracking-wider text-muted/60 uppercase` |
| Body text | `text-sm text-foreground` |
| Metadata | `text-xs text-muted` |
| Dense label | `text-2xs` (10px) — axis ticks, HUD keys, badges in tight cells |

The ramp is `text-2xs` → `xs` → `sm` → `base` → `lg` → `2xl`. Never use arbitrary pixel sizes
(`text-[9px]`, `text-[11px]`); `text-2xs` is the floor.

---

## Controls

Filter rows mix selects, segmented controls and buttons, so they share one size:

| Control | Recipe |
|---|---|
| Height | `h-8` (32px) for SelectMenu, SegmentedControl, Button `sm`, filter inputs |
| Text | `text-sm` (selects, inputs, buttons), `text-xs font-medium` (segments) |
| Surface | `bg-surface border border-border` — no blur, no shadow |
| Hover | `hover:border-primary/40` |
| Chevron | `size-4 text-muted` |

## Interactive States

| State | Pattern |
Expand All @@ -138,40 +174,88 @@ bg-surface rounded-sm
Light: shadow-sm
Dark: outline outline-border
Sections: divide-y divide-border
Header: px-4 py-5 sm:px-6
Body: px-4 py-5 sm:p-6
Header: px-4 py-3 sm:px-5
Body: p-4 sm:p-5
```

Pass `title`, `subtitle` and `actions` instead of hand-built header markup. The header row is
one recipe everywhere: `text-sm/6` semibold title, `text-xs/5` muted subtitle, actions right-aligned
and pulled into the header's vertical padding so they never make it taller. Header and body share
the same horizontal inset, so content starting at the body's left edge lines up with the title.
Rows with their own hover background (`px-2`) offset it with `-mx-2` on the list.

Variants: `default`, `muted`, `primary`, `accent`, `elevated`, `surface`.

### PopoutCard

Wraps Card with expand-to-modal and download capabilities.

- Title: `text-lg/7 font-semibold text-foreground`
- Subtitle: `text-sm text-muted`
- Actions (top-right): expand icon + dropdown for export
- Uses the Card header (`title`/`subtitle`/`actions`)
- Actions (top-right): `size-4` icons in `p-1.5` buttons — expand + export dropdown
- Modal size options: `default`, `lg`, `xl`, `full`
- `anchorId` prop for deep-linking via URL hash
- Render prop `({ inModal })` for responsive chart heights
- Download metadata footer: grid with logo, title, subtitle, branding

### Long lists

Any list of a sequence that goes further back than one screen (slots, epochs, an entity's
proposals) pages through its full indexed history. Never hard-cap to "the last N" and never use
"Load more".
- `RangePager` (`src/components/Lists/RangePager`): range on screen, Latest / Newer / Older, and
"Go to" a number. Put it above the table (and below long tables).
- Pages are addressed by their top number in the URL (`?from=`), so every page is linkable.
- `getRangePage` clamps to the earliest indexed row from `/bounds`, so a jump past the start
lands on the oldest page instead of an empty one.
- Tables that summarise a page (e.g. offline validators by entity) list every row, worst first,
with a name filter; charts above them may show the top N but must say so.

### Stats (KPI strip)

Headline numbers are one panel, not a card per number: a hairline panel split by internal rules.
- Cell: `px-4 py-3.5 sm:px-5`; label `text-xs/5 font-medium text-muted` with an optional
`size-3.5` icon (tinted by `iconColor` when given); value `text-2xl/8 font-semibold tracking-tight tabular-nums`
- Delta: `text-xs` with a 16px arrow, inline after the value
- `accentColor` draws a 2px rule on the cell's top edge
- Pages control columns with `gridClassName`; gaps are always zero


`Table` (`src/components/Lists/Table`) has one density for every list of records:

- Rows: `py-2.5` (~40px), `divide-y divide-border`, no zebra, `hover:bg-background`
- Column labels: `text-xs font-medium text-muted` (not bold foreground — the data is the content)
- Cells: `text-sm text-foreground tabular-nums`; pass `cellClassName` to mute secondary columns
- Edge columns are padded by `--table-inset`. A `nested` table inside a Card body bleeds to the
card edges (rules span the full card) and takes the card's inset, so the first column lines up
with the card title. Placed first or last in the body it also absorbs the vertical padding.
- Placed directly on a page, a `nested` table gets the Card surface and depth; inside a Card body
it is transparent (the Card body sets `data-slot="card-body"`, `--card-inset`, `--card-inset-y`).

### Header

Page-level header with optional accent bar.
Page-level header. Size and weight carry the hierarchy; no accent bar or other ornament.

- Accent bar: `h-1 w-24 rounded-sm bg-linear-to-r from-primary to-accent`
- Sizes: `xs`, `sm`, `md` (default), `lg`, `xl`
- Margin below: `mb-8` (md)
- Margin below: `mb-6` (md)

### Container

Page wrapper. `mx-auto p-4 sm:p-6 lg:p-8 max-w-screen-2xl`.

### App frame

- Top bar: `h-12 border-b border-border bg-background/85 backdrop-blur-sm`, sticky on desktop, holds
the breadcrumb. Its rule continues the sidebar header's rule, so the frame is one line.
- Breadcrumb: ancestors `text-muted hover:text-foreground`, current page `font-medium text-foreground`,
separators `text-muted/50`. No accent color.
- Navigation states are neutral: hover `bg-foreground/5 text-foreground`, active
`bg-foreground/[0.07] text-foreground` with a primary icon. Primary color marks selection in
controls, not navigation.

### Sidebar

- Width: `w-14` collapsed, `w-56` expanded
- Background: `bg-surface/95 backdrop-blur-xl`
- Background: `bg-background` (same as the page)
- Border: `border-r border-border`
- Nav items: `rounded-sm px-2 py-1 text-sm/5 font-medium`
- Icons: `size-4 shrink-0`
Expand All @@ -184,19 +268,20 @@ Page wrapper. `mx-auto p-4 sm:p-6 lg:p-8 max-w-screen-2xl`.
### Grid

```tsx
grid: { left: 60, right: 24, top: 16, bottom: 50 }
// top: 40 if title present
// right: 70 if dual y-axes
// bottom: +40 for legend, +40 for dataZoom
grid: { left: 52, right: 16, top: 12, bottom: 28 }
// top: 32 when the y-axis has a name (it sits above the axis)
// right: 56 with a secondary y-axis
// bottom: 48 when a numeric x-axis has a name, +40 for native legend, +28 for dataZoom
```

### Axes

- Always show axis lines: `axisLine: { show: true }`, color = `themeColors.border`
- Never show grid lines: `splitLine: { show: false }`
- Label color: `themeColors.muted`, size 12
- Name position: `nameLocation: 'middle'`, xAxis gap 30, yAxis gap 50
- Name color: `themeColors.foreground`, size 12
- Horizontal hairline grid lines on the value axis (`splitLine` solid, `themeColors.border`); no y-axis line
- X-axis line in `themeColors.border`, no ticks, `hideOverlap` labels
- Labels `themeColors.muted`, size 11
- Y-axis name reads horizontally above the axis (`nameLocation: 'end'`), not rotated beside it
- X-axis name only on numeric axes ("Slot"); dates and categories label themselves, so "Date" is dropped
- dataZoom slider: an 8px neutral track with thin handles, no data preview, no accent color

### Lines

Expand All @@ -206,19 +291,26 @@ grid: { left: 60, right: 24, top: 16, bottom: 50 }
- Emphasis: symbol size 8, line width 4
- Animation: 300ms, cubicOut easing

### End labels

MultiLine names each plain line at its right end (2–8 lines; not stacked/area/bar/band series).
Off on charts narrower than 480px and on charts with a secondary y-axis; `endLabels={false}`
opts out. Legends stay: they toggle series.

### Tooltip

- Background: `themeColors.background`
- Panel: `themeColors.surface`, 6px radius, soft shadow, padding 8×10
- Background: `themeColors.surface`
- Border: `themeColors.border`
- Text: `themeColors.foreground`, size 12
- Axis pointer: dashed line in `themeColors.muted`

### Legend (Interactive)

Pill-style buttons (not native ECharts legend):
- `rounded-sm border px-2 py-1 text-xs/5`
- Active: `bg-surface-hover border-border text-foreground`
- Inactive: `border-transparent bg-surface/50 text-muted/50`
Borderless toggles (not native ECharts legend), with no rule between legend and chart:
- `rounded-xs px-1.5 py-0.5 text-xs/5 hover:bg-muted/10`
- Visible: `text-foreground`, filled dot
- Hidden: `text-muted/60`, hollow dot
- Color indicator: `h-2 w-2 rounded-full`

### Synchronized Crosshairs
Expand Down Expand Up @@ -265,9 +357,10 @@ Two-column responsive grid with time controls:

### Time Period Selector

Button group with pill buttons:
- Active: `bg-primary text-primary-foreground ring-2 ring-primary/30`
- Inactive: `bg-surface text-muted ring-1 ring-border`
`SegmentedControl` (`src/components/Forms/SegmentedControl`) — never hand-roll pill rows.
- Track: `h-8 rounded-sm bg-surface p-0.5 ring-1 ring-border`
- Selected segment: `bg-primary/15 text-primary` (the app-wide selected state)
- Other segments: `text-muted hover:bg-muted/10 hover:text-foreground`

### Fork Annotations

Expand Down Expand Up @@ -299,6 +392,6 @@ Elements with fade-in start with `opacity: 0`.
- Use locale formatting for slots/epochs (no commas)
- Override chart grid config in wrapper components unless necessary
- Mix depth strategies (shadows in dark mode, outlines in light mode)
- Use `rounded-md`, `rounded-lg` — only `rounded-sm`
- Add grid lines to charts
- Reach for `rounded-md`/`rounded-lg` — they alias to `rounded-sm`; write `rounded-sm` (or `rounded-xs` for inline marks)
- Use `rounded-full` for buttons or segmented controls
- Use multiple accent colors — terracotta is the accent
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"build-storybook:gh-pages": "STORYBOOK_BASE_PATH=/lab storybook build"
},
"dependencies": {
"@fontsource-variable/geist": "5.3.0",
"@fontsource-variable/geist-mono": "5.3.0",
"@fontsource-variable/space-grotesk": "5.2.10",
"@headlessui/react": "2.2.9",
"@heroicons/react": "2.2.0",
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

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

Loading
Loading