Skip to content

feat: rebrand atlas frontend#63

Merged
tac0turtle merged 6 commits intomainfrom
pthmas/evolve-rebrand
Apr 22, 2026
Merged

feat: rebrand atlas frontend#63
tac0turtle merged 6 commits intomainfrom
pthmas/evolve-rebrand

Conversation

@pthmas
Copy link
Copy Markdown
Collaborator

@pthmas pthmas commented Apr 20, 2026

Summary

  • rework the frontend shell and shared visual primitives around the new Evolve-inspired Atlas design
  • refresh the homepage, explorer page heroes, search, tables, and icons to match the new brand direction
  • restore white-label compatibility for logos and theme-derived colors so custom deployments still drive the UI

Summary by CodeRabbit

  • New Features

    • New reusable UI primitives (hero, stat cards, section panels, empty states, brand ornament, entity visuals)
    • Search bar variant support and theme-aware default logos
    • Brand token pipeline for accent-driven theming
  • Style

    • Global redesign: updated layout, palettes, gradients, typography, spinner, error, pagination, buttons, and copy button
  • Chores

    • CI Rust toolchain workflow update
    • Added Docker platform configuration example
  • Tests

    • Added color/palette derivation tests

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

This PR adds white‑label branding (theme-aware logos, accent-derived CSS tokens), new reusable UI primitives, extensive CSS/token system refactor, Linux ARM64 solc target/version handling, CI rust-toolchain step adjustments, and minor Docker/env and page layout updates.

Changes

Cohort / File(s) Summary
Env & Compose
\.env.example, docker-compose\.yml
Documented DOCKER_DEFAULT_PLATFORM examples; removed platform: linux/amd64 from atlas-server; added CHAIN_LOGO_URL_LIGHT and CHAIN_LOGO_URL_DARK env vars.
CI Workflow
.github/workflows/ci.yml
Replaced dtolnay/rust-toolchain@stable with dtolnay/rust-toolchain@master and added explicit with: toolchain: stable blocks across jobs.
Backend: contracts & config
backend/crates/atlas-server/src/api/handlers/contracts.rs, backend/crates/atlas-server/src/main.rs
Make solc target selection version-aware; add parser for solc version triplet and conditional linux-arm64 support (>=0.8.31). Refactor postgres dbname query parsing with guard-based handling and added tests.
Branding: context & assets
frontend/src/context/BrandingContext.tsx, frontend/src/assets/defaultLogos.ts
Switch logo fallback to theme-aware getDefaultLogo(theme); add effect to derive/apply/clear brand tokens from config accent; update initial loading visuals.
Color utilities & tests
frontend/src/utils/color.ts, frontend/src/utils/color.test.ts, frontend/src/hooks/useChartColors.ts
Add brand token derivation (deriveBrandTokens, applyBrandTokens, clearBrandTokens); extend deriveSurfaceShades to accept accentHex; add tests; update chart color resolution to use CSS var --color-accent-primary.
Global styles & Tailwind
frontend/src/index.css, frontend/tailwind.config.js
Overhaul CSS token set, add brand-driven gradients/variables, new layout/typography primitives, animation renames; extend Tailwind fonts and add brand color palette using CSS variables.
UI primitives & components
frontend/src/components/BrandPrimitives.tsx, frontend/src/components/index.ts, frontend/src/components/Layout.tsx, frontend/src/components/SearchBar.tsx, frontend/src/components/*
Add PageHero, StatCard, SectionPanel, EmptyState, BrandOrnament, EntityHeroVisual; export them in barrel. Refactor Layout to shared NAV, use getDefaultLogo(theme), update header/footer and theme toggle. Make SearchBar variant-aware and update multiple smaller components (Loading, CopyButton, Error, Pagination).
Pages: apply primitives & behavior
frontend/src/pages/...
Replace many inline headers/cards with PageHero, StatCard, EmptyState, SectionPanel, and EntityHeroVisual. Update BlocksPage fresh-block highlighting logic; add WelcomePage metrics/chain-age fetch; adjust search/results/error empty states.
App & loader
frontend/src/App.tsx, frontend/src/components/Loading.tsx
Change route fallback UI to new card styling and update spinner/text styles.
Tests & small edits
frontend/src/utils/color.test.ts, various small frontend tweaks
Add color tests; multiple styling and markup updates across components and pages.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant App
    participant BrandingCtx as Branding Context
    participant ColorUtils as Color Utils
    participant DOM as Document/CSS
    participant UI as UI Component

    User->>App: start / load config & theme
    App->>BrandingCtx: initialize (config, theme)
    BrandingCtx->>ColorUtils: deriveBrandTokens(bgHex, accentHex, mode)
    ColorUtils->>ColorUtils: compute RGB triplets, mix accent, build gradients
    ColorUtils-->>BrandingCtx: DerivedBrandTokens
    BrandingCtx->>DOM: applyBrandTokens(tokens)
    DOM->>DOM: set --color-brand-*, --page-gradient, etc.
    App->>UI: render components (use CSS vars)
    UI-->>User: display branded UI
    Note over BrandingCtx,ColorUtils: on accent/theme change -> recalc + reapply
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Suggested reviewers

  • tac0turtle

Poem

🐰 I mixed the hues, I tuned the light,
I hopped through CSS by day and night,
Logos by theme, and tokens bright—
ARM64 joins the build tonight.
A tiny rabbit applauds this sight. 🎨✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: rebrand atlas frontend' is concise, specific, and directly describes the main change: a rebranding effort for the Atlas frontend UI using the new Evolve-inspired design direction.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pthmas/evolve-rebrand

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

pthmas added 3 commits April 20, 2026 17:17
# Conflicts:
#	frontend/src/components/Layout.tsx
#	frontend/src/components/SearchBar.tsx
#	frontend/src/components/index.ts
#	frontend/src/pages/TransactionDetailPage.tsx
@pthmas pthmas marked this pull request as ready for review April 21, 2026 11:45
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/index.css (1)

156-165: ⚠️ Potential issue | 🟠 Major

Overlapping placeholder rules can darken placeholder text on the default (light) theme.

Line 156–159 correctly targets dark mode only. Line 162–165 targets :root:not([data-theme='light']), which also matches the default :root state when no data-theme attribute is present yet (e.g., initial HTML before JS sets the attribute). Since :root here defines the light palette, that selector will render a white placeholder on light inputs during the brief pre-hydration window (and permanently for any deployment that forgets to set data-theme). Drop the broader :not([data-theme='light']) rule and rely on the explicit [data-theme='dark'] variant, or ensure the document always boots with data-theme="light" or "dark" set before first paint.

🛠️ Proposed fix
   :root[data-theme='dark'] input::placeholder,
   :root[data-theme='dark'] textarea::placeholder {
     color: rgb(255 255 255 / 0.72);
   }
-}
-
-  /* Match form input placeholder brightness to search bar in dark mode. */
-  :root:not([data-theme='light']) input::placeholder,
-  :root:not([data-theme='light']) textarea::placeholder {
-    color: rgb(255 255 255 / 0.72);
-  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/index.css` around lines 156 - 165, The overlapping placeholder
rules cause white placeholders on the default/light theme; remove or replace the
broader selectors `:root:not([data-theme='light']) input::placeholder` and
`:root:not([data-theme='light']) textarea::placeholder` so they only target dark
mode (i.e., use the existing `:root[data-theme='dark'] input::placeholder` /
`textarea::placeholder` selector), ensuring you either delete the
`:not([data-theme='light'])` block or change its selector to
`:root[data-theme='dark']` to avoid applying dark placeholder styles when no
data-theme is set.
🧹 Nitpick comments (12)
frontend/src/components/Pagination.tsx (1)

58-73: Optional: render the ellipsis as a non-interactive element.

The '...' separator is a disabled <button>, so assistive tech still announces it as a button. Consider rendering a <span> (with aria-hidden="true") for the ellipsis and keeping buttons only for real page targets. Purely a polish item; keyboard focus is already blocked via disabled.

♻️ Sketch
-        {getPageNumbers().map((page, index) => (
-          <button
-            key={index}
-            onClick={() => typeof page === 'number' && onPageChange(page)}
-            disabled={page === '...'}
-            className={`btn min-w-[40px] ${
-              page === currentPage
-                ? 'btn-primary'
-                : page === '...'
-                ? 'bg-transparent cursor-default text-fg-subtle'
-                : 'btn-secondary'
-            }`}
-          >
-            {page}
-          </button>
-        ))}
+        {getPageNumbers().map((page, index) =>
+          page === '...' ? (
+            <span
+              key={`gap-${index}`}
+              aria-hidden="true"
+              className="min-w-[40px] text-center text-fg-subtle"
+            >
+
+            </span>
+          ) : (
+            <button
+              key={page}
+              onClick={() => onPageChange(page)}
+              aria-current={page === currentPage ? 'page' : undefined}
+              className={`btn min-w-[40px] ${page === currentPage ? 'btn-primary' : 'btn-secondary'}`}
+            >
+              {page}
+            </button>
+          ),
+        )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/Pagination.tsx` around lines 58 - 73, The ellipsis
items from getPageNumbers() are currently rendered as disabled <button>s which
remain exposed to assistive tech; change the rendering so that when page ===
'...' you render a non-interactive <span> (e.g., <span
aria-hidden="true">...</span>) with the same visual classes used for the
disabled state, and only render <button>s for numeric pages that call
onPageChange(page) and use currentPage to determine the active class; update the
rendering logic in the component where getPageNumbers(), onPageChange, and
currentPage are used so the ellipsis is not a button and is marked aria-hidden.
frontend/src/pages/BlockTransactionsPage.tsx (1)

16-31: Avoid <br/> inside the hero title; use two-line layout via structure.

PageHero renders title inside an <h1 className="page-title">. A <br> inside a heading is valid HTML but is awkward for screen readers and responsive layouts — the line break is forced regardless of viewport. Consider a small span-based structure, or collapse to a single-line title like Block #{n} — Transactions.

♻️ Suggested tweak
-        title={
-          <>
-            Block #{formatNumber(blockNumber || 0)}
-            <br />
-            Transactions
-          </>
-        }
+        title={`Transactions in Block #${formatNumber(blockNumber || 0)}`}

Also note: when blockNumber is undefined the title falls back to #0, which would mis-render on an invalid route. Consider guarding against the missing param earlier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/BlockTransactionsPage.tsx` around lines 16 - 31, The hero
title uses a forced <br/> which hurts accessibility and responsiveness; change
the PageHero title so it renders semantic, multi-line content via structure or
punctuation (e.g., two inline spans or "Block #{...} — Transactions") instead of
a literal line break: update the title prop passed to PageHero (and the
EntityHeroVisual usage is fine) to use a split/span layout or single-line
separator and remove the <br/>; additionally, guard the blockNumber/formatNumber
usage by checking blockNumber is defined (e.g., early-return or conditional
render) so you don't render "Block `#0`" when the route param is missing,
referencing the blockNumber variable and formatNumber(...) call to locate the
code to change.
frontend/src/pages/AddressesPage.tsx (2)

215-269: Duplicated inline pagination instead of the shared Pagination component.

Other pages in this PR (e.g., TokensPage, NFTsPage, BlockTransactionsPage) continue to use the shared Pagination component, while AddressesPage now hand-rolls a first/prev/range/next/last control. This introduces drift: behavior/styling fixes to Pagination won't propagate here, and the range label logic (Lines 241–243) is easy to get wrong on the last page when addresses.length < limit.

If the goal was the pill-shaped container styling or the range readout, consider folding that into Pagination itself so all pages benefit, rather than diverging in this one page.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/AddressesPage.tsx` around lines 215 - 269, AddressesPage
contains a duplicated, hand-rolled pagination UI (the button group and range
label using addresses, pagination, setPage, formatNumber) instead of using the
shared Pagination component; replace the inline control in AddressesPage with
the existing Pagination component (or extend Pagination to support the
pill-shaped container and range readout) so styling/behavior is centralized,
ensure Pagination receives pagination, page, setPage, and addresses.length (or
limit) to compute the correct range display (fixing the last-page case where
addresses.length < limit), and remove the duplicated JSX/buttons so
TokensPage/NFTsPage/BlockTransactionsPage behavior stays consistent.

104-113: Minor a11y/UX: "Live On/Off" label loses iconography.

The previous SVG-based toggle conveyed state at a glance. Plain text "Live On" / "Live Off" is functional but lower affordance, and "Live Off" next to a primary/secondary button color swap is the main state signal now. Consider adding a small pulse dot or keeping an icon alongside the label for scanability. aria-pressed is correctly set, so a11y is fine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/AddressesPage.tsx` around lines 104 - 113, The Live On/Off
button currently uses plain text which reduces visual affordance; update the
button rendered in the AddressesPage so it includes a small decorative state
indicator (e.g., an inline SVG or a <span> dot) next to the text that reflects
autoRefresh (pulse/green when true, grey when false), keep the current text
label derived from autoRefresh, ensure the decorative icon has
aria-hidden="true" so screen readers still rely on aria-pressed, and use the
existing setAutoRefresh and autoRefresh identifiers to implement the conditional
classes/styles for the icon.
frontend/src/App.tsx (1)

27-29: Consider using the shared Loading spinner for route fallbacks.

A text-only kicker placeholder is a regression in perceived responsiveness compared to the rebranded Loading spinner already used elsewhere. Since this fallback is shown on every lazy route transition, a visual spinner is likely more consistent with the rest of the app.

♻️ Optional refactor
-    <div className="card flex h-64 items-center justify-center">
-      <span className="kicker">Loading route</span>
-    </div>
+    <div className="card flex h-64 items-center justify-center">
+      <Loading size="md" />
+    </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.tsx` around lines 27 - 29, Replace the text-only kicker
placeholder with the shared Loading spinner component: inside the JSX block that
renders the route fallback (the div with className "card flex h-64 items-center
justify-center"), remove the <span className="kicker">Loading route</span> and
render the Loading component instead (e.g., <Loading />), and add the
corresponding import for Loading at the top of the file; keep the existing
wrapper div and its classes so the spinner preserves the same sizing/alignment
for lazy route transitions.
frontend/src/pages/WelcomePage.tsx (1)

20-29: Optional: cancel the chain‑age fetch on unmount.

getBlockByNumber(1) is fired once on mount with no cancellation flag, so setChainAge can run after the component unmounts if navigation happens mid‑flight. A cancelled ref/flag (matching the pattern used in TransactionDetailPage.tsx lines 30–66) would make this consistent with the rest of the codebase.

♻️ Suggested pattern
-  useEffect(() => {
-    getBlockByNumber(1).then((block) => {
-      if (!block.timestamp) return;
-      const now = Math.floor(Date.now() / 1000);
-      const seconds = now - block.timestamp;
-      const days = Math.floor(seconds / 86400);
-      setChainAge(`${days} days`);
-    }).catch(() => {});
-  }, []);
+  useEffect(() => {
+    let cancelled = false;
+    getBlockByNumber(1)
+      .then((block) => {
+        if (cancelled || !block?.timestamp) return;
+        const now = Math.floor(Date.now() / 1000);
+        const days = Math.floor((now - block.timestamp) / 86400);
+        setChainAge(`${days} days`);
+      })
+      .catch(() => {});
+    return () => { cancelled = true; };
+  }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/WelcomePage.tsx` around lines 20 - 29, The effect calling
getBlockByNumber(1) can set state after unmount; update the useEffect in
WelcomePage.tsx to use the same cancellation pattern as TransactionDetailPage
(create a ref/flag like cancelledRef), set cancelledRef.current = false on mount
and true in the cleanup function, and before calling setChainAge check the flag
(and avoid state updates if cancelled); ensure the promise rejection handling
still swallows/errors appropriately but does not call setChainAge after unmount.
frontend/src/pages/StatusPage.tsx (2)

259-268: StatusStat wrapper is redundant.

StatusStat now just forwards label and value straight to StatCard with no additional behavior. Inline StatCard at the call sites (lines 103–108) and drop the StatusStat helper + its props interface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/StatusPage.tsx` around lines 259 - 268, Remove the
redundant StatusStat wrapper and its props interface StatusStatProps: replace
all uses of <StatusStat label={...} value={...} /> with direct <StatCard
label={...} value={...} /> calls (at the existing call sites) and delete the
StatusStat function and StatusStatProps interface declaration; ensure
imports/exports remain correct and run a quick typecheck to remove any leftover
references to StatusStat or StatusStatProps.

341-354: Dead branches in formatBucketTick.

WINDOWS only exposes '1h' | '6h' | '24h' | '7d' | '1m', so the '6m' and '1y' branches (and their entries in BUCKET_MS) are currently unreachable from this page. If these windows are intentionally reserved for a future UI, leave a comment; otherwise drop them to avoid confusing maintainers. Note that this is pre-existing and unchanged by the PR, so feel free to defer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/StatusPage.tsx` around lines 341 - 354, The function
formatBucketTick contains unreachable branches for '6m' and '1y' (and BUCKET_MS
entries) because ChartWindow only allows '1h' | '6h' | '24h' | '7d' | '1m';
remove the '6m' and '1y' cases (and corresponding BUCKET_MS entries) to
eliminate dead code, or if those window values are intentionally reserved, add a
clear comment near formatBucketTick (and BUCKET_MS) stating they are kept for
future UI support so maintainers know they are intentionally present.
frontend/src/pages/BlocksPage.tsx (2)

87-108: Fresh-block state update looks correct; one small tidy-up.

The add + auto-expire flow (build newlyPrepended, seed freshBlocks, schedule 1400ms timeouts with de-duped clearing of stale timers, and cleanup via freshBlockTimeoutsRef) is race-safe with the RAF batching above. One tidy-up: the per-block add loop (95–107) already accumulates into freshBlockTimeoutsRef, so the preceding setFreshBlocks (88–94) is redundant with the fact that the timeout will also call setFreshBlocks. It's fine as-is for immediate visual feedback, but worth a one-line comment explaining that 88–94 provides the initial "on" state while 95–107 schedules the "off" transition, to aid future readers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/BlocksPage.tsx` around lines 87 - 108, Add a one-line
clarifying comment above the setFreshBlocks call that seeds the immediate "on"
state for blocks (when handling newlyPrepended) and note that the subsequent
loop using freshBlockTimeoutsRef schedules the "off" transition and also
de-dupes/cleans up timers; reference the newlyPrepended loop, setFreshBlocks,
and freshBlockTimeoutsRef so future readers understand why both the immediate
state update and the timeout-based state removal are present.

113-135: Minor race between fresh-block reset RAF and a concurrent SSE prepend.

When page !== 1 || !autoRefresh, this effect clears pending per-block timeouts and schedules setFreshBlocks(new Set()) on the next frame. If autoRefresh is toggled off and then on again within the same frame (or a queued SSE event is already flushing), the setFreshBlocks(new Set()) from the reset path can land after new entries are added, briefly wiping freshly-prepended block highlights. Consider clearing freshBlocks synchronously here (no RAF needed, since it's only reached when fresh highlights should be off) to remove the race.

♻️ Suggested simplification
     if (page !== 1 || !autoRefresh) {
       bufferedDaBlocksRef.current = new Set();
       for (const [, timeoutId] of freshBlockTimeoutsRef.current) clearTimeout(timeoutId);
       freshBlockTimeoutsRef.current.clear();
-      freshBlocksResetRafRef.current = window.requestAnimationFrame(() => {
-        setFreshBlocks((prev) => (prev.size === 0 ? prev : new Set()));
-        freshBlocksResetRafRef.current = null;
-      });
+      setFreshBlocks((prev) => (prev.size === 0 ? prev : new Set()));
       return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/BlocksPage.tsx` around lines 113 - 135, The effect in
BlocksPage that runs when page !== 1 || !autoRefresh currently schedules
clearing fresh highlights via freshBlocksResetRafRef and requestAnimationFrame,
which can race with concurrent SSE prepends; instead cancel any pending RAF,
clear freshBlockTimeoutsRef.current, set bufferedDaBlocksRef.current = new
Set(), and call setFreshBlocks(() => new Set()) synchronously (and set
freshBlocksResetRafRef.current = null) so highlights are cleared immediately;
update the logic around freshBlocksResetRafRef, freshBlockTimeoutsRef,
bufferedDaBlocksRef, and setFreshBlocks inside the useEffect to remove the
RAF-based reset path.
frontend/src/index.css (1)

246-250: Ornament/hero overrides split awkwardly between dark and :not(dark) selectors.

:root[data-theme='dark'] .brand-ornament, :root[data-theme='dark'] .entity-hero-visual (246–250) and the light-mode counterpart :root:not([data-theme='dark']) .brand-ornament, :root:not([data-theme='dark']) .entity-hero-visual (407–413) set border-color and box-shadow to nearly identical values, with only the background differing. You can fold the shared declarations into the base .brand-ornament / .entity-hero-visual rules (283–288, 332–337) and only theme-override the background. This also avoids the same subtle bug as the placeholder rule: :not([data-theme='dark']) matches the attribute-less initial state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/index.css` around lines 246 - 250, The dark- and light-theme
rules for .brand-ornament and .entity-hero-visual duplicate border-color and
box-shadow; move the shared declarations (border-color, box-shadow) into the
base .brand-ornament and .entity-hero-visual rules and leave only the differing
background in the theme-specific blocks (currently :root[data-theme='dark']
.brand-ornament/.entity-hero-visual and :root:not([data-theme='dark'])
.brand-ornament/.entity-hero-visual), and replace the :not([data-theme='dark'])
selector with an explicit theme selector such as :root[data-theme='light'] or
only override background where needed to avoid matching the attribute-less
initial state.
frontend/src/components/Layout.tsx (1)

68-157: Duplicated nav rendering between desktop and mobile.

The NAV_ITEMS.map(...) block plus the conditional Faucet NavLink is rendered identically in both the desktop nav (lines 68–79) and the mobile nav (lines 146–157). Consider extracting a small inline component / render helper (or computing a single items array that optionally includes faucet) so there's one source of truth for link markup.

♻️ Proposed refactor
+  const navItems = faucet.enabled
+    ? [...NAV_ITEMS, { to: '/faucet', label: 'Faucet' }]
+    : NAV_ITEMS;
+
+  const renderNavLinks = () =>
+    navItems.map((item) => (
+      <NavLink key={item.to} to={item.to} className={navLinkClass}>
+        {item.label}
+      </NavLink>
+    ));

Then use {renderNavLinks()} inside both <nav> elements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/Layout.tsx` around lines 68 - 157, The desktop and
mobile nav both duplicate the NAV_ITEMS.map(...) + conditional Faucet NavLink;
extract that mapping into a single render helper (e.g., function
renderNavLinks() or const navLinks = computeNavLinks()) that uses NAV_ITEMS,
faucet.enabled and navLinkClass to produce the same set of <NavLink> elements,
then replace both inline NAV_ITEMS.map(...) blocks in Layout (the duplicated
blocks around nav elements) with a call to the helper so there is one source of
truth for link markup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/crates/atlas-server/src/api/handlers/contracts.rs`:
- Around line 399-412: The solc_binary_target function currently assumes
linux-arm64 binaries exist for all versions; update it to either accept a
compiler version and validate availability or perform a runtime availability
check against Solidity's list.json and return a clearer error: change fn
solc_binary_target(os: &str, arch: &str) -> Result<&'static str, AtlasError> to
include a version parameter (e.g., version: &str) or add a helper that
fetches/parses list.json, then for ("linux","aarch64") only return
Ok("linux-arm64") when the requested version is present (otherwise fall back to
"linux-amd64" or return AtlasError::Verification with a message that the
requested version is not available for ARM64), and update callers of
solc_binary_target accordingly so the Verification error distinguishes
platform-unsupported vs version-unavailable.

In `@backend/crates/atlas-server/src/main.rs`:
- Around line 120-123: Add a unit test in this file's #[cfg(test)] mod tests
that verifies the new empty-"dbname" branch: construct a connection URL like
"postgres://user:pass@host:5432/?dbname=" (and another case with a path DB name
present) then call the same parsing logic that sets database_name (referencing
the database_name variable/logic in main.rs) and assert that an empty dbname
query param is ignored (database_name stays unchanged or uses the path DB name
as appropriate). Make the test call the parsing/path-selection code exercised by
the match arm for "dbname" so it covers the branch and add assertions for both
the empty and non-empty dbname cases.

In `@frontend/src/components/BrandPrimitives.tsx`:
- Around line 129-142: BrandOrnament currently hardcodes the "Atlas" /
"Explorer" labels; update BrandOrnament to read the brand name from the
BrandingContext by importing and calling useBranding() (same pattern as
Layout.tsx) and render the returned chainName instead of the two static
spans—e.g., get const { chainName } = useBranding() inside BrandOrnament and
replace the hardcoded <span>Atlas</span><span>Explorer</span> with the chainName
rendered (if you need two-line styling, split chainName on whitespace or render
it in one element but preserve the existing className "brand-ornament__label").
Ensure the component still accepts the compact prop and keeps the same wrapper
classes.

In `@frontend/src/index.css`:
- Around line 705-731: Stylelint is failing due to camelCase keyframe names and
missing blank lines before declarations; rename the keyframes highlightFade,
livePulse, fadeInUp to kebab-case (highlight-fade, live-pulse, fade-in-up) and
update any references to those animations, and run the repo stylelint autofix
(e.g., bunx stylelint --fix) or manually add the required
declaration-empty-line-before blank lines and fix import/quote notation issues
flagged elsewhere in the file so the CSS conforms to the project's Stylelint
rules.
- Line 1: The CSS currently uses an external `@import` url(...) in
frontend/src/index.css which is render-blocking and leaks requests to Google;
replace this by self-hosting the font files (add woff2/woff assets to the repo)
and declare them with `@font-face` in frontend/src/index.css including
font-display: swap, updating the font-family usages to the same names, or if you
must keep remote fonts move the request into index.html using <link
rel="preconnect"> + <link rel="stylesheet"> instead of `@import`; ensure the new
approach points to local asset paths and that build/static asset pipeline serves
the fonts.

In `@frontend/src/pages/StatusPage.tsx`:
- Around line 284-287: Replace the hardcoded black spinner border so it remains
visible in dark mode: in the StatusPage.tsx loading JSX (the inner div with
className "w-5 h-5 border-2 border-black border-t-transparent rounded-full
animate-spin"), remove "border-black" and use a theme-aware approach such as
"border-current" and add a color token class like "text-fg" (or "text-fg-subtle"
if you prefer a subtler tone) on that spinner element (or its container) so the
spinner uses currentColor and is visible in both light and dark themes.

In `@frontend/src/pages/TransactionDetailPage.tsx`:
- Around line 131-142: The EmptyState is rendered before the PageHero causing
inconsistent layout with other detail pages; in TransactionDetailPage.tsx move
the PageHero (and its visual EntityHeroVisual) so it renders unconditionally
before the conditional EmptyState, then render EmptyState only when !txLoading
&& !transaction (using txError?.error for the description) to mirror
BlockDetailPage's order; ensure you keep the existing props (compact,
title="Transaction") on PageHero and leave the EmptyState conditional unchanged.

---

Outside diff comments:
In `@frontend/src/index.css`:
- Around line 156-165: The overlapping placeholder rules cause white
placeholders on the default/light theme; remove or replace the broader selectors
`:root:not([data-theme='light']) input::placeholder` and
`:root:not([data-theme='light']) textarea::placeholder` so they only target dark
mode (i.e., use the existing `:root[data-theme='dark'] input::placeholder` /
`textarea::placeholder` selector), ensuring you either delete the
`:not([data-theme='light'])` block or change its selector to
`:root[data-theme='dark']` to avoid applying dark placeholder styles when no
data-theme is set.

---

Nitpick comments:
In `@frontend/src/App.tsx`:
- Around line 27-29: Replace the text-only kicker placeholder with the shared
Loading spinner component: inside the JSX block that renders the route fallback
(the div with className "card flex h-64 items-center justify-center"), remove
the <span className="kicker">Loading route</span> and render the Loading
component instead (e.g., <Loading />), and add the corresponding import for
Loading at the top of the file; keep the existing wrapper div and its classes so
the spinner preserves the same sizing/alignment for lazy route transitions.

In `@frontend/src/components/Layout.tsx`:
- Around line 68-157: The desktop and mobile nav both duplicate the
NAV_ITEMS.map(...) + conditional Faucet NavLink; extract that mapping into a
single render helper (e.g., function renderNavLinks() or const navLinks =
computeNavLinks()) that uses NAV_ITEMS, faucet.enabled and navLinkClass to
produce the same set of <NavLink> elements, then replace both inline
NAV_ITEMS.map(...) blocks in Layout (the duplicated blocks around nav elements)
with a call to the helper so there is one source of truth for link markup.

In `@frontend/src/components/Pagination.tsx`:
- Around line 58-73: The ellipsis items from getPageNumbers() are currently
rendered as disabled <button>s which remain exposed to assistive tech; change
the rendering so that when page === '...' you render a non-interactive <span>
(e.g., <span aria-hidden="true">...</span>) with the same visual classes used
for the disabled state, and only render <button>s for numeric pages that call
onPageChange(page) and use currentPage to determine the active class; update the
rendering logic in the component where getPageNumbers(), onPageChange, and
currentPage are used so the ellipsis is not a button and is marked aria-hidden.

In `@frontend/src/index.css`:
- Around line 246-250: The dark- and light-theme rules for .brand-ornament and
.entity-hero-visual duplicate border-color and box-shadow; move the shared
declarations (border-color, box-shadow) into the base .brand-ornament and
.entity-hero-visual rules and leave only the differing background in the
theme-specific blocks (currently :root[data-theme='dark']
.brand-ornament/.entity-hero-visual and :root:not([data-theme='dark'])
.brand-ornament/.entity-hero-visual), and replace the :not([data-theme='dark'])
selector with an explicit theme selector such as :root[data-theme='light'] or
only override background where needed to avoid matching the attribute-less
initial state.

In `@frontend/src/pages/AddressesPage.tsx`:
- Around line 215-269: AddressesPage contains a duplicated, hand-rolled
pagination UI (the button group and range label using addresses, pagination,
setPage, formatNumber) instead of using the shared Pagination component; replace
the inline control in AddressesPage with the existing Pagination component (or
extend Pagination to support the pill-shaped container and range readout) so
styling/behavior is centralized, ensure Pagination receives pagination, page,
setPage, and addresses.length (or limit) to compute the correct range display
(fixing the last-page case where addresses.length < limit), and remove the
duplicated JSX/buttons so TokensPage/NFTsPage/BlockTransactionsPage behavior
stays consistent.
- Around line 104-113: The Live On/Off button currently uses plain text which
reduces visual affordance; update the button rendered in the AddressesPage so it
includes a small decorative state indicator (e.g., an inline SVG or a <span>
dot) next to the text that reflects autoRefresh (pulse/green when true, grey
when false), keep the current text label derived from autoRefresh, ensure the
decorative icon has aria-hidden="true" so screen readers still rely on
aria-pressed, and use the existing setAutoRefresh and autoRefresh identifiers to
implement the conditional classes/styles for the icon.

In `@frontend/src/pages/BlocksPage.tsx`:
- Around line 87-108: Add a one-line clarifying comment above the setFreshBlocks
call that seeds the immediate "on" state for blocks (when handling
newlyPrepended) and note that the subsequent loop using freshBlockTimeoutsRef
schedules the "off" transition and also de-dupes/cleans up timers; reference the
newlyPrepended loop, setFreshBlocks, and freshBlockTimeoutsRef so future readers
understand why both the immediate state update and the timeout-based state
removal are present.
- Around line 113-135: The effect in BlocksPage that runs when page !== 1 ||
!autoRefresh currently schedules clearing fresh highlights via
freshBlocksResetRafRef and requestAnimationFrame, which can race with concurrent
SSE prepends; instead cancel any pending RAF, clear
freshBlockTimeoutsRef.current, set bufferedDaBlocksRef.current = new Set(), and
call setFreshBlocks(() => new Set()) synchronously (and set
freshBlocksResetRafRef.current = null) so highlights are cleared immediately;
update the logic around freshBlocksResetRafRef, freshBlockTimeoutsRef,
bufferedDaBlocksRef, and setFreshBlocks inside the useEffect to remove the
RAF-based reset path.

In `@frontend/src/pages/BlockTransactionsPage.tsx`:
- Around line 16-31: The hero title uses a forced <br/> which hurts
accessibility and responsiveness; change the PageHero title so it renders
semantic, multi-line content via structure or punctuation (e.g., two inline
spans or "Block #{...} — Transactions") instead of a literal line break: update
the title prop passed to PageHero (and the EntityHeroVisual usage is fine) to
use a split/span layout or single-line separator and remove the <br/>;
additionally, guard the blockNumber/formatNumber usage by checking blockNumber
is defined (e.g., early-return or conditional render) so you don't render "Block
`#0`" when the route param is missing, referencing the blockNumber variable and
formatNumber(...) call to locate the code to change.

In `@frontend/src/pages/StatusPage.tsx`:
- Around line 259-268: Remove the redundant StatusStat wrapper and its props
interface StatusStatProps: replace all uses of <StatusStat label={...}
value={...} /> with direct <StatCard label={...} value={...} /> calls (at the
existing call sites) and delete the StatusStat function and StatusStatProps
interface declaration; ensure imports/exports remain correct and run a quick
typecheck to remove any leftover references to StatusStat or StatusStatProps.
- Around line 341-354: The function formatBucketTick contains unreachable
branches for '6m' and '1y' (and BUCKET_MS entries) because ChartWindow only
allows '1h' | '6h' | '24h' | '7d' | '1m'; remove the '6m' and '1y' cases (and
corresponding BUCKET_MS entries) to eliminate dead code, or if those window
values are intentionally reserved, add a clear comment near formatBucketTick
(and BUCKET_MS) stating they are kept for future UI support so maintainers know
they are intentionally present.

In `@frontend/src/pages/WelcomePage.tsx`:
- Around line 20-29: The effect calling getBlockByNumber(1) can set state after
unmount; update the useEffect in WelcomePage.tsx to use the same cancellation
pattern as TransactionDetailPage (create a ref/flag like cancelledRef), set
cancelledRef.current = false on mount and true in the cleanup function, and
before calling setChainAge check the flag (and avoid state updates if
cancelled); ensure the promise rejection handling still swallows/errors
appropriately but does not call setChainAge after unmount.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 98b358ab-d9cd-4080-a376-39d8a898c4ee

📥 Commits

Reviewing files that changed from the base of the PR and between cf9cabb and 31f1636.

⛔ Files ignored due to path filters (2)
  • frontend/src/assets/evolve-logo-light.svg is excluded by !**/*.svg
  • frontend/src/assets/evolve-logo.svg is excluded by !**/*.svg
📒 Files selected for processing (34)
  • .env.example
  • .github/workflows/ci.yml
  • backend/crates/atlas-server/src/api/handlers/contracts.rs
  • backend/crates/atlas-server/src/main.rs
  • docker-compose.yml
  • frontend/src/App.tsx
  • frontend/src/assets/defaultLogos.ts
  • frontend/src/components/BrandPrimitives.tsx
  • frontend/src/components/CopyButton.tsx
  • frontend/src/components/Error.tsx
  • frontend/src/components/Layout.tsx
  • frontend/src/components/Loading.tsx
  • frontend/src/components/Pagination.tsx
  • frontend/src/components/SearchBar.tsx
  • frontend/src/components/index.ts
  • frontend/src/context/BrandingContext.tsx
  • frontend/src/hooks/useChartColors.ts
  • frontend/src/index.css
  • frontend/src/pages/AddressesPage.tsx
  • frontend/src/pages/BlockDetailPage.tsx
  • frontend/src/pages/BlockTransactionsPage.tsx
  • frontend/src/pages/BlocksPage.tsx
  • frontend/src/pages/FaucetPage.tsx
  • frontend/src/pages/NFTsPage.tsx
  • frontend/src/pages/NotFoundPage.tsx
  • frontend/src/pages/SearchResultsPage.tsx
  • frontend/src/pages/StatusPage.tsx
  • frontend/src/pages/TokensPage.tsx
  • frontend/src/pages/TransactionDetailPage.tsx
  • frontend/src/pages/TransactionsPage.tsx
  • frontend/src/pages/WelcomePage.tsx
  • frontend/src/utils/color.test.ts
  • frontend/src/utils/color.ts
  • frontend/tailwind.config.js

Comment thread backend/crates/atlas-server/src/api/handlers/contracts.rs Outdated
Comment thread backend/crates/atlas-server/src/main.rs
Comment thread frontend/src/components/BrandPrimitives.tsx Outdated
Comment thread frontend/src/index.css Outdated
Comment thread frontend/src/index.css Outdated
Comment thread frontend/src/pages/StatusPage.tsx
Comment thread frontend/src/pages/TransactionDetailPage.tsx Outdated
@pthmas
Copy link
Copy Markdown
Collaborator Author

pthmas commented Apr 22, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
frontend/src/index.css (1)

156-165: Redundant dark-mode placeholder rule.

Lines 156–159 (:root[data-theme='dark'] input::placeholder, ...) are fully subsumed by the existing rule on lines 162–165 (:root:not([data-theme='light']) input::placeholder, ...) — they set the exact same color, and the "not light" selector already matches both data-theme='dark' and an unset theme. Drop one of the two to avoid duplicate declarations and keep the intent unambiguous.

♻️ Proposed cleanup
-  :root[data-theme='dark'] input::placeholder,
-  :root[data-theme='dark'] textarea::placeholder {
-    color: rgb(255 255 255 / 0.72);
-  }
-
   /* Match form input placeholder brightness to search bar in dark mode. */
   :root:not([data-theme='light']) input::placeholder,
   :root:not([data-theme='light']) textarea::placeholder {
     color: rgb(255 255 255 / 0.72);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/index.css` around lines 156 - 165, Remove the redundant
dark-mode placeholder rule: the selector ":root[data-theme='dark']
input::placeholder, :root[data-theme='dark'] textarea::placeholder" is fully
subsumed by the existing ":root:not([data-theme='light']) input::placeholder,
:root:not([data-theme='light']) textarea::placeholder" rule, so delete the
former block and keep only the ":root:not([data-theme='light']) ..." declaration
to avoid duplicate declarations and ambiguity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/index.css`:
- Around line 684-702: BlocksPage.tsx is applying a missing utility class
`animate-da-pulse` to status dots causing no animation; add a CSS utility
`.animate-da-pulse` that aliases the existing `live-pulse` animation (same
duration/easing/infinite) or change the four usages in BlocksPage.tsx to use the
existing `.live-dot` class instead—locate the class application in
BlocksPage.tsx and either (A) add `.animate-da-pulse { animation: live-pulse
1.6s ease-in-out infinite; }` to frontend/src/index.css alongside `.live-dot`
and the `@keyframes live-pulse`, or (B) replace `animate-da-pulse` with
`live-dot` in the JSX where the status indicator dots are rendered.

---

Nitpick comments:
In `@frontend/src/index.css`:
- Around line 156-165: Remove the redundant dark-mode placeholder rule: the
selector ":root[data-theme='dark'] input::placeholder, :root[data-theme='dark']
textarea::placeholder" is fully subsumed by the existing
":root:not([data-theme='light']) input::placeholder,
:root:not([data-theme='light']) textarea::placeholder" rule, so delete the
former block and keep only the ":root:not([data-theme='light']) ..." declaration
to avoid duplicate declarations and ambiguity.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c3653e89-f887-4b81-bed6-d587f1c03c51

📥 Commits

Reviewing files that changed from the base of the PR and between 31f1636 and 56e07a8.

📒 Files selected for processing (7)
  • backend/crates/atlas-server/src/api/handlers/contracts.rs
  • backend/crates/atlas-server/src/main.rs
  • frontend/src/components/BrandPrimitives.tsx
  • frontend/src/index.css
  • frontend/src/pages/StatusPage.tsx
  • frontend/src/pages/TransactionDetailPage.tsx
  • frontend/tailwind.config.js
✅ Files skipped from review due to trivial changes (1)
  • frontend/src/components/BrandPrimitives.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • backend/crates/atlas-server/src/main.rs
  • frontend/src/pages/TransactionDetailPage.tsx
  • backend/crates/atlas-server/src/api/handlers/contracts.rs
  • frontend/tailwind.config.js

Comment thread frontend/src/index.css
Comment on lines +684 to +702
.live-dot {
animation: live-pulse 1.6s ease-in-out infinite;
}

.row-highlight {
animation: highlight-fade 1.1s ease-out forwards;
}

.fresh-block {
background: color-mix(in srgb, rgb(var(--color-brand-aqua)) 10%, rgb(var(--color-surface-700)) 90%);
}

.fresh-block:hover {
background: color-mix(in srgb, rgb(var(--color-brand-aqua)) 14%, rgb(var(--color-surface-700)) 86%);
}

.fade-in-up {
animation: fade-in-up 340ms cubic-bezier(0.22, 1, 0.36, 1);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm animate-da-pulse / da-pulse references still exist and no corresponding class/keyframe is defined.
rg -nP '\banimate-da-pulse\b|\bda-pulse\b' --type=ts --type=tsx --type=css --type=html
echo '---- keyframes / class definitions in CSS ----'
rg -nP '@keyframes\s+(da-pulse|live-pulse|highlight-fade|fade-in-up)\b|\.animate-da-pulse\b|\.live-dot\b' --type=css

Repository: evstack/atlas

Length of output: 346


🏁 Script executed:

#!/bin/bash
# Find BlocksPage.tsx
find . -name "BlocksPage.tsx" -o -name "BlocksPage.ts" 2>/dev/null

# Search more broadly for animate-da-pulse references with different patterns
rg "animate-da-pulse" . 2>/dev/null || echo "No matches found for animate-da-pulse"
rg "da-pulse" . 2>/dev/null || echo "No matches found for da-pulse"

Repository: evstack/atlas

Length of output: 864


🏁 Script executed:

#!/bin/bash
# Let's check the CSS file to confirm the current state
cat -n frontend/src/index.css | sed -n '680,730p'

Repository: evstack/atlas

Length of output: 1379


Missing animate-da-pulse class breaks DA-inclusion flash on the Blocks page.

BlocksPage.tsx conditionally applies the animate-da-pulse class to status indicator dots (4 instances), but this utility is undefined in the CSS. No .animate-da-pulse class or corresponding keyframe exists.

Either (a) update BlocksPage to use an existing class (e.g. .live-dot), or (b) add the missing utility:

♻️ Proposed fix (add utility aliased to the live-pulse keyframe)
   .live-dot {
     animation: live-pulse 1.6s ease-in-out infinite;
   }
+
+  .animate-da-pulse {
+    animation: live-pulse 1.2s ease-in-out infinite;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/index.css` around lines 684 - 702, BlocksPage.tsx is applying a
missing utility class `animate-da-pulse` to status dots causing no animation;
add a CSS utility `.animate-da-pulse` that aliases the existing `live-pulse`
animation (same duration/easing/infinite) or change the four usages in
BlocksPage.tsx to use the existing `.live-dot` class instead—locate the class
application in BlocksPage.tsx and either (A) add `.animate-da-pulse { animation:
live-pulse 1.6s ease-in-out infinite; }` to frontend/src/index.css alongside
`.live-dot` and the `@keyframes live-pulse`, or (B) replace `animate-da-pulse`
with `live-dot` in the JSX where the status indicator dots are rendered.

@tac0turtle tac0turtle merged commit 5b47524 into main Apr 22, 2026
10 checks passed
@tac0turtle tac0turtle deleted the pthmas/evolve-rebrand branch April 22, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants