feat: centralize brand palette, strengthen funding meter, gate money routes, lazy-load Explore - #313
feat: centralize brand palette, strengthen funding meter, gate money routes, lazy-load Explore#313wheval wants to merge 3 commits into
Conversation
… meter Two related presentation fixes. Brand palette (Heliobond#85) The literal brand hexes were restated in three places — the SVG <Helio> gradient stops, the <HelioWebGL> module constants, and the solar dot in <Mark> — which duplicated the CSS colour tokens and left four copies of the same value free to drift apart. Adds src/brand/palette.ts as the single source for the values that cannot read a CSS custom property: the SVG builds gradient stops, and three.js needs a parsed colour rather than a var() string. SOLAR and INK mirror --solar and --ink exactly; the rest are the solar ramp the orb is built from. The SVG orb now derives its stop list from SOLAR_RAMP and its glow from solarAlpha(), so both orbs and the mark resolve to one definition. Funding meter (Heliobond#90) The creator funding bar was a bare solar fill. The percent text sat beside it, but the meter itself exposed no value to assistive technology and its fill boundary was carried by hue alone. The track is now a real progressbar with aria-valuenow/min/max, an aria-valuetext carrying the same sentence sighted users read, and aria-labelledby pointing at that caption. The fill's leading edge is drawn in ink, so the boundary stays legible in greyscale, under a colour-vision deficiency, and with forced colours on. Solar decorates the value; it no longer carries it alone. Closes Heliobond#85 Closes Heliobond#90
|
@wheval is attempting to deploy a commit to the David Dada's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@wheval Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
There was a problem hiding this comment.
Pull request overview
This PR consolidates hard-coded brand colors into a shared palette module for the Helio/Mark brand assets, and upgrades the creator dashboard funding meter to expose an accessible progressbar value (Issue #85 and #90).
Changes:
- Add
src/brand/palette.tsas a single source of truth for literal brand colors needed by SVG/WebGL renderers. - Update
Helio,HelioWebGL, andMarkto consume the shared palette instead of repeating hex literals. - Improve the creator funding meter by adding
role="progressbar"and ARIA value attributes, and drawing a non-color boundary on the fill.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/screens/creator/CreatorDashboard.tsx | Adds ARIA progressbar semantics and a non-color boundary for the funding meter. |
| src/brand/palette.ts | Introduces centralized brand palette constants/helpers for renderers that can’t read CSS vars. |
| src/brand/Mark.tsx | Replaces the solar dot hex literal with SOLAR from the palette. |
| src/brand/HelioWebGL.tsx | Switches WebGL orb colors to the shared palette constants. |
| src/brand/Helio.tsx | Derives SVG gradient stops and glow colors from the shared palette. |
Comments suppressed due to low confidence (1)
src/screens/creator/CreatorDashboard.tsx:105
- The progressbar’s
aria-labelledbypoints at this caption. Right now the caption renders{fundedPct}%and also callst('dashGoalDeployed', { pct: fundedPct, ... }), but thedashGoalDeployedmessage already includes "{pct}%" (e.g. inmessages/en.json). That will produce duplicated visible/screen-reader output like "30% 30% of your ...". Simplify the caption to render the translation once.
<div
id={fundingCaptionId}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| aria-valuenow={Math.min(100, fundedPct)} | ||
| aria-valuetext={t('dashGoalDeployed', { | ||
| pct: fundedPct, | ||
| goal: data.fundingGoal.toLocaleString('en-US'), | ||
| })} |
| /** | ||
| * The Heliobond analemma — a single continuous tilted figure-eight (the sun's | ||
| * year-long path), drawn in currentColor with the one permitted second colour: | ||
| * a solar dot. Upper loop subtly smaller, as in the real analemma. | ||
| */ |
…d in pages Wallet gate (Heliobond#119) /portfolio, /deposit and /withdraw rendered for anyone. Only /connect redirected. Those three screens assume a connection — they show balances or move value — so without one they render empty or misleading, and any action on them fails. Adds RequireWallet and wraps the three routes in it. Two details make it correct rather than merely present: · It waits for rehydration. The session is restored from localStorage inside an effect, so on the first render `connected` is false even for a user who is connected. Redirecting on that render would eject a connected user to Connect on every refresh. WalletProvider now exposes `restoring`, and the guard holds until it clears. · It preserves intent. The requested path, query string included, is carried to Connect as ?next=, and Connect returns the visitor there once connected instead of dropping them at the default stop. Only same-origin absolute paths are honoured, so a crafted ?next= cannot bounce a freshly connected wallet holder off-site. The guard withholds the gated content itself rather than relying on the navigation winning the race, so balances never flash before the redirect. Explore pagination (Heliobond#124) Explore rendered the entire registry in one pass — fine for the seed list, unbounded once the real registry lands. The grid now starts at 12 (whole rows at every breakpoint) and grows on demand. Switching filter restarts at the first page, since a new filter is a new list. Progress is announced politely as text, and the control retires when nothing is left rather than sitting inert. Adds 14 tests covering both, including the rehydration regression and the filter-change page reset. New strings added to en and fr; namespace key parity verified. Closes Heliobond#119 Closes Heliobond#124
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
src/brand/Mark.tsx:10
- The file header JSDoc block is duplicated verbatim, which makes the file harder to scan and looks like an accidental paste. Keep only a single header comment.
/**
* The Heliobond analemma — a single continuous tilted figure-eight (the sun's
* year-long path), drawn in currentColor with the one permitted second colour:
* a solar dot. Upper loop subtly smaller, as in the real analemma.
*/
/**
* The Heliobond analemma — a single continuous tilted figure-eight (the sun's
* year-long path), drawn in currentColor with the one permitted second colour:
* a solar dot. Upper loop subtly smaller, as in the real analemma.
*/
src/wallet/WalletProvider.tsx:105
- The
react-hooks/set-state-in-effectsuppression now only applies tosetRestoring(false)in the early-return branch. The subsequentsetAddress,setIsDemo, andsetRestoringcalls are still inside the effect and will be re-flagged by the same ESLint rule (this file previously suppressed it for the restore path). Consider disabling the rule for the whole restore block so lint stays consistent.
// The disable above covers this effect's set-state calls; the restore is
// intentionally a post-mount read of localStorage.
setAddress(saved)
setIsDemo(savedWallet === 'demo')
setRestoring(false)
src/screens/Explore.test.tsx:23
- This test mocks/imports
getProjectsfrom@/lib/api, butExplore.tsximports it via../lib/api. Vitest module mocks are keyed by the import specifier, so the component under test may not receive the mocked function (and could hit the real implementation). Mock and import the same module specifier thatExploreuses (or switchExploreto use the alias consistently).
vi.mock('@/lib/api', () => ({ getProjects: vi.fn() }))
`prettier --check .` fails on main for five files this branch does not otherwise touch: FormField.tsx, components/index.ts, OracleForms.tsx, CreatorApplication.tsx and Portfolio.tsx. `format:check` runs in the same `build` job that gates every pull request, so that job is red on main regardless of what a contributor changes — including this branch, whose own files are already clean. This is `prettier --write` output only: line wrapping, no semantic change. Kept as its own commit so it can be dropped or split out if a maintainer would rather fix it separately.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
src/brand/Mark.tsx:10
- The file now contains the same header docblock twice (lines 1–10). This duplication is unintentional noise and makes future edits error-prone (one copy may get updated while the other doesn’t).
/**
* The Heliobond analemma — a single continuous tilted figure-eight (the sun's
* year-long path), drawn in currentColor with the one permitted second colour:
* a solar dot. Upper loop subtly smaller, as in the real analemma.
*/
src/screens/creator/CreatorDashboard.tsx:92
- The progress fill width should be clamped to
0–100%. IffundedPctis negative,width: "-5%"is invalid CSS and can produce unpredictable rendering.
<div
style={{
width: `${Math.min(100, fundedPct)}%`,
height: '100%',
background: 'var(--solar)',
src/screens/creator/CreatorDashboard.tsx:74
aria-valuenowshould always be within[aria-valuemin, aria-valuemax]. IffundedPctever goes negative (bad/partial data), the current code will emit an invalid negative value for assistive tech.
This issue also appears on line 88 of the same file.
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.min(100, fundedPct)}
aria-valuetext={t('dashGoalDeployed', {
Closes #85
Closes #90
Closes #119
Closes #124
Four front-end fixes across the brand layer, the creator dashboard, the authenticated routes, and Explore.
#85 · Centralize the brand palette
The literal brand hexes were restated in three separate places:
src/brand/Helio.tsx#FFF4D6,#FFD451,#FFB400,#F59A00,#0B2B23,rgba(255,180,0,…)src/brand/HelioWebGL.tsx#FFB400,#FFD451,#FFC633,#0B2B23,#FFF4D6src/brand/Mark.tsx#FFB400Together with
src/styles/tokens/colors.cssthat left four copies of--solarfree to drift apart.Adds
src/brand/palette.tsas the one definition for the values that genuinely cannot read a CSS custom property — the SVG builds gradient stops, and three.js needs a parsed colour rather than avar(--solar)string. Everything else in the app keeps reading the tokens, which remains the preferred route.SOLARmirrors--solarexactly;INKmirrors--ink.SOLAR_RAMPso the SVG derives its stop list rather than restating it.solarAlpha(a)builds the glow'srgba()stops from one channel triplet.The ramp is deliberately theme-independent: the Helio is a fixed brand object and renders the same sun after sunset as it does at noon.
Mark.tsxwas not named in the issue, but it carried the fourth copy of#FFB400. Leaving it behind would have missed the point of the change, so it is included.#90 · Colour is never the only carrier, on the funding bar
The creator funding bar was a bare solar fill. The percent text sat beside it, but two things were still wrong:
<div>s.Now:
role="progressbar"witharia-valuemin/aria-valuemax/aria-valuenow.aria-valuetextcarries the same sentence sighted users read, so the screen-reader announcement and the visible caption cannot drift apart.aria-labelledbypoints at that caption, which picked up anidviauseId().--inkwhen the value is strictly between 0 % and 100 %, giving the boundary a non-colour carrier.#119 · Gate the authenticated routes behind a wallet connection
/portfolio,/depositand/withdrawrendered for anyone; only/connectredirected. All three assume a connection — they show balances or move value — so without one they render empty or misleading, and any action on them fails.Adds
src/wallet/RequireWallet.tsxand wraps the three routes. Two details make it correct rather than merely present:1. It waits for rehydration. This is the subtle one.
WalletProviderrestores the session fromlocalStorageinside an effect, so on the very first renderconnectedisfalseeven for a user who is connected. A naive guard would therefore eject a connected user to Connect on every page refresh — turning an access-control fix into a worse bug than the one it fixes.WalletProvidernow exposes arestoringflag, and the guard holds its decision until that clears.2. It preserves intent. The requested path — query string included — travels to Connect as
?next=, and Connect returns the visitor there once connected rather than dropping them at the default stop.Two smaller points:
nextarrives in the URL, so treating it as a bare redirect would let a crafted link bounce a freshly connected wallet holder off-site; protocol-relative//evil.comis rejected too.router.replace, notpush, so Back doesn't bounce between the gated route and Connect.Each route wraps the guard in
<Suspense>, matching the existing pattern insrc/app/creator/page.tsx, because the guard readsuseSearchParams.#124 · Lazy-load the projects grid
Explore rendered the entire registry in one pass — fine for the seed list, unbounded once the real registry lands.
The grid now starts at 12 and grows on demand. Twelve fills whole rows at every breakpoint (the grid runs 1–4 columns), so a page boundary never leaves a ragged half-row.
Showing 12 of 40 projects) is announced viaaria-live="polite", so a screen-reader user learns the grid grew without the update stealing focus.Show 2 more, notShow 12 more) and retires when nothing is left rather than sitting inert.New strings were added to both
enandfr;Explorenamespace key parity is verified.Verification
npm run typechecknpm run lintnpm run test2 failed | 181 passed (183)upstream/mainbaseline2 failed | 167 passed (169)Zero regressions. The 2 failures are pre-existing and untouched by this branch —
AmountInput.test.tsx > shows Max button when cap and localized label are providedandAdminConsole.test.tsx > parses funded amounts with thousands separators. I confirmed them by checking outupstream/main(4c18af8) directly and running the suite there: the same 2 fail, in the same 2 files. Neither file is touched by this branch.+14 tests, all green:
src/wallet/RequireWallet.test.tsx(7) — renders for a connected wallet; redirects when unconnected; never renders gated content to an unconnected visitor; waits for rehydration before deciding (the regression that motivated therestoringflag); preserves intent including query string; honours a custom target; shows the fallback while pending.src/screens/Explore.test.tsx(7) — first page only; grows a page at a time; offers only the remainder then retires; no control when everything fits; reports progress as text; paginates the API-failure fallback; restarts at page one when the filter changes.Happy to split any of the four into its own PR if you'd rather review them separately.
A note on the third commit — CI was red before this branch
prettier --check .fails onmainfor five files this branch does not otherwise touch:FormField.tsx,components/index.ts,OracleForms.tsx,CreatorApplication.tsx,Portfolio.tsx.format:checkruns inside the samebuildjob that gates every pull request, so that job was red onmainregardless of what a contributor changes. I verified this by checking outupstream/main(4c18af8) and runningprettier --check .there — the same five files fail, and this branch's own files were already clean.The third commit is
prettier --writeoutput only — line wrapping, no semantic change — and it turnsbuildgreen. It is deliberately a separate commit so you can drop it, or ask me to split it into its own PR, without disturbing the four fixes.buildis now passing on this branch. The remaining Vercel check reportsAuthorization required to deploy, which is a fork-deployment permission on your side rather than anything in the diff.