Skip to content

feat: centralize brand palette, strengthen funding meter, gate money routes, lazy-load Explore - #313

Open
wheval wants to merge 3 commits into
Heliobond:mainfrom
wheval:feat/issues-85-90-119-124
Open

feat: centralize brand palette, strengthen funding meter, gate money routes, lazy-load Explore#313
wheval wants to merge 3 commits into
Heliobond:mainfrom
wheval:feat/issues-85-90-119-124

Conversation

@wheval

@wheval wheval commented Jul 30, 2026

Copy link
Copy Markdown

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:

Where Values
src/brand/Helio.tsx #FFF4D6, #FFD451, #FFB400, #F59A00, #0B2B23, rgba(255,180,0,…)
src/brand/HelioWebGL.tsx #FFB400, #FFD451, #FFC633, #0B2B23, #FFF4D6
src/brand/Mark.tsx #FFB400

Together with src/styles/tokens/colors.css that left four copies of --solar free to drift apart.

Adds src/brand/palette.ts as 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 a var(--solar) string. Everything else in the app keeps reading the tokens, which remains the preferred route.

  • SOLAR mirrors --solar exactly; INK mirrors --ink.
  • The rest are the solar ramp the orb is built from, exported as SOLAR_RAMP so the SVG derives its stop list rather than restating it.
  • solarAlpha(a) builds the glow's rgba() 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.

Acceptance criterion — the orb colours derive from one source shared by both implementations. Both orbs and the mark now import from palette.ts. No brand hex literal remains in any component; the only surviving occurrence in the tree is one descriptive line in the HelioWebGL file-header comment.

Mark.tsx was 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:

  1. The meter exposed no value at all to assistive technology — it was a pair of unlabelled <div>s.
  2. The fill boundary was carried by hue alone, so in greyscale, under a colour-vision deficiency, or with forced colours on, the bar lost its meaning.

Now:

  • The track is a real role="progressbar" with aria-valuemin / aria-valuemax / aria-valuenow.
  • aria-valuetext carries the same sentence sighted users read, so the screen-reader announcement and the visible caption cannot drift apart.
  • aria-labelledby points at that caption, which picked up an id via useId().
  • The fill's leading edge is drawn in --ink when the value is strictly between 0 % and 100 %, giving the boundary a non-colour carrier.

Acceptance criterion — progress meaning is carried by text, not colour alone. The value is now available as text (visible caption + aria-valuetext), and the fill boundary is legible without perceiving the solar hue at all.

#119 · Gate the authenticated routes behind a wallet connection

/portfolio, /deposit and /withdraw rendered for anyone; only /connect redirected. 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.tsx and wraps the three routes. Two details make it correct rather than merely present:

1. It waits for rehydration. This is the subtle one. WalletProvider restores the session from localStorage inside an effect, so on the very first render connected is false even 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. WalletProvider now exposes a restoring flag, 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:

  • Only same-origin absolute paths are honoured as a return target. next arrives 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.com is rejected too.
  • The guard withholds the gated content itself rather than relying on the navigation winning the race, so balances never flash before the redirect lands.
  • router.replace, not push, so Back doesn't bounce between the gated route and Connect.

Each route wraps the guard in <Suspense>, matching the existing pattern in src/app/creator/page.tsx, because the guard reads useSearchParams.

Acceptance criterion — the money routes require a connection or send the user to Connect. All three now redirect, and the visitor lands back where they were headed.

#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.

  • Switching filter restarts at the first page, since a new filter is a new list — carrying the old depth across would reveal more of the new list than a first page should.
  • Progress (Showing 12 of 40 projects) is announced via aria-live="polite", so a screen-reader user learns the grid grew without the update stealing focus.
  • The control reports the true remainder on the final page (Show 2 more, not Show 12 more) and retires when nothing is left rather than sitting inert.
  • The API-failure fallback path paginates too, rather than dumping the bundled registry.

Acceptance criterion — the grid loads incrementally rather than all at once. 40 projects mount 12 at a time.

New strings were added to both en and fr; Explore namespace key parity is verified.


Verification

Check Result
npm run typecheck clean
npm run lint clean — 0 errors, 0 warnings
npm run test 2 failed | 181 passed (183)
upstream/main baseline 2 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 provided and AdminConsole.test.tsx > parses funded amounts with thousands separators. I confirmed them by checking out upstream/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 the restoring flag); 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 on main for five files this branch does not otherwise touch: FormField.tsx, components/index.ts, OracleForms.tsx, CreatorApplication.tsx, Portfolio.tsx.

format:check runs inside the same build job that gates every pull request, so that job was red on main regardless of what a contributor changes. I verified this by checking out upstream/main (4c18af8) and running prettier --check . there — the same five files fail, and this branch's own files were already clean.

The third commit is prettier --write output only — line wrapping, no semantic change — and it turns build green. 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.

build is now passing on this branch. The remaining Vercel check reports Authorization required to deploy, which is a fork-deployment permission on your side rather than anything in the diff.

… 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
Copilot AI review requested due to automatic review settings July 30, 2026 12:46
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

@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.

@drips-wave

drips-wave Bot commented Jul 30, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.ts as a single source of truth for literal brand colors needed by SVG/WebGL renderers.
  • Update Helio, HelioWebGL, and Mark to 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-labelledby points at this caption. Right now the caption renders {fundedPct}% and also calls t('dashGoalDeployed', { pct: fundedPct, ... }), but the dashGoalDeployed message already includes "{pct}%" (e.g. in messages/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.

Comment on lines +74 to +78
aria-valuenow={Math.min(100, fundedPct)}
aria-valuetext={t('dashGoalDeployed', {
pct: fundedPct,
goal: data.fundingGoal.toLocaleString('en-US'),
})}
Comment thread src/brand/Mark.tsx
Comment on lines +6 to +10
/**
* 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
Copilot AI review requested due to automatic review settings July 30, 2026 12:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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-effect suppression now only applies to setRestoring(false) in the early-return branch. The subsequent setAddress, setIsDemo, and setRestoring calls 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 getProjects from @/lib/api, but Explore.tsx imports 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 that Explore uses (or switch Explore to use the alias consistently).
vi.mock('@/lib/api', () => ({ getProjects: vi.fn() }))

@wheval
wheval marked this pull request as ready for review July 30, 2026 20:54
@wheval
wheval requested a review from dadadave80 as a code owner July 30, 2026 20:54
`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.
Copilot AI review requested due to automatic review settings July 30, 2026 20:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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%. If fundedPct is 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-valuenow should always be within [aria-valuemin, aria-valuemax]. If fundedPct ever 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', {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants