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
186 changes: 185 additions & 1 deletion src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,26 @@
.site-footer__inner {
display: flex;
justify-content: space-between;
gap: 24px;
gap: 16px 24px;
align-items: center;
/* QA v3-ER06: without `flex-wrap` the inner row used to
collide at viewport widths between the main mobile
breakpoint and ~1000px — the nav link cluster would
overflow onto the copy paragraph, producing a visual
overlap that the QA report flagged as a layout bug.
Allowing wrap lets the nav drop to its own row before
touching the copy; the mobile-stack breakpoint below
still collapses the columns for narrow screens. */
flex-wrap: wrap;
}

.site-footer__copy {
/* flex children default to `min-width: auto`, which means
long copy cannot shrink and forces the sibling nav to
overflow. Clamp to 0 so the text wraps gracefully once
horizontal space runs out. */
min-width: 0;
flex: 1 1 320px;
}

.site-footer__copy p {
Expand All @@ -189,13 +207,18 @@

.site-footer__copy span {
color: var(--muted);
display: block;
}

.site-footer__links {
display: flex;
flex-wrap: wrap;
gap: 16px;
color: var(--muted);
/* Ensure links cluster never splits across "copy" column
during a wrap — keep them together on the right edge. */
justify-content: flex-end;
flex: 0 1 auto;
}

.site-footer__links a:hover {
Expand Down Expand Up @@ -1612,6 +1635,19 @@
transition: border-color 120ms ease, box-shadow 120ms ease;
}

/* QA v1-SG10: the browser default placeholder color on
light backgrounds drifts toward ~#888 or lower depending
on UA. Pin to our darker --muted so placeholder text
still clears WCAG AA against .auth-input's light
surface; without this override the border + placeholder
can both sit below 4:1 on Safari. */
.auth-input::placeholder,
.proposal-wizard .form-field input::placeholder,
.proposal-wizard .form-field textarea::placeholder {
color: var(--muted);
opacity: 1;
}

.auth-input:focus {
outline: none;
border-color: var(--accent);
Expand Down Expand Up @@ -2686,6 +2722,17 @@
margin-top: 0;
}

.vote-modal__hint {
color: var(--muted);
font-size: 0.9rem;
margin: 8px 0 0;
line-height: 1.45;
}

.vote-modal__hint--warn {
color: #b45309;
}

.vote-modal__outcome {
border: 0;
padding: 0;
Expand Down Expand Up @@ -3625,6 +3672,69 @@
font-size: 0.9rem;
}

.proposal-wizard__help--warn {
color: #b45309;
}

.proposal-wizard__schedule {
border: 1px solid var(--panel-outline);
border-radius: 14px;
padding: 14px 16px;
background: var(--panel-strong);
display: flex;
flex-direction: column;
gap: 8px;
}

.proposal-wizard__schedule-heading {
margin: 0;
font-size: 1rem;
font-weight: 600;
}

.proposal-wizard__schedule-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 6px;
}

.proposal-wizard__schedule-list li {
display: grid;
grid-template-columns: 48px 1fr auto;
gap: 12px;
align-items: center;
padding: 8px 10px;
border-radius: 10px;
background: rgba(15, 23, 42, 0.04);
font-variant-numeric: tabular-nums;
}

.proposal-wizard__schedule-idx {
color: var(--muted);
font-weight: 600;
letter-spacing: 0.02em;
}

.proposal-wizard__schedule-amount {
text-align: right;
font-weight: 600;
}

@media (max-width: 480px) {
.proposal-wizard__schedule-list li {
grid-template-columns: 36px 1fr;
row-gap: 2px;
}
.proposal-wizard__schedule-amount {
grid-column: 2 / 3;
text-align: left;
color: var(--muted);
}
}

.proposal-wizard__cli {
background: #0f172a;
color: #e2e8f0;
Expand Down Expand Up @@ -3849,8 +3959,82 @@
margin: 0;
}

.proposal-status__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 6px;
}

.proposal-status__detail {
margin-top: 6px;
}

.proposal-status__detail > summary {
cursor: pointer;
color: var(--muted);
font-size: 0.9rem;
user-select: none;
}

.proposal-status__detail > summary:hover {
color: inherit;
}

.status-chip.is-info {
background: rgba(37, 99, 235, 0.15);
color: #1e40af;
border: 1px solid rgba(37, 99, 235, 0.3);
}

/* Session-expired banner
---------------------
Global, non-blocking strip shown between <Header> and the page
content when AuthContext.sessionExpired is true. Uses the warn
palette (amber) deliberately — it's not an error we can recover
from automatically, but it's not a hard failure either. The copy
+ actions live in the component; this file owns positioning,
surface, and the mobile stack. */
.session-expired-banner {
background: #fff7ed;
border-bottom: 1px solid rgba(180, 83, 9, 0.35);
color: #7c2d12;
}

.session-expired-banner__wrap {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 0;
flex-wrap: wrap;
}

.session-expired-banner__copy {
flex: 1 1 260px;
line-height: 1.45;
font-size: 0.95rem;
}

.session-expired-banner__actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}

@media (max-width: 640px) {
.session-expired-banner__wrap {
padding: 10px 0 12px;
align-items: flex-start;
flex-direction: column;
}

.session-expired-banner__actions {
width: 100%;
}

.session-expired-banner__actions > .button {
flex: 1 1 auto;
}
}

2 changes: 2 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Redirect, Route, Switch, useLocation } from 'react-router-dom';
import Header from './parts/Header';
import Footer from './parts/Footer';
import PrivateRoute from './parts/PrivateRoute';
import SessionExpiredBanner from './components/SessionExpiredBanner';

import Home from './pages/Home';
import Learn from './pages/Learn';
Expand Down Expand Up @@ -42,6 +43,7 @@ export default function App() {
<div className="site-shell">
<ScrollToTop />
<Header />
<SessionExpiredBanner />
<Switch>
<Route path="/" component={Home} exact />
<Route path="/network" component={Network} />
Expand Down
54 changes: 54 additions & 0 deletions src/components/GovernanceActivityLink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
import { Link } from 'react-router-dom';

// GovernanceActivityLink
// ----------------------
// Compact Account-page card that points the user at their recent
// governance vote history. The actual history lives in
// `GovernanceActivity` on the /governance page — that's the
// natural home because receipts are most useful next to the
// proposal feed they reference.
//
// Why the card exists here at all:
//
// The QA audit flagged that a user who opens "Account" to ask
// "how / where do I see my votes?" found no obvious path — the
// list lived two clicks away on Governance without any
// signpost from the Account screen. Surfacing a single
// deep-link card on Account closes that gap without
// duplicating the receipts widget (which would then have to
// refetch and potentially diverge from the canonical one).
//
// Keep this intentionally small — one heading, one CTA. No
// data fetch, no counts, no inline list. If we ever want those,
// move the full `<GovernanceActivity />` here behind a feature
// flag instead of sprouting a second implementation that can
// drift from the Governance-page one.
export default function GovernanceActivityLink() {
return (
<section
className="auth-card auth-card--info"
data-testid="account-gov-activity-link"
aria-labelledby="account-gov-activity-heading"
>
<h2
id="account-gov-activity-heading"
className="auth-card__title"
>
Your governance activity
</h2>
<p className="auth-card__hint">
Review every proposal you've voted on, with timestamps and a
jump link back to the proposal. Lives on the Governance page
right next to the feed — this is just a shortcut.
</p>
<Link
to="/governance"
className="button button--ghost"
data-testid="account-gov-activity-cta"
>
Open my governance activity
</Link>
</section>
);
}
42 changes: 42 additions & 0 deletions src/components/GovernanceActivityLink.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';

import GovernanceActivityLink from './GovernanceActivityLink';

// The Account-page deep-link card is a pure, stateless component —
// there's no data fetch, no conditional branches. What matters is:
//
// 1. It actually renders (so the Account page import doesn't
// turn into a ghost "blank card" if someone hollows this
// component out by mistake).
// 2. The CTA is a real router Link pointing at /governance.
// The QA gap being closed is "user on Account asked 'where
// do I see my votes?'" — if the Link regresses to /# or
// /governance?vote=... we break the exact fix.
describe('GovernanceActivityLink', () => {
function renderLink() {
return render(
<MemoryRouter>
<GovernanceActivityLink />
</MemoryRouter>
);
}

test('renders heading and hint copy', () => {
renderLink();
expect(
screen.getByTestId('account-gov-activity-link')
).toBeInTheDocument();
expect(
screen.getByRole('heading', { name: /your governance activity/i })
).toBeInTheDocument();
});

test('CTA is a real router link targeting /governance', () => {
renderLink();
const cta = screen.getByTestId('account-gov-activity-cta');
expect(cta.tagName).toBe('A');
expect(cta.getAttribute('href')).toBe('/governance');
});
});
Loading
Loading