Skip to content

feat(portal): Make the World logo interactable - #2161

Open
SeanROlszewski wants to merge 18 commits into
mainfrom
feat/pixel-strip-hover-lens
Open

feat(portal): Make the World logo interactable#2161
SeanROlszewski wants to merge 18 commits into
mainfrom
feat/pixel-strip-hover-lens

Conversation

@SeanROlszewski

@SeanROlszewski SeanROlszewski commented Jul 28, 2026

Copy link
Copy Markdown

PR Type

  • Regular Task
  • Bug Fix
  • QA Tests

Description

Extends the homepage World logomark pixel field (BasePixelStrip) from a static halftone SVG into an interactive piece.

Screen.Recording.2026-07-31.at.11.14.31.mov
  • Pointer lens: nearby halftone dots scale up and lighten toward a soft warm gray as the cursor approaches, instead of a flat opacity dim.
  • App icon reveal: within a tight radius, real World App icons (fetched lazily from world-id-assets.com, zero upfront requests, and cached by URL so an icon reused across multiple cells only triggers one network request) develop in place of the dots on one continuous falloff - scale pops steeply near the cursor, while opacity/blur share a gentler falloff so neighbors fade/blur smoothly and z-order hand-offs between icons look continuous instead of snapping. Icons stay part of the grid permanently once revealed, settling to rest size (never re-blurring) when the cursor moves away.
  • Click ripple: pressing sends a wave expanding outward through the dot grid, blurring any icon it sweeps through. Releasing sends a second wave that's the literal time-reverse of the press wave - continuing seamlessly from wherever the forward wave currently is (not resetting to the outer edge, which used to look like a second wave arriving from nowhere) and playing out much faster, reading as a snap back rather than a slow mirror.
  • Press spring + negative-space push: clicking a revealed icon holds it near 3x its resting size (rather than shrinking away) so it stays the clearly most prominent thing on screen, while a radial push shoves its immediate neighbors out of the way so they don't crowd/cover it - it's fine for neighbors to look disorganized as a result. The pressed icon also shakes gently in place for as long as it's held.
  • Ambient pop-and-ring: every so often, an idle dot pops up, scales, and spins a couple of full turns - a passive invitation to interact. Each pop also sends out a slow, quiet ring to its neighbors (a soft scale pulse plus a lightening toward the same warm gray the lens uses), reading as a gentle ripple radiating outward from the spinning tile.
  • icon-manifest.ts provides 553 unique World App icons sourced from the public app-rankings API, mapped 1:1 to the pixel grid's 1,645 cells (each icon reused 2-3x, placements kept far enough apart that no two copies can ever appear in the same lens at once).
  • An icon preprocessing/refresh pipeline (scripts/normalize-app-icons.ts + .github/workflows/refresh-pixel-strip-icons.yml) fetches, normalizes to a consistent WebP, optionally uploads to S3, and patches the manifest in place on a weekly schedule - without disturbing the cell<->app index mapping - opening a PR rather than pushing directly so a broken upstream response can't silently reach production.
  • app/pixel-probe/ is a dev-only QA route that mounts BasePixelStrip directly, bypassing the auth/env setup the real homepage needs, so this can be reviewed/tested without a full local environment. The real homepage shows the same component at / for logged-out visitors - no special setup needed there either, since that render path never touches auth/GraphQL and its one network call (network stats) falls back to static values if it fails.

All of the above is layered as direct DOM style writes driven by requestAnimationFrame, not React re-renders/state, since ~1,645 SVG elements re-rendering per pointermove would be far more expensive than mutating a handful of style properties per frame.

Performance

Profiling (CDP traces with invalidation-reason attribution, rAF frame timing, JS self-time instrumentation, and CPU-throttled runs simulating mid/low-tier mobile) found the click/hold interaction dropping well below 60fps - driven by browser style-recalculation cost, not JS compute (applyLens itself only ever took 1-2ms at full speed):

  • Merged three separate full-grid render passes (lens, ripple, icon-blur wave) into one, instead of each walking all 1,645 cells independently.
  • Skip rewriting halftone dot styles - and the separate "settled icon" branch's styles - on frames where the pointer hasn't moved and no ripple is active, since both are pure functions of cursor position that don't need re-writing when nothing's actually changed. The settled-icon case in particular scaled with how much of the grid had already been explored, since revealed icons never un-reveal.
  • Narrowed the click ripple's band width so fewer dots are touched per frame (the actual dominant cost, per a CDP trace breakdown of invalidation reasons).
  • Skip redundant icon z-re-stacking (appendChild, which is a real DOM move even when the result is unchanged) when the stacking order hasn't actually changed frame to frame.
  • Cache icon loads by URL instead of by cell index, so an icon reused across 2-3 cells shares one network request/decode instead of each cell fetching independently.
  • The new ambient ring effect (see above) is built to stay cheap by construction despite pops now being frequent and concurrent: each pop's neighbor list is computed once via direct grid-lattice lookups, never a full-grid scan, and only the thin band of neighbors the wavefront is currently crossing gets written to on any given frame.

Median frame time during a held click/ripple is back to a steady 16.7ms (60fps) in local testing, down from routinely blowing past 20-33ms; idle ambient animation (pops + rings) holds the same flat 16.7ms with zero dropped frames even with several effects overlapping.

Security

Pinned third-party GitHub Actions in the icon-refresh workflow to verified commit SHAs instead of mutable tags, and validate app_id against a strict identifier pattern before it's used in any file path or S3 key - addressing CodeQL/Semgrep findings from initial review (supply-chain risk and untrusted network data written to file, respectively).

Verified end-to-end against both the live /pixel-probe route and the real homepage with Playwright throughout: zero icon requests before hover, correct lazy-fetch and dedup counts, ripple direction/timing (forward growing outward, reverse collapsing inward, seamless continuity when reversed mid-flight), press scale/push/shake behavior, ambient pop/ring timing and decay, clean state reset after every effect settles, and frame-timing measurements (including CPU-throttled) before/after each performance change.

Checklist

  • I have self-reviewed this PR.
  • I have left comments in the code for clarity.
  • I have added necessary unit tests.
  • I have updated the documentation as needed.

🤖 Generated with Claude Code

…rld logomark

Extends the homepage World logomark pixel field with a hover lens that
scales/lightens nearby halftone dots, progressively reveals real World App
icons (lazily fetched) as the cursor approaches, a depth-of-field blur ring,
a click-triggered ripple wave, and a spring press animation on click.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Icon preprocessing pipeline (fetch, normalize to a consistent 256x256 WebP,
optional S3 upload) plus a scheduled workflow to refresh existing icons
in place without disturbing the cell<->app index mapping. Also hardens
against icon-load retry storms, code-splits BasePixelStrip out of shared
chunks so unrelated routes stop paying for it, and gates the /pixel-probe
QA route to dev-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread .github/workflows/refresh-pixel-strip-icons.yml Fixed
Comment thread .github/workflows/refresh-pixel-strip-icons.yml Fixed
Comment thread .github/workflows/refresh-pixel-strip-icons.yml Fixed
Comment thread web/scripts/normalize-app-icons.ts Dismissed
Comment thread .github/workflows/refresh-pixel-strip-icons.yml Outdated
Comment thread .github/workflows/refresh-pixel-strip-icons.yml Outdated
Comment thread .github/workflows/refresh-pixel-strip-icons.yml Outdated
Comment thread .github/workflows/refresh-pixel-strip-icons.yml Outdated
Comment thread .github/workflows/refresh-pixel-strip-icons.yml Outdated
Pin all third-party GitHub Actions in the icon-refresh workflow to
verified commit SHAs instead of mutable tags (CodeQL + Semgrep:
supply-chain risk if a tag gets silently repointed). Also validate
app_id against a strict identifier pattern before it's used in any
file path or S3 key, rather than trusting the rankings API's shape
(CodeQL: untrusted network data written to file).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…bors

Replace the binary sharp-zone/blur-ring split with one continuous falloff:
scale still drops off steeply so the focused icon pops disproportionately,
but opacity and blur now share the same distance falloff at a gentler
power, so neighbors fade/blur smoothly instead of snapping between fully
opaque and hidden. This also makes z-order hand-offs between icons
naturally smooth (two icons trading the frontmost slot are now nearly
identical in opacity/blur at the crossover point), replacing an earlier
opacity-dip hack that didn't look right.

The click-triggered press spring now ripples outward to neighboring icons,
scaled down by distance from the pressed one, instead of only animating
the single pressed icon in isolation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SeanROlszewski and others added 4 commits July 30, 2026 19:35
…reverse ripple

The random-displacement shockwave caused FPS issues, so it's gone in favor
of a deterministic radial push (reusing state already computed for the
press pyramid effect) that clears space around the pressed icon instead.
The pressed icon now holds near 3x scale instead of shrinking away, shakes
in place while held, and release fires a second ripple that's the time-
reverse of the press ripple - collapsing back to the click point instead
of expanding - and runs much faster so it reads as a snap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Profiling (CDP trace + rAF frame timing) showed the lens/ripple/icon-wave
passes each walking the full 1,645-cell grid separately, plus rewriting
unchanged halftone-dot styles every frame during a stationary press, and
the ripple's band width, were driving browser style-recalc cost well past
frame budget while holding a click. Merges the three passes into one, skips
dot-style writes when the pointer hasn't moved, and narrows the ripple band
so fewer dots are touched per frame - median frame time during a held
click/ripple is back to a steady 16.7ms (60fps) in testing, down from
routinely blowing past 20-33ms.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…second wave

Releasing before the forward ripple finished animating used to fire an
independent reverse ripple starting fresh from the outer edge, while the
still-expanding original kept going - two rings on screen, reading as a
second wave arriving from the edge. Tracks the forward ripple by reference
and, on release, removes it and replaces it with a reverse ripple whose
start time is backdated to match its current radius/intensity exactly, so
it continues the same wave rather than resetting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A CDP invalidation trace showed appendChild-triggered layout invalidations
(remove + reinsert) as a real chunk of style-recalc cost, and the revealed-
icon z-stacking loop was re-appending every revealed icon every frame
unconditionally, regardless of whether its stacking order actually changed.
Tracks last frame's applied order and only re-stacks when it's different.

Verified z-order stays correct (most-zoomed icon still paints last) both
stationary and while moving the cursor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SeanROlszewski and others added 2 commits July 30, 2026 22:30
Every icon is reused 2-3x across the grid's 1,645 cells, but the loading
bookkeeping was keyed by cell index, not href - so a second cell sharing an
already-(or currently-)fetched icon still spun up its own new Image() and
network request. Caches the load promise by href instead, shared across the
page's lifetime, so every cell after the first for a given href reuses the
same in-flight or settled promise. A failed fetch is cached too, matching
the existing per-cell "don't retry a known-broken icon" behavior.

Verified live: hovering 3 cells that share the same icon now fires exactly
1 network request total instead of 3, while all 3 still display it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…hanged

The "settled, outside ICON_REVEAL_RADIUS but still inside LENS_RADIUS"
branch rewrote transform/opacity/filter/fillOpacity for every such icon on
every single frame unconditionally, even though every value it computes is
a pure function of (cell, pointer position, ripple state). Since revealed
icons never un-reveal, the number of cells that can land in this branch
grows with how much of the grid has been explored - so a fuller grid meant
more redundant per-frame writes, independent of clicking/holding. Skips the
whole branch when the pointer hasn't moved and no ripple is currently
affecting icons, reusing the pointerMoved/iconWaveActive checks already
computed for the raw dot styling.

Verified: settled icons still render correctly at rest, still catch ripple
blur while a wave is passing through, still clear cleanly afterward, and
the underlying dot dimming is still applied.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SeanROlszewski and others added 2 commits July 31, 2026 11:07
…ency

Each ambient "pop and spin" tile now sends out a slow, quiet ring to its
neighbors - a soft scale pulse plus a lightening toward the same warm gray
the lens/click-ripple already use - so it reads as a gentle ripple radiating
from the spinning tile. Kept cheap despite the slow/wide travel: each pop's
neighbor list is computed once via direct grid-lattice lookups
(findRingNeighbors/CELL_INDEX_BY_KEY), never a scan of all 1,645 cells, and
at any instant only the thin band of neighbors the wavefront is currently
crossing gets written to.

Also dialed back how many tiles pop concurrently (8 -> 4, slower interval)
so the new ring has room to be the calmer, more prominent motion instead of
competing with a lot of simultaneous rotation.

Verified live: the ring's leading/trailing edges grow steadily over a
multi-second window (not a quick snap), the lightened color is present,
concurrent rotating tiles dropped from ~2.9 avg to ~1.5 avg, and idle frame
timing stays a flat 16.7ms/60fps with multiple overlapping rings in flight.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cuts amplitude roughly in half again (1.5px -> 0.8px) and slows both axes'
frequencies to match, continuing the earlier dampening pass now that it's
been tried alongside the rest of the click/press feedback.

Verified live: tracking the specific pressed icon shows continued
oscillation at a visibly smaller peak-to-peak range than before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@SeanROlszewski SeanROlszewski changed the title feat(portal): interactive pointer lens + app icon reveal on World logomark feat(portal): Make the World logo interactable Jul 31, 2026
@SeanROlszewski
SeanROlszewski marked this pull request as ready for review July 31, 2026 18:17
@Gr1dlock

Gr1dlock commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fbd35eeb4d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread web/scripts/normalize-app-icons.ts Outdated
Comment on lines +250 to +253
Key: `${S3_PREFIX}/${fileName}`,
Body: normalized,
ContentType: "image/webp",
CacheControl: "public, max-age=31536000, immutable",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Version refreshed icons instead of overwriting live keys

In the scheduled refresh, every successful download is uploaded directly over the existing pixel-strip-icons/<appId>.webp object before create-pull-request runs. After the initial manifest migration, these stable URLs produce no tracked manifest change, so changed or corrupted bytes can reach the live asset bucket without any PR or CI review; moreover, the one-year immutable directive allows browsers and intermediary caches to continue serving the old bytes. Upload each refresh under versioned keys and change the manifest URLs in the reviewable PR, or otherwise stage the objects until approval.

Useful? React with 👍 / 👎.

Comment on lines +2704 to +2713
const progress = (now - pop.start) / POP_DURATION_MS;

if (progress >= 1) {
const rect = rectsRef.current.get(pop.key);

if (rect) {
rect.style.transform = "";
}

continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep pop rings alive through their full travel

Every pop is removed after POP_DURATION_MS (1100 ms), and only stillPopping entries drive the ring pass below. At POP_RING_SPEED = 0.025, the wavefront has traveled only 27.5 px when this removal occurs, despite POP_RING_RADIUS being 70 px (which requires 2800 ms), so most configured neighbors are never reached and any active ring is cleared prematurely. Track the ring until its radius plus fade duration has elapsed rather than tying its lifetime to the spin animation.

Useful? React with 👍 / 👎.

Comment on lines +2782 to +2789
for (const key of popRingActiveRef.current) {
if (!nextPopRingActive.has(key)) {
const rect = rectsRef.current.get(key);

if (rect) {
rect.style.transform = "";
rect.style.fill = "";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve styles when another effect claims a ring cell

When the pointer lens or a click ripple reaches a cell that was active in the previous pop ring, the ring loop correctly skips that claimed cell, but this cleanup then clears the transform and fill that the lens/ripple wrote earlier in the same frame. With a stationary pointer, the pointerMoved optimization prevents those lens styles from being restored on subsequent frames, leaving the cell visually unscaled until the pointer moves again. Only clear properties when the cell is not currently claimed, or perform this cleanup before the claiming effects write their styles.

Useful? React with 👍 / 👎.

Gr1dlock and others added 2 commits August 3, 2026 17:29
A pop's entry was retired as soon as its 1100ms spin finished, but the
ring it emits travels at POP_RING_SPEED and only reaches POP_RING_RADIUS
after 2800ms - so the wavefront was cut off 27.5px out of a configured
70px, and any ring still in flight was cleared early. Measured against
/pixel-probe, a ring reached at most 26.8px and lit 12.8 cells per pop;
it now reaches the full 70.0px and lights 40.4.

An entry's lifetime is now the union of its spin and its ring
(ringEndsAt, derived from the farthest real neighbor so a pop at the
edge of the logomark isn't kept alive waiting on a wavefront with
nothing left to reach). The spin hands its cell back once, on the frame
it settles, rather than rewriting the same empty transform for the
remaining ~2.4s. POP_MAX_CONCURRENT now counts spinning entries, since
it exists to bound simultaneous rotation - counting every live entry
would have starved pops to a third of the intended cadence.

Also stop the ring's cleanup pass from wiping styles it doesn't own. A
cell leaves the ring either because the wavefront passed (reset it) or
because the lens/reveal/ripple claimed it - and in that second case the
claiming pass already wrote the cell's transform/fill earlier in the
same frame, so clearing them there clobbered a live write. With the
pointer stationary the lens skips rewriting styles, so the cell stayed
visibly unscaled until the cursor moved again. A style-mutation trace
over an 18s pointer sweep recorded 4 such same-frame wipes before and 0
after.

Frame timing over 15s idle holds a 16.7ms median (p95 18.3ms, 3/900
frames over 20ms, none over 33ms) despite ~3x more ring cells lit per
pop; a moving pointer stays at a 16.7ms median with a slightly heavier
tail (40/1072 frames over 20ms, against 19/1082 before).

Co-Authored-By: Claude <noreply@anthropic.com>
The weekly refresh uploaded every icon straight over the existing
pixel-strip-icons/<appId>.webp object. Once the manifest points at those
stable URLs, a refresh produces no manifest diff at all, so the "opens a
PR so a human reviews it first" gate was vacuous - changed or corrupt
upstream bytes reached the live asset bucket the moment the workflow
ran. The one-year `immutable` Cache-Control made it worse: caches would
keep serving the old bytes anyway, so the overwrite carried the risk
without the effect.

Keys now carry a hash of the exact bytes stored, so an object is only
ever created, never rewritten with different content:

  - new bytes land under a key nothing references yet, and merging the
    manifest PR is what puts them in front of visitors
  - that PR always carries a visible diff, because the URL changes
    whenever the bytes do
  - reverting it is a complete rollback - the previous key is still
    there, untouched
  - `immutable` becomes truthful

An unchanged icon hashes to the key it already has, so the upload is
skipped (verified: a second --refresh-existing run over unchanged
upstream bytes leaves the manifest byte-identical, so no spurious PRs).
A HeadObject failure that isn't "missing" - credentials scoped to
PutObject only, say - warns once and falls through to uploading rather
than failing the refresh, which is safe precisely because the key is
derived from the content.

Note this hashes the normalized output, so a sharp/libvips upgrade that
re-encodes the same source differently will legitimately rotate every
key at once; the auto-PR body now says so, since it otherwise reads as
the "broken upstream response" signal it warns about.

Co-Authored-By: Claude <noreply@anthropic.com>
@Gr1dlock

Gr1dlock commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e2d2853567

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

ASSETS_S3_REGION: ${{ secrets.ASSETS_S3_REGION }}
ASSETS_S3_BUCKET_NAME: ${{ secrets.ASSETS_S3_BUCKET_NAME }}
run: |
npx tsx scripts/normalize-app-icons.ts \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pin tsx before executing the refresh

This workflow does not declare tsx in web/package.json or web/pnpm-lock.yaml, so after the frozen install, npx tsx resolves and installs a remote package at runtime—the npm exec documentation describes commands as coming from a local or remote package. Because this scheduled step runs after long-lived AWS credentials are configured, every refresh executes unpinned registry code with access to those credentials; add a pinned tsx dependency to the lockfile and invoke it through pnpm exec instead.

Useful? React with 👍 / 👎.

Comment on lines +3013 to +3014
svg.addEventListener("pointerdown", handlePointerDown);
svg.addEventListener("pointerup", handlePointerUp);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Capture pointer release outside the SVG

For a mouse/trackpad press that is dragged outside the logo before release, pointerup is dispatched outside this SVG because no pointer capture or window-level listener is installed. handlePointerLeave releases the icon spring but leaves pressRippleRef untouched, so the forward ripple completes without the promised reverse release wave and the stale reference remains until another in-SVG release; capture the pointer on pointerdown or listen for pointerup/pointercancel outside the SVG.

Useful? React with 👍 / 👎.

Gr1dlock and others added 2 commits August 3, 2026 17:49
Pressing on the logo and releasing after the cursor has moved off it sent
the pointerup to whatever element was under the cursor instead of the
SVG, so handlePointerUp never ran: the forward wave just faded out with
no reverse release wave, and pressRippleRef kept pointing at that
finished ripple - which the next in-SVG release then reversed, sending a
wave out from wherever the previous press had been. Capture the pointer
on pointerdown so the release always lands here, and treat pointercancel
(the browser taking the pointer back) as a release too.

Driven in Playwright against /pixel-probe with a transparent element
covering part of the SVG, standing in for the real homepage's copy laid
over the pixel field: pressing on the mark, waiting out the forward wave,
then releasing over that element lit 31 cells before (ambient pop rings
only - no release wave at all) and 504 after.

Also restore a hard bound on the live pop list. POP_MAX_CONCURRENT used
to double as one, but it now counts only spinning pops, and pops are
started by a timer while they're retired by the rAF loop - which is
paused for as long as the page sits in a background tab while that timer
keeps firing. POP_MAX_LIVE is derived from the ring's full lifetime and
the fastest start interval, so it can't bind during normal playback:
pop cadence, ring reach (70px) and frame timing are unchanged with it in
place (23 pops per 15s, median 16.7ms, p95 17.4ms).

Co-Authored-By: Claude <noreply@anthropic.com>
The scheduled refresh invoked `npx tsx`, and tsx wasn't declared
anywhere in web/package.json or the lockfile - so after the frozen
install, npx fell back to fetching it from the registry at runtime. That
step runs with the long-lived AWS credentials configured for the upload
already in its environment, which made every weekly run execute
freshly-downloaded, unpinned third-party code with access to them.

Declare tsx at an exact version so `--frozen-lockfile` pins it, and
invoke it (plus the prettier call the script shells out to) through
`pnpm exec`, which only ever resolves what's installed.

Co-Authored-By: Claude <noreply@anthropic.com>
@Gr1dlock

Gr1dlock commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a7d1531e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +27 to +28
logoUrl:
"https://world-id-assets.com/app_d2905e660b94ad24d6fc97816182ab35/da7a488a-d4b8-4f30-b5c6-be42df8de4cd.png",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow the manifest's icon host through every deployment CSP

When NEXT_PUBLIC_IMAGES_CDN_URL points to staging or any custom CDN, generateCsp() in web/proxy.ts permits only that configured hostname, while every newly added manifest entry requests world-id-assets.com. Those requests are therefore rejected by img-src, loadImage() records each cell as failed, and the icon-reveal feature silently shows only dots in that environment. Either generate URLs from the configured CDN or explicitly permit the manifest host.

Useful? React with 👍 / 👎.

Comment on lines +2868 to +2873
if (
ripplesRef.current.length > 0 ||
pressRef.current ||
popsRef.current.length > 0
) {
scheduleFrame();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop ambient frames when the hero is offscreen

After the first ambient pop, the 400–900 ms pop cadence is shorter than the roughly 3.5-second ring lifetime, so popsRef.current normally remains nonempty and this recursively schedules animation frames at display refresh rate for the rest of the page visit. Because there is no intersection/visibility guard, scrolling past the hero still runs the ring traversal and DOM style writes continuously for decoration that cannot be seen; pause the timer and frame loop while the component is outside the viewport.

Useful? React with 👍 / 👎.

Comment on lines +393 to +395
const base =
cdnBaseUrl ??
`https://${process.env.NEXT_PUBLIC_IMAGES_CDN_URL ?? "world-id-assets.com"}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve schemes when deriving the CDN base URL

When the documented --upload --write-manifest invocation omits --cdn-base-url, the normal environment format shown in web/.env.example is a complete URL such as https://your-cdn-url; prefixing it again produces https://https://your-cdn-url/... and writes unusable logoUrl values into the manifest. Normalize the configured value rather than unconditionally adding a scheme.

Useful? React with 👍 / 👎.

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.

4 participants