Skip to content

feat(card): redesign launch share asset as a sticker collage#2274

Merged
Hugo0 merged 17 commits into
devfrom
feat/card-share-sticker-collage
Jun 29, 2026
Merged

feat(card): redesign launch share asset as a sticker collage#2274
Hugo0 merged 17 commits into
devfrom
feat/card-share-sticker-collage

Conversation

@0xkkonrad

@0xkkonrad 0xkkonrad commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Card-launch shareable assets. Started as the win-share sticker collage; now bundles the full share surface for the closed beta:

  1. Win share — sticker collage (force-directed layout): card centre, badges slapped around it, hero "I'M IN!" sticker, peanut.me/<handle> pill. Force-directed relaxation keeps badges from piling up; keep-outs reserve the rotated hero + the rendered pill; a final separation pass kills a bottom-right corner-trap (verified: worst centre-gap 0.22→0.75×size across a 7k-layout sweep).
  2. "Hide username" anti-dox toggle: a checkbox under the asset hides the pill (and badges reclaim the corner). Driven by visibility so toggling is instant and the hidden handle is excluded from the captured PNG.
  3. Win-caption rotation: the Share button posts a random line from a 16-caption pool (hype / invite / Devconnect callbacks / "shhhh" / anti-bank) — same line on the native share sheet and the desktop X intent.
  4. Berghain "not tonight" rejection (no-card-access waitlist path): a shareable door-rejection asset ("not tonight, " + smug peanut bouncer) with a "Tweet to appeal" share (random @joinpeanut caption) that doubles as the waitlist-join CTA; the friendly joined screen is the cooldown.

Risk

  • peanut-ui only, no backend / cross-repo change. Blast radius is the /card waitlist + waitlist-skip-celebration states and the history-replay drawer.
  • Replaces the old CardWaitlistScreen (removed) with CardRejectionScreen in the not-joined slot. applicants/admitted (213/7) are intentional static scarcity copy.

QA

  • 1498 unit tests pass (incl. a 160-seed layout no-heavy-overlap sweep + containment).
  • Verified live: hide-username toggles instantly and the real generated PNG excludes the handle; caption rotation produces distinct lines on the X intent; rejection screen renders.
  • Dev iterators: /dev/share-builder (collage + hide-username), /dev/rejection-builder (rejection screen).

Not in this PR

Replace the postage-stamp layout with a force-directed sticker collage —
the pixelated card sits centred, badges are placed by a repulsion-based
solver that fills the field without heavy overlap, and a hero "I'M IN!"
burst + a peanut.me/<handle> pill frame it. The card's peanut logo and
pixel hand stay uncovered via sticker-half-inflated keep-outs.

Why: the launch "I got in" share moment needed a louder, more on-brand
asset. The old stamp framing + EDITION/tier/points/stats chrome read as
cluttered, and the prior ring layout crowded the edges at high badge
counts. The look ships via component defaults, so existing surfaces
(BadgeSkipCelebration, CardUnlockDrawer) pick it up with no caller
changes. Also exposes /dev/share-builder as a dev-only public route
(prod still 404s) for iterating on the asset.
@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jun 26, 2026 5:53pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The PR adds a rejection-screen flow and reworks the share-asset builder, layout, and rendering pipeline around sticker placement, hero messages, username-pill styling, and share-text generation.

Changes

Share-asset sticker-collage refactor

Layer / File(s) Summary
Public types and layout contract
src/components/Card/share-asset/shareAsset.types.ts, src/components/Card/share-asset/shareAssetLayout.ts
Adds hero-message and username-pill types, extends ShareAssetD3Props, and introduces keep-out geometry plus the updated sticker placement result shape.
Sticker placement solver and tests
src/components/Card/share-asset/shareAssetLayout.ts, src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts
Replaces slot-based placement with seeded relaxation, keep-out constraints, and canvas clamping, and updates the placement tests to match the new sticker behavior.
ShareAssetD3 collage rendering
src/components/Card/share-asset/ShareAssetD3.tsx
Rebuilds ShareAssetD3 around card-face rendering, badge stickers, username-pill styling, optional hero stickers, and the new hero geometry and burst-shape helpers.
Card face and share privacy controls
src/components/Card/share-asset/PixelatedCardFace.tsx, src/components/Card/BadgeSkipCelebration.tsx, src/components/Card/CardUnlockDrawer.tsx, src/components/Card/share-asset/ShareAssetActions.tsx, src/components/Card/share-asset/share.utils.ts, src/components/Card/share-asset/winCaptions.ts
Adds an optional hideVisa prop to PixelatedCardFace, and adds hide-username controls plus mount-stable win captions and caller-provided Twitter share text in the share entry points.
Dev share-builder wiring
src/app/(mobile-ui)/dev/share-builder/page.tsx, src/constants/routes.ts, src/styles/globals.css
Reworks the /dev/share-builder page state and controls for hero messages, username-pill styling, badge presets, and username shortcuts, then wires the new props into ShareAssetD3, updates the preview JSON, registers the dev route, and removes the old global stamp styles.

Rejection screen flow

Layer / File(s) Summary
Rejection asset and captions
src/components/Card/share-asset/rejectionCaptions.ts, src/components/Card/share-asset/ScaledRejectionAsset.tsx, src/components/Card/share-asset/RejectionAssetD3.tsx
Adds the rejection caption pool, the scaled capture wrapper, and the D3 rejection asset with mascot, vignette, and headline rendering.
Rejection screen actions
src/components/Card/CardRejectionScreen.tsx, src/components/Card/CardEligibilityCheckScreen.tsx
Adds the rejection screen UI, share-to-appeal flow, waitlist join handler, analytics calls, and the eligibility-screen comment update that points to the new rejection screen.
Rejection builder page and card routing
src/app/(mobile-ui)/dev/rejection-builder/page.tsx, src/app/(mobile-ui)/card/page.tsx
Adds the dev rejection-builder page and switches the card page from the waitlist screen to the rejection screen in the not-joined branch.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Hugo0
  • jjramirezn
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% 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
Title check ✅ Passed The title clearly summarizes the main change: redesigning the launch share asset into a sticker collage.
Description check ✅ Passed The description is detailed and matches the changeset, covering the collage share asset, hide-username toggle, captions, and rejection flow.
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.


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.

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5764.4 → 5801.56 (+37.16)
Findings: +15 net (+52 new, -37 resolved)

🆕 New findings (52)

  • critical complexity — src/app/(mobile-ui)/card/page.tsx — CC 95, MI 57.44, SLOC 376
  • critical complexity — src/components/Card/share-asset/shareAssetLayout.ts — CC 55, MI 47.56, SLOC 258
  • critical complexity — src/app/(mobile-ui)/dev/share-builder/page.tsx — CC 51, MI 68.07, SLOC 199
  • high hotspot — src/app/(mobile-ui)/card/page.tsx — 36 commits, +1049/-455 lines since 6 months ago
  • high complexity — src/components/Card/share-asset/ShareAssetD3.tsx — CC 36, MI 49.87, SLOC 226
  • high complexity — src/components/Card/share-asset/PixelatedCardFace.tsx — CC 30, MI 57.63, SLOC 154
  • high complexity — src/components/Card/share-asset/RejectionAssetD3.tsx — CC 12, MI 47.24, SLOC 59
  • medium react-long-component — src/app/(mobile-ui)/dev/share-builder/page.tsx:28 — ShareBuilderPage is 385 lines — split it
  • medium high-mdd — src/app/(mobile-ui)/dev/share-builder/page.tsx:28 — ShareBuilderPage: MDD 223.0 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/card/page.tsx:53 — CardPage: MDD 104.3 (uses across many lines from declarations)
  • medium high-mdd — src/components/Card/BadgeSkipCelebration.tsx:61 — BadgeSkipCelebration: MDD 50.8 (uses across many lines from declarations)
  • medium high-mdd — src/components/Card/share-asset/RejectionAssetD3.tsx:56 — RejectionAssetD3: MDD 49.7 (uses across many lines from declarations)
  • medium high-mdd — src/components/Card/share-asset/shareAssetLayout.ts:155 — placeStamps: MDD 50.4 (uses across many lines from declarations)
  • medium structural-dup — components/Card/share-asset/shareAssetLayout.ts:194 — 43 duplicate lines / 346 tokens with components/Card/share-asset/shareAssetLayout.ts:266
  • medium high-mdd — src/components/Card/share-asset/ShareAssetD3.tsx:94 — ShareAssetD3: MDD 42.4 (uses across many lines from declarations)
  • medium structural-dup — app/(mobile-ui)/dev/rejection-builder/page.tsx:139 — 38 duplicate lines / 192 tokens with app/(mobile-ui)/dev/share-builder/page.tsx:407
  • medium high-mdd — src/components/Card/CardUnlockDrawer.tsx:33 — CardUnlockDrawer: MDD 37.0 (uses across many lines from declarations)
  • medium high-mdd — src/components/Card/CardRejectionScreen.tsx:41 — CardRejectionScreen: MDD 31.2 (uses across many lines from declarations)
  • medium method-complexity — src/components/Card/share-asset/shareAssetLayout.ts:155 — placeStamps CC 26 SLOC 128
  • medium high-mdd — src/components/Card/share-asset/ShareAssetD3.tsx:339 — HeroMessageEl: MDD 24.0 (uses across many lines from declarations)

…and 32 more.

✅ Resolved (37)

  • src/app/(mobile-ui)/card/page.tsx — CC 95, MI 57.45, SLOC 376
  • src/app/(mobile-ui)/dev/share-builder/page.tsx — CC 45, MI 67.49, SLOC 185
  • src/app/(mobile-ui)/card/page.tsx — 35 commits, +1036/-450 lines since 6 months ago
  • src/components/Card/share-asset/ShareAssetD3.tsx — CC 34, MI 52.34, SLOC 215
  • src/components/Card/share-asset/shareAssetLayout.ts — CC 33, MI 44.28, SLOC 340
  • src/app/(mobile-ui)/dev/share-builder/page.tsx:26 — ShareBuilderPage is 344 lines — split it
  • src/app/(mobile-ui)/dev/share-builder/page.tsx:26 — ShareBuilderPage: MDD 171.6 (uses across many lines from declarations)
  • src/app/(mobile-ui)/card/page.tsx:53 — CardPage: MDD 103.3 (uses across many lines from declarations)
  • src/components/Card/share-asset/ShareAssetD3.tsx:94 — ShareAssetD3: MDD 100.5 (uses across many lines from declarations)
  • src/components/Card/BadgeSkipCelebration.tsx:60 — BadgeSkipCelebration: MDD 49.5 (uses across many lines from declarations)
  • src/components/Card/CardUnlockDrawer.tsx:32 — CardUnlockDrawer: MDD 32.8 (uses across many lines from declarations)
  • src/components/Card/share-asset/PixelatedCardFace.tsx — CC 29, MI 57.76, SLOC 154
  • src/components/Card/share-asset/ShareAssetActions.tsx — CC 21, MI 50.31, SLOC 103
  • src/components/Card/share-asset/ShareAssetActions.tsx:75 — ShareAssetActions: MDD 21.2 (uses across many lines from declarations)
  • src/components/Card/BadgeSkipCelebration.tsx — CC 19, MI 56.95, SLOC 76
  • src/components/Card/share-asset/ShareAssetD3.tsx:94 — CC 16 SLOC 100
  • src/components/Card/share-asset/PixelatedCardFace.tsx:135 — direct DOM: document.createElement
  • src/components/Card/share-asset/PixelatedCardFace.tsx:152 — direct DOM: document.createElement
  • src/components/Card/share-asset/PixelatedCardFace.tsx:220 — direct DOM: document.createElement
  • src/components/Card/share-asset/PixelatedCardFace.tsx:77 — Use next/image

…and 17 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Card/share-asset/RejectionAssetD3.tsx 0.0 14.3 +14.3
src/components/Card/CardRejectionScreen.tsx 0.0 8.6 +8.6
src/components/Card/share-asset/ScaledRejectionAsset.tsx 0.0 6.8 +6.8
src/app/(mobile-ui)/dev/rejection-builder/page.tsx 0.0 4.1 +4.1
src/components/Card/share-asset/winCaptions.ts 0.0 3.6 +3.6
src/components/Card/share-asset/rejectionCaptions.ts 0.0 3.4 +3.4
src/components/Card/share-asset/shareAssetLayout.ts 11.6 12.6 +1.1
src/components/Card/share-asset/shareAsset.types.ts 0.6 1.3 +0.7
src/app/(mobile-ui)/dev/share-builder/page.tsx 8.3 8.8 +0.5
src/components/Card/share-asset/share.utils.ts 3.1 2.6 -0.5
src/components/Card/CardWaitlistScreen.tsx 7.1 0.0 -7.1

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1562 ran, 0 failed, 0 skipped, 23.6s

📊 Coverage (unit)

metric %
statements 53.8%
branches 36.4%
functions 40.9%
lines 53.6%
⏱ 10 slowest test cases
time test
3.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
0.5s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle too long for US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle invalid ETH address (invalid characters)
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claim in progress shows disabled button + progress
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claimed shows shake class + go home button
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ETH address with surrounding spaces
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/app/(mobile-ui)/dev/share-builder/page.tsx (1)

49-62: 🚀 Performance & Scalability | 🔵 Trivial

heroMessage and usernameStyle are fresh object refs each render, but their performance impact is minimal.

You memoized badgesArray so ShareAssetD3's layout memo doesn't re-run unnecessarily. However, heroMessage and usernameStyle are rebuilt every render. While this is inconsistent with your memoization pattern for badgesArray, the actual performance impact is limited: ShareAssetD3's heroGeo useMemo keys on extracted properties ([hero?.text, hero?.variant, hero?.scale]), not the object reference, so fresh heroMessage objects don't trigger re-runs. Similarly, usernameStyle is read but not part of any memo dependency. If you want consistency and defensiveness against future changes, memoizing these would be low-effort.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(mobile-ui)/dev/share-builder/page.tsx around lines 49 - 62, Memoize
both heroMessage and usernameStyle objects for consistency with the badgesArray
memoization pattern. Wrap the heroMessage object creation (the ternary that
returns either null or an object with text, variant, scale, and tilt) in a
useMemo hook with dependencies on heroVariant, heroText, heroScale, and
heroTilt. Similarly, wrap the usernameStyle object creation (which contains bg,
prefixRatio, scale, and letterSpacing properties) in a useMemo hook with
dependencies on unameBg, unamePrefix, unameScale, and unameTracking. This
ensures consistency with your memoization approach and defends against potential
future changes to component dependencies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/Card/share-asset/shareAsset.types.ts`:
- Around line 78-82: The JSDoc comments for the heroMessage and usernameStyle
properties in the shareAssetD3.types.ts interface file do not match the actual
component behavior. For the heroMessage property, the documentation incorrectly
states that omitted values mean no hero message, but the ShareAssetD3 component
treats undefined as the default hero and only null as no hero, so update the
comment to reflect that undefined (or omitted) uses the default hero while null
disables it. For the usernameStyle property, the documentation incorrectly
claims the default is pink with auto-fit sizing, but the actual component
default is white, so update the comment to correctly state the default color as
white while keeping any other relevant sizing details accurate.

In `@src/components/Card/share-asset/ShareAssetD3.tsx`:
- Around line 50-63: The heroGeometry function calculates dimensions that don't
match the actual rendered width of pill and banner stickers in HeroMessageEl,
causing the keep-out geometry to be inaccurate. Update the heroGeometry function
to calculate the width and height values that correspond to the actual padding
and content dimensions used when rendering the pill and banner variants, rather
than using the current formula that doesn't account for the actual rendered
width of these elements.

In `@src/components/Card/share-asset/shareAssetLayout.ts`:
- Around line 74-79: The PILL_KEEPOUT constant in the shareAssetLayout.ts file
only sets x0 to 690, which protects only the right side of the username pill.
Since the pill can render up to 780px wide from right: 56, its left edge extends
to approximately 364px, leaving the left portion unprotected from sticker
overlaps. Update the PILL_KEEPOUT object to extend its x0 value to approximately
364 (or the correct calculated left edge position) instead of 690 to ensure the
hitsPill collision detection covers the complete rendered width of the pill.
Additionally, verify that y0 is also properly sized to cover the top edge of the
pill's actual rendered footprint.

---

Nitpick comments:
In `@src/app/`(mobile-ui)/dev/share-builder/page.tsx:
- Around line 49-62: Memoize both heroMessage and usernameStyle objects for
consistency with the badgesArray memoization pattern. Wrap the heroMessage
object creation (the ternary that returns either null or an object with text,
variant, scale, and tilt) in a useMemo hook with dependencies on heroVariant,
heroText, heroScale, and heroTilt. Similarly, wrap the usernameStyle object
creation (which contains bg, prefixRatio, scale, and letterSpacing properties)
in a useMemo hook with dependencies on unameBg, unamePrefix, unameScale, and
unameTracking. This ensures consistency with your memoization approach and
defends against potential future changes to component dependencies.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d8637f6f-dbf2-44ec-b414-0e986abcf9cb

📥 Commits

Reviewing files that changed from the base of the PR and between d3eb9a3 and 7d295fc.

📒 Files selected for processing (8)
  • src/app/(mobile-ui)/dev/share-builder/page.tsx
  • src/components/Card/share-asset/PixelatedCardFace.tsx
  • src/components/Card/share-asset/ShareAssetD3.tsx
  • src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts
  • src/components/Card/share-asset/shareAsset.types.ts
  • src/components/Card/share-asset/shareAssetLayout.ts
  • src/constants/routes.ts
  • src/styles/globals.css
💤 Files with no reviewable changes (1)
  • src/styles/globals.css

Comment thread src/components/Card/share-asset/shareAsset.types.ts Outdated
Comment thread src/components/Card/share-asset/ShareAssetD3.tsx
Comment thread src/components/Card/share-asset/shareAssetLayout.ts
CodeRabbit review on #2274:
- Size the username-pill keep-out to the rendered pill (peanut.me/<handle>
  widens with the handle); a fixed x0 only guarded the right edge, so a
  sticker could land on a long handle. Pill box now computed from the
  rendered geometry and passed into placeStamps; add a regression test.
- Reserve the *rotated* hero bounding box so a tilted hero sticker's corners
  aren't covered; extract shared heroTilt() used by render + keep-out.
- Correct the heroMessage/usernameStyle prop docs (undefined=default hero,
  null=none; pill defaults to white).
@0xkkonrad 0xkkonrad marked this pull request as ready for review June 23, 2026 21:38
@0xkkonrad 0xkkonrad requested a review from Hugo0 June 23, 2026 21:38
Users who pass the eligibility hold but lack a card-access badge hit a
flat "you don't have the required badge :(" wall. Replace it with a
shareable door rejection that doubles as a growth loop: a "not tonight,
<username>" asset (smug peanut bouncer, scarcity tally as screen copy)
plus a primary "Tweet to appeal" share that attaches the asset and tags
@joinpeanut with a random caption. The secondary "Join the waitlist
anyway" still calls joinWaitlist, flipping the state machine to the
friendly joined screen as the post-share cooldown.

CardRejectionScreen owns the join itself (mirrors the old
CardWaitlistScreen contract: joinWaitlist + posthog + loading/error), so
the /card state machine drops it straight into the not-joined slot.
Removes the now-orphaned CardWaitlistScreen. Adds /dev/rejection-builder
to iterate on copy, the door tally, and the bouncer mascot.
react/no-unknown-property flags the styled-jsx `jsx` attr; scope a
disable to keep the new file lint-clean, matching ShareAssetD3's pattern.
handleAppeal logged + Sentry-captured a capture/native-share failure but
showed the user nothing — the spinner just stopped and no tweet went
out. The appeal is the whole point of the rejection screen, so fall back
to the text-only twitter intent on any non-abort error: the @joinpeanut
tag still goes out even if html-to-image or the OS share sheet fails.
@0xkkonrad

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

The keep-out-box approach still let stickers pile up / heavily overlap on
dense badge sets. Replace it with a force-directed relaxation: pairwise
repulsion between stickers, keep-clear ellipses around the hero + card,
and a pill keep-out with exit vectors, iterated to a stable spread. Drops
the now-unused pillKeepoutBox / PILL_RIGHT / PILL_BOTTOM exports and
updates the layout tests to assert the no-heavy-overlap invariant.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/Card/CardRejectionScreen.tsx`:
- Around line 95-116: The share flow in CardRejectionScreen should not treat a
missing captureRef node from ScaledRejectionAsset as a real share failure.
Update the handler around captureRef.current / captureShareAsset so the CTA is
disabled until useFitToWidth has mounted the capture node, or replace the thrown
error path with a retry/early-return state instead of entering the catch
fallback. Keep the AbortError behavior, but avoid falling through to the Twitter
intent fallback when the asset simply has not measured yet.

In `@src/components/Card/share-asset/RejectionAssetD3.tsx`:
- Around line 80-189: The exported canvas currently renders only the mascot and
rejection message, so the attribution/CTA is lost in image-only reposts. Update
RejectionAssetD3 to bake a persistent “@joinpeanut” mark into the JSX canvas
itself, alongside the existing headline and mascot, so it always appears in the
generated PNG regardless of where the asset is shared.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc0cac71-69cd-41e7-b681-57a202c84856

📥 Commits

Reviewing files that changed from the base of the PR and between 7d295fc and 57ab76f.

📒 Files selected for processing (11)
  • src/app/(mobile-ui)/card/page.tsx
  • src/app/(mobile-ui)/dev/rejection-builder/page.tsx
  • src/components/Card/CardEligibilityCheckScreen.tsx
  • src/components/Card/CardRejectionScreen.tsx
  • src/components/Card/CardWaitlistScreen.tsx
  • src/components/Card/share-asset/RejectionAssetD3.tsx
  • src/components/Card/share-asset/ScaledRejectionAsset.tsx
  • src/components/Card/share-asset/ShareAssetD3.tsx
  • src/components/Card/share-asset/rejectionCaptions.ts
  • src/components/Card/share-asset/shareAsset.types.ts
  • src/constants/routes.ts
💤 Files with no reviewable changes (1)
  • src/components/Card/CardWaitlistScreen.tsx
✅ Files skipped from review due to trivial changes (2)
  • src/components/Card/CardEligibilityCheckScreen.tsx
  • src/components/Card/share-asset/rejectionCaptions.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/Card/share-asset/shareAsset.types.ts
  • src/components/Card/share-asset/ShareAssetD3.tsx

Comment thread src/components/Card/CardRejectionScreen.tsx Outdated
Comment thread src/components/Card/share-asset/RejectionAssetD3.tsx
…ner-trap

Merging the force-directed engine onto the branch dropped two keep-out
refinements the earlier collage had, and a self-review found the engine
itself could still pile badges up:

- Rotated hero keep-out: reserve the *rotated* hero bounding box again
  (shared heroTilt() helper drives both the render and the keep-out, so
  they stay in lockstep) — badges no longer clip the tilted hero corners.
- Dynamic pill keep-out: size the bottom-right keep-out to the *rendered*
  username pill (pillKeepoutBox, threaded through placeStamps) instead of
  a static box, so badges clear the whole pill for long handles.
- Corner-trap: the soft edge + pill pulls could deadlock two stickers
  against the bottom-right corner (Gauss-Seidel local minimum) — across a
  7k-layout sweep the worst centre gap was 0.22×size, well into "heavy
  overlap". Add a final separation-only pass (separation + hard keep-outs
  + clamp, no edge/pill pull) so pairwise spread wins the last word; worst
  gap is now 0.75×size. Broaden the overlap test to a 160-seed sweep so
  the corner-trap regime is actually exercised.

Also: appeal share treats an unmeasured asset ref as a graceful text-only
fallback rather than a Sentry-logged error.
Some users don't want their peanut.me/<handle> on a public victory post.
Add a `hideUsername` prop to ShareAssetD3: when set, the username pill
doesn't render and its bottom-right keep-out is pushed off-canvas so the
badge collage reclaims that corner. Surfaced as a simple "Hide username"
checkbox directly beneath the asset (above the share buttons) on both
share surfaces — BadgeSkipCelebration and CardUnlockDrawer — and in the
/dev/share-builder iterator. The captured PNG honours the toggle, so a
hidden handle never leaks into the shared image.
Hiding then un-hiding the username left the pill invisible for ~2.3s: the
conditional unmount re-ran the pill's staggered entrance animation (600ms
fade + 1700ms delay) on every toggle, so it looked like the handle never
came back. Keep the pill mounted and drive the toggle with `visibility`
instead — the entrance plays once on the initial reveal, and toggling is
instant. `visibility: hidden` is also excluded from the html-to-image
capture, so a single capture-at-share-time still produces the correct PNG
(verified: with-handle vs no-handle) — no need to pre-render two images.
The win share posted one identical line ("I got my Peanut card. shhhh.")
for everyone. Add a win-caption pool (winCaptions.ts, Hugo's picks: hype
+ invite/FOMO + Devconnect callbacks + "shhhh" + anti-bank) and pick one
at random per mount in ShareAssetActions — used for both the native share
sheet and the desktop twitter intent (shareCardOnTwitter now takes the
caption), so the two paths post the same line. Drops the now-unused
shareText prop from the celebration + history-replay callers.
@0xkkonrad

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

Self-review caught one curly apostrophe (U+2019) in the 'can't talk' line
while the other 15 use straight ASCII; normalize so the tweet text reads
consistently.
@0xkkonrad

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

The file/caption comments promised the @joinpeanut tag was rendered into
the pixels so it survives an image-only re-post, but the asset only drew
the headline + mascot — the growth tag lived only in the share caption.
A user who re-posts just the PNG would carry no tag, silently breaking
the screen's entire referral rationale. Render the handle bottom-right.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/components/Card/share-asset/shareAssetLayout.ts (1)

118-138: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale magnitude in the stickerSize doc comment.

Line 120 says the fallback "eases down a 2300/count curve," but stickerSize uses Math.round(2700 / count). Align the comment with the actual constant to avoid confusing future maintainers (2700 also keeps continuity better: count 11 → 245 vs the table's 280 at count 10).

📝 Proposed doc fix
-// the repulsion packs them across the whole field (not a thin scatter). Past
-// the table it eases down a 2300/count curve with a legibility floor.
+// the repulsion packs them across the whole field (not a thin scatter). Past
+// the table it eases down a 2700/count curve with a legibility floor.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Card/share-asset/shareAssetLayout.ts` around lines 118 - 138,
The doc comment above STICKER_SIZE_BY_COUNT and stickerSize is out of sync with
the implementation: it mentions a 2300/count fallback, but stickerSize()
actually uses 2700/count. Update the comment so it matches the real fallback
curve and keeps the description aligned with the behavior in stickerSize,
preserving the existing tone about the size scaling and legibility floor.
src/app/(mobile-ui)/dev/share-builder/page.tsx (1)

393-405: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Include hideUsername in the "Resulting props" panel.

hideUsername is wired into <ShareAssetD3 /> (Line 363) and is one of the PR's headline features, but the props panel devs copy from to wire real surfaces omits it.

♻️ Add the missing field
                                     heroMessage,
                                     usernameStyle,
+                                    hideUsername,
                                     animate,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(mobile-ui)/dev/share-builder/page.tsx around lines 393 - 405, The
“Resulting props” panel in the share builder is missing the hideUsername field
even though ShareAssetD3 already consumes it. Update the JSON shown in the page
component so the object passed to JSON.stringify includes hideUsername alongside
username, badges, seedOverride, heroMessage, usernameStyle, and animate, using
the existing share-builder page render logic as the reference point.
src/components/Card/share-asset/ShareAssetD3.tsx (1)

259-278: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use PILL_MAX_W for the pill maxWidth to keep the documented lockstep.

Line 51 declares PILL_MAX_W = 780 and the comment states it is "Shared by the pill render and the layout keep-out estimate so they stay in lockstep," yet the render hardcodes maxWidth: 780. The keep-out memo (pillBox) does use PILL_MAX_W, so if the constant is ever changed the render and keep-out silently diverge.

♻️ Reference the shared constant
                         whiteSpace: 'nowrap',
-                        maxWidth: 780,
+                        maxWidth: PILL_MAX_W,
                         overflow: 'hidden',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Card/share-asset/ShareAssetD3.tsx` around lines 259 - 278, The
pill render in ShareAssetD3 is hardcoding the width instead of using the shared
limit, which can make it drift from the keep-out estimate. Update the span style
in ShareAssetD3 to use PILL_MAX_W for maxWidth so it stays in lockstep with
pillBox and the documented shared constant. Keep the change localized to the
pill render block and reference the existing PILL_MAX_W symbol rather than
repeating the numeric value.
src/components/Card/share-asset/ShareAssetActions.tsx (1)

102-106: 🚀 Performance & Scalability | 🔵 Trivial

Native share targets can ignore the caption when a file is attached. If caption consistency matters on mobile share sheets, keep the text-only Twitter intent fallback as the reliable path or render the caption into the image itself.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Card/share-asset/ShareAssetActions.tsx` around lines 102 -
106, The native sharing path in ShareAssetActions can drop the caption once a
file is attached, so don’t rely on navigator.share in the file-based flow.
Update the share logic around captureShareAsset and the filename/File creation
to keep the text-only Twitter intent fallback as the dependable caption path, or
ensure the caption is baked into the generated image before sharing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/Card/share-asset/RejectionAssetD3.tsx`:
- Around line 5-14: The docstring in RejectionAssetD3 contradicts the rendered
JSX by claiming the `@joinpeanut` handle is baked into the image, while the asset
only draws the mascot and “not tonight, <username>”. Update the comment near the
RejectionAssetD3 component to match the actual canvas output and state that the
handle is not included in the image. Keep the wording aligned with the
share-asset behavior so future edits don’t infer the PNG contains attribution.

In `@src/components/Card/share-asset/rejectionCaptions.ts`:
- Around line 9-10: Update the stale comment in rejectionCaptions so it matches
the current behavior of RejectionAssetD3: the `@joinpeanut` handle is no longer
baked into the asset image and only survives through the caption. Adjust the
docstring text near the asset-caption logic to remove the PNG repost claim and
accurately describe caption-only tagging, keeping the wording aligned with the
current implementation.

---

Nitpick comments:
In `@src/app/`(mobile-ui)/dev/share-builder/page.tsx:
- Around line 393-405: The “Resulting props” panel in the share builder is
missing the hideUsername field even though ShareAssetD3 already consumes it.
Update the JSON shown in the page component so the object passed to
JSON.stringify includes hideUsername alongside username, badges, seedOverride,
heroMessage, usernameStyle, and animate, using the existing share-builder page
render logic as the reference point.

In `@src/components/Card/share-asset/ShareAssetActions.tsx`:
- Around line 102-106: The native sharing path in ShareAssetActions can drop the
caption once a file is attached, so don’t rely on navigator.share in the
file-based flow. Update the share logic around captureShareAsset and the
filename/File creation to keep the text-only Twitter intent fallback as the
dependable caption path, or ensure the caption is baked into the generated image
before sharing.

In `@src/components/Card/share-asset/ShareAssetD3.tsx`:
- Around line 259-278: The pill render in ShareAssetD3 is hardcoding the width
instead of using the shared limit, which can make it drift from the keep-out
estimate. Update the span style in ShareAssetD3 to use PILL_MAX_W for maxWidth
so it stays in lockstep with pillBox and the documented shared constant. Keep
the change localized to the pill render block and reference the existing
PILL_MAX_W symbol rather than repeating the numeric value.

In `@src/components/Card/share-asset/shareAssetLayout.ts`:
- Around line 118-138: The doc comment above STICKER_SIZE_BY_COUNT and
stickerSize is out of sync with the implementation: it mentions a 2300/count
fallback, but stickerSize() actually uses 2700/count. Update the comment so it
matches the real fallback curve and keeps the description aligned with the
behavior in stickerSize, preserving the existing tone about the size scaling and
legibility floor.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 35d41f7f-e5ec-413d-975c-682701458969

📥 Commits

Reviewing files that changed from the base of the PR and between d3eb9a3 and 49e51e8.

📒 Files selected for processing (21)
  • src/app/(mobile-ui)/card/page.tsx
  • src/app/(mobile-ui)/dev/rejection-builder/page.tsx
  • src/app/(mobile-ui)/dev/share-builder/page.tsx
  • src/components/Card/BadgeSkipCelebration.tsx
  • src/components/Card/CardEligibilityCheckScreen.tsx
  • src/components/Card/CardRejectionScreen.tsx
  • src/components/Card/CardUnlockDrawer.tsx
  • src/components/Card/CardWaitlistScreen.tsx
  • src/components/Card/share-asset/PixelatedCardFace.tsx
  • src/components/Card/share-asset/RejectionAssetD3.tsx
  • src/components/Card/share-asset/ScaledRejectionAsset.tsx
  • src/components/Card/share-asset/ShareAssetActions.tsx
  • src/components/Card/share-asset/ShareAssetD3.tsx
  • src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts
  • src/components/Card/share-asset/rejectionCaptions.ts
  • src/components/Card/share-asset/share.utils.ts
  • src/components/Card/share-asset/shareAsset.types.ts
  • src/components/Card/share-asset/shareAssetLayout.ts
  • src/components/Card/share-asset/winCaptions.ts
  • src/constants/routes.ts
  • src/styles/globals.css
💤 Files with no reviewable changes (2)
  • src/styles/globals.css
  • src/components/Card/CardWaitlistScreen.tsx

Comment thread src/components/Card/share-asset/RejectionAssetD3.tsx Outdated
Comment thread src/components/Card/share-asset/rejectionCaptions.ts Outdated

@Hugo0 Hugo0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

lgtm. rec to make appeal join waitlist

- shareAssetLayout: doc comment said 2300/count, code uses 2700/count
- share-builder: surface hideUsername in the 'Resulting props' panel devs
  copy from (it's a headline feature, was omitted)
- ShareAssetD3: pill render now uses the shared PILL_MAX_W constant instead
  of a hardcoded 780, keeping render + keep-out estimate in lockstep
@0xkkonrad

Copy link
Copy Markdown
Contributor Author

CodeRabbit nitpick triage (commit 2c1bb95):

  • shareAssetLayout.ts — fixed stale 2300/count2700/count doc comment.
  • share-builder/page.tsx — added hideUsername to the "Resulting props" panel.
  • ShareAssetD3.tsx — pill maxWidth now uses the shared PILL_MAX_W constant (lockstep with the keep-out estimate).
  • ⏭️ ShareAssetActions.tsx (native share may drop caption when a file is attached) — declining. Passing text is best-effort; targets that honor it get the caption, those that don't show a clean share sheet for the user to type their own. Baking a random caption permanently into the branded PNG is undesirable, and the reliable-caption path (desktop Twitter intent) already exists as the fallback.

The two actionable docstring comments were resolved by baking the @joinpeanut handle into the asset (337a5e7) rather than deleting the claim — see thread replies.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

@0xkkonrad Thanks for the detailed triage! To summarize:

  • ✅ Resolved (3 items): stale 2300/count doc comment in shareAssetLayout.ts, missing hideUsername in the "Resulting props" panel, and PILL_MAX_W constant alignment in ShareAssetD3.tsx.
  • ⏭️ Declined (ShareAssetActions.tsx caption): Understood — the best-effort nature of text in the Web Share API is a known platform limitation, keeping the share sheet clean for user-typed captions is a valid UX choice, and the Twitter intent fallback already reliably delivers the caption on desktop.
  • @joinpeanut docstring comments: Noted as resolved via baking the handle into the asset in 337a5e7.

Kicking off the review now.

✅ Action performed

Review finished.

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.

…to-access)

The 'Tweet to appeal' CTA tweeted but left no in-app record — the appealer
wasn't on the waitlist, so the manual-grant workflow ('we let them in by
hand') had nothing to release from. Now appeal fires joinWaitlist() in
parallel with the share: drops them into the userId-keyed queue and flips
to the friendly cooldown after sharing. Joining is NOT access — release
stays manual (admin grant / grant-card-access), so no one gets auto-access.
PostHog tags the join source:'appeal' so appealers are distinguishable from
quiet 'Join anyway' joiners. onJoined() is deferred to finally so the
unmount can't race captureShareAsset's read of captureRef.
@Hugo0

Hugo0 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Pushed one commit to this branch (e2332cb0a) — flagging since it's your asset:

feat(card): appeal also joins the waitlist — Hugo's call. The "Tweet to appeal" CTA tweeted but left no in-app record, so the manual-grant workflow ("we let appealers in by hand") had nothing to release from — the appealer wasn't on the waitlist. Now appeal fires joinWaitlist() in parallel with the share:

  • Drops them into the userId-keyed waitlist queue + flips to the friendly cooldown after sharing.
  • Joining ≠ access — release stays 100% manual (admin grant / grant-card-access). No auto-access.
  • PostHog tags it card_waitlist_joined { source: 'appeal' } so appealers are distinguishable from quiet "Join anyway" joiners.
  • onJoined() deferred to finally so the unmount can't race captureShareAsset's read of captureRef.

Touches only handleAppeal in CardRejectionScreen.tsx (no overlap with your 3 latest commits). Local gate green: typecheck, prettier, eslint, layout+state tests (50/50). Revert if you'd rather own the change — just wanted the manual-grant queue wired before this ships.

… handle

The @joinpeanut tag belongs in the share text, not the pixels. Revert the
baked-in handle on the rejection asset (keep the image clean) and instead
ensure every shareable caption carries the handle: the rejection captions
already did, so add @joinpeanut to all 16 win captions. Now both win and
rejection shares tag the brand via the tweet text on every post.
@0xkkonrad

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

Punchier rejection lines (drop the trailing justifications), add a
'bouncer now??' jab and two Devconnect-callback rejections, plus an
Argentina/global callback in the win pool — all keep the @joinpeanut tag.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants