Skip to content

refactor(ui): extract the shared hero-card surface (#288) - #489

Merged
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card
Jul 31, 2026
Merged

refactor(ui): extract the shared hero-card surface (#288)#489
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

The warm gradient behind the sign-in and beta modals was inlined as a literal at five sites, and had already drifted at four of them. --surface-hero / --surface-hero-shadow sat in globals.css with zero consumers.

SiteRoleRadiusShadow
page.tsx:756beta success modal20rgba(15,23,42,0.15), no inset
page.tsx:785beta signup shell24rgba(15,23,42,0.12) + inset
page.tsx:819modal left panel (inner)background only
page.tsx:871modal right panel (inner)background only
SignInModal.tsx:226sign-in modal24rgba(15,23,42,0.18) + inset

What replaced it

  • .card--hero — a named variant of .card, used as class="card card--hero" (declared after .card so it wins).
  • .hero-surface — the gradient alone, for the two inner panels, which must not restate the parent's border/radius/shadow.
  • <HeroCard> — a thin wrapper that owns those two classes and forwards ref, className and every other prop. Callers keep their own layout and dialog semantics.

Both classes are now the tokens' only consumers.

This is a visible change, and an intended one

Adopting the token moves the shadow hue from cool slate rgba(15,23,42) to the warm rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and gives the beta success modal the 24px radius and inset highlight its two siblings already had. Before/after screenshots in a comment below.

On the literal fallbacks

var(--surface-hero, <literal>) in both rules is deliberate. The tokens are scoped to .public-surface, .landing-page. I verified every consumer sits inside that subtree today — SignInModal is mounted only from the landing page, and nothing here portals to document.body — so the fallback is defensive, not load-bearing. For a shared component it is the cheap guard against the first mount that isn't.

Deliberately not folded in

The close button and the logo/wordmark row. The close buttons are not duplicated verbatim after all — they differ in offset (18 vs 14) and only one carries a testid. The wordmark appears at 20 sites repo-wide with no shared component, so extracting it for 2 of them would leave a half-migration; #111 touches every icon site anyway, which is the coherent place for it.

Tests

HeroCard.test.tsx pins the class contract and prop/ref forwarding, and adds two anti-drift guards: no inlined copy of the gradient may exist anywhere in src/, and both CSS rules must keep a literal fallback (including the inset half, or token and fallback would render as two different surfaces).

data-testid="signin-modal" and "signin-close" are unchanged; no testid surface was added, so neither half of the registry needed updating.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 56 files, 403 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #288

@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bd63ae22-9f71-498c-a826-dc974e36b26e

📥 Commits

Reviewing files that changed from the base of the PR and between 9bbacc6 and 8e6d2b4.

📒 Files selected for processing (7)
  • docs/superpowers/followups/2026-06-30-token-unification-followups.md
  • frontend/src/app/(public)/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/globals.test.ts
  • frontend/src/components/HeroCard.test.tsx
  • frontend/src/components/HeroCard.tsx
  • frontend/src/components/SignInModal.tsx

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging8e6d2b4Commit Preview URL

Branch Preview URL
Jul 31 2026, 03:17 AM

AndresL230and others added 2 commits July 30, 2026 20:10
The warm gradient behind the sign-in and beta modals was inlined as a
literal at five sites, and had already drifted at four of them: three
different shadow alphas (0.12 / 0.15 / 0.18), two radii (20 / 24), and one
card missing the inset highlight entirely. Meanwhile --surface-hero and
--surface-hero-shadow sat in globals.css with zero consumers.
Adds `.card--hero` (a named variant of .card, used as `card card--hero`) and
`.hero-surface` (the gradient alone, for panels nested inside a hero card
that must not restate its border, radius or shadow), plus a thin <HeroCard>
wrapper that owns those classes and forwards everything else. Both classes
are now the tokens' only consumers.
This is a VISIBLE change, not a pure refactor, and an intended one: adopting
the token moves the shadow hue from cool slate rgba(15,23,42) to the warm
rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and
gives the beta success modal the 24px radius and inset highlight its two
siblings already had.
The literal fallbacks in both CSS rules are deliberate. The tokens are
scoped to `.public-surface, .landing-page`; every consumer sits inside that
subtree today (verified — SignInModal is mounted only from the landing page,
and nothing here portals to document.body), so the fallback is defensive
rather than load-bearing. For a shared component it is the cheap guard
against the first mount that isn't.
data-testid="signin-modal" and "signin-close" are unchanged.
Deliberately NOT folded in: the close button and the logo/wordmark row. The
close buttons are not actually duplicated verbatim — they differ in offset
(18 vs 14) and only one carries a testid — and the wordmark appears at 20
sites repo-wide with no shared component, so extracting it for 2 of them
would leave a half-migration. #111 touches every icon site anyway; that is
the coherent place for it.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tick [P1-E] in the token-unification follow-ups. This PR is what completes
it, and the repo's convention is to fold that tick into the same PR — #486
existed purely to backfill a tick missed the same way.
Name the shadow hue change for what it is. The comments described the
adoption as de-drifting "three shadow alphas, two radii, one missing inset",
which is accurate but incomplete in a misleading direction: the shadow's base
COLOUR was the one value that had not drifted — all five sites agreed on
slate rgba(15,23,42). The token is rgba(19,38,16) = --sap-900, the base the
app's other shadows use, so adopting it re-tints these shadows rather than
reconciling them. Deliberate, and called out in the PR body, but a reader of
the code would have assumed the colour was untouched. Now stated in
globals.css and HeroCard.tsx.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cturally (#288)
The review-fix commit closed the hero-surface block comment early: the `*/`
that used to end the whole comment stayed put while a new paragraph was
inserted above it, so the rest of the original comment became raw stylesheet
text. postcss reported it as "Unclosed string" at line 1007 — 780 lines below
the actual mistake, at the first quote it happened to reach.
It got that far because nothing in the fast lane reads this file as CSS.
eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet;
CI's `lint + tsc + vitest` job went green on the broken file. The first thing
that actually parses globals.css is the Next production build, which is why
the local e2e cycle caught it and everything cheaper did not.
Adds src/app/globals.test.ts: a dependency-free structural scan (comments,
strings, brace depth) that fails on exactly this class of breakage, in the
fast lane, pointing at the right line. Hand-rolled rather than importing
postcss on purpose — postcss is only a transitive dependency here and the
installed copy already drifts from CI's. It also tests itself: a second case
feeds it each broken shape to prove it does not silently return "fine".
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — green (and it caught a real break)

Final cycle at 8e6d2b4, one flock, SAPLING_MODEL_MODE=function:

35 passed (1.4m)
0 finding(s), 1 suppressed (allowlisted)

Gates: tsc --noEmit clean · npm run lint 0 errors · npx vitest run 58 files / 409 tests.

Worth flagging: the first cycle failed, and no cheaper gate could have caught it

The review-fix commit closed the hero-surface block comment early — the original */ stayed put while a new paragraph was inserted above it, so the tail of the comment became raw stylesheet text. make e2e-up died building the frontend:

CssSyntaxError: globals.css:1007:13: Unclosed string

780 lines below the actual mistake, at the first quote postcss happened to reach.

CI's lint + tsc + vitest job was green on that broken file. Nothing in the fast lane reads globals.css as CSS: eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet. The first thing that parses it is the Next production build.

So this PR also adds src/app/globals.test.ts — a dependency-free structural scan (comments, strings, brace depth) that fails in the fast lane and points at the right line. It's hand-rolled rather than a postcss import because postcss is only a transitive dependency here and the installed copy already drifts from CI's. It tests itself too: a second case feeds it each broken shape to prove it doesn't silently return "fine".

Rebase note

Rebased onto main after #488 landed. Both PRs touch globals.css; the two ordering invariants were re-verified after the merge — .card--hero (:242) still follows .card (:210), and .pending-* (:278) still precedes .anim-d* (:306).

@AndresL230
AndresL230 merged commit b335dfb into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated review pass (Claude Code), originally run against f8eb2ac and re-checked after the force-push to 8e6d2b4. Independently verified at the original head: extraction is genuinely complete (zero residual gradient literals outside the token/fallbacks/test constant), the PR body's drift table is accurate row-for-row against base, .card--hero overrides every .card property at equal-and-later specificity so card adoption is cascade-neutral, testids preserved, tsc clean + full vitest suite green. The e2e-cycle comment and the [P1-E] tick landed since — both good.

Still outstanding

  1. The PR body promises "Before/after screenshots in a comment below," but no comment contains any image. Since this PR declares a deliberate visible change (cool→warm shadow hue, unified alphas, success modal gaining radius+inset), the visual sign-off it leans on isn't actually on the record — post the screenshots or amend the body.

Minor

  1. HeroCard.test.tsx — the anti-drift walk still scans only .ts/.tsx (its own comment at ~:65 notes globals.css "is not walked (only .ts/.tsx)"). A future inlined copy of the gradient in a new.css file (e.g. a CSS module) would evade the guard. The legitimate fallback literals are an argument for excluding app/globals.css by path, not the whole .css extension.

Note for future edits:.card--hero currently overrides everything .card declares, so the pairing is purely nominal today — any future addition to .card (padding, hover elevation, transition) will silently restyle all three modals. Worth one sentence in the globals.css comment block.

Verdict: ready to merge once the screenshots are posted (or the body reference removed).

Sign up for freeto 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.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
refactor(ui): extract the shared hero-card surface (#288) by AndresL230 · Pull Request #489 · SaplingLearn/Sapling · GitHub
Skip to content

refactor(ui): extract the shared hero-card surface (#288) - #489

Merged
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card
Jul 31, 2026
Merged

refactor(ui): extract the shared hero-card surface (#288)#489
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

The warm gradient behind the sign-in and beta modals was inlined as a literal at five sites, and had already drifted at four of them. --surface-hero / --surface-hero-shadow sat in globals.css with zero consumers.

SiteRoleRadiusShadow
page.tsx:756beta success modal20rgba(15,23,42,0.15), no inset
page.tsx:785beta signup shell24rgba(15,23,42,0.12) + inset
page.tsx:819modal left panel (inner)background only
page.tsx:871modal right panel (inner)background only
SignInModal.tsx:226sign-in modal24rgba(15,23,42,0.18) + inset

What replaced it

  • .card--hero — a named variant of .card, used as class="card card--hero" (declared after .card so it wins).
  • .hero-surface — the gradient alone, for the two inner panels, which must not restate the parent's border/radius/shadow.
  • <HeroCard> — a thin wrapper that owns those two classes and forwards ref, className and every other prop. Callers keep their own layout and dialog semantics.

Both classes are now the tokens' only consumers.

This is a visible change, and an intended one

Adopting the token moves the shadow hue from cool slate rgba(15,23,42) to the warm rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and gives the beta success modal the 24px radius and inset highlight its two siblings already had. Before/after screenshots in a comment below.

On the literal fallbacks

var(--surface-hero, <literal>) in both rules is deliberate. The tokens are scoped to .public-surface, .landing-page. I verified every consumer sits inside that subtree today — SignInModal is mounted only from the landing page, and nothing here portals to document.body — so the fallback is defensive, not load-bearing. For a shared component it is the cheap guard against the first mount that isn't.

Deliberately not folded in

The close button and the logo/wordmark row. The close buttons are not duplicated verbatim after all — they differ in offset (18 vs 14) and only one carries a testid. The wordmark appears at 20 sites repo-wide with no shared component, so extracting it for 2 of them would leave a half-migration; #111 touches every icon site anyway, which is the coherent place for it.

Tests

HeroCard.test.tsx pins the class contract and prop/ref forwarding, and adds two anti-drift guards: no inlined copy of the gradient may exist anywhere in src/, and both CSS rules must keep a literal fallback (including the inset half, or token and fallback would render as two different surfaces).

data-testid="signin-modal" and "signin-close" are unchanged; no testid surface was added, so neither half of the registry needed updating.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 56 files, 403 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #288

@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bd63ae22-9f71-498c-a826-dc974e36b26e

📥 Commits

Reviewing files that changed from the base of the PR and between 9bbacc6 and 8e6d2b4.

📒 Files selected for processing (7)
  • docs/superpowers/followups/2026-06-30-token-unification-followups.md
  • frontend/src/app/(public)/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/globals.test.ts
  • frontend/src/components/HeroCard.test.tsx
  • frontend/src/components/HeroCard.tsx
  • frontend/src/components/SignInModal.tsx

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging8e6d2b4Commit Preview URL

Branch Preview URL
Jul 31 2026, 03:17 AM

AndresL230and others added 2 commits July 30, 2026 20:10
The warm gradient behind the sign-in and beta modals was inlined as a
literal at five sites, and had already drifted at four of them: three
different shadow alphas (0.12 / 0.15 / 0.18), two radii (20 / 24), and one
card missing the inset highlight entirely. Meanwhile --surface-hero and
--surface-hero-shadow sat in globals.css with zero consumers.
Adds `.card--hero` (a named variant of .card, used as `card card--hero`) and
`.hero-surface` (the gradient alone, for panels nested inside a hero card
that must not restate its border, radius or shadow), plus a thin <HeroCard>
wrapper that owns those classes and forwards everything else. Both classes
are now the tokens' only consumers.
This is a VISIBLE change, not a pure refactor, and an intended one: adopting
the token moves the shadow hue from cool slate rgba(15,23,42) to the warm
rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and
gives the beta success modal the 24px radius and inset highlight its two
siblings already had.
The literal fallbacks in both CSS rules are deliberate. The tokens are
scoped to `.public-surface, .landing-page`; every consumer sits inside that
subtree today (verified — SignInModal is mounted only from the landing page,
and nothing here portals to document.body), so the fallback is defensive
rather than load-bearing. For a shared component it is the cheap guard
against the first mount that isn't.
data-testid="signin-modal" and "signin-close" are unchanged.
Deliberately NOT folded in: the close button and the logo/wordmark row. The
close buttons are not actually duplicated verbatim — they differ in offset
(18 vs 14) and only one carries a testid — and the wordmark appears at 20
sites repo-wide with no shared component, so extracting it for 2 of them
would leave a half-migration. #111 touches every icon site anyway; that is
the coherent place for it.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tick [P1-E] in the token-unification follow-ups. This PR is what completes
it, and the repo's convention is to fold that tick into the same PR — #486
existed purely to backfill a tick missed the same way.
Name the shadow hue change for what it is. The comments described the
adoption as de-drifting "three shadow alphas, two radii, one missing inset",
which is accurate but incomplete in a misleading direction: the shadow's base
COLOUR was the one value that had not drifted — all five sites agreed on
slate rgba(15,23,42). The token is rgba(19,38,16) = --sap-900, the base the
app's other shadows use, so adopting it re-tints these shadows rather than
reconciling them. Deliberate, and called out in the PR body, but a reader of
the code would have assumed the colour was untouched. Now stated in
globals.css and HeroCard.tsx.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cturally (#288)
The review-fix commit closed the hero-surface block comment early: the `*/`
that used to end the whole comment stayed put while a new paragraph was
inserted above it, so the rest of the original comment became raw stylesheet
text. postcss reported it as "Unclosed string" at line 1007 — 780 lines below
the actual mistake, at the first quote it happened to reach.
It got that far because nothing in the fast lane reads this file as CSS.
eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet;
CI's `lint + tsc + vitest` job went green on the broken file. The first thing
that actually parses globals.css is the Next production build, which is why
the local e2e cycle caught it and everything cheaper did not.
Adds src/app/globals.test.ts: a dependency-free structural scan (comments,
strings, brace depth) that fails on exactly this class of breakage, in the
fast lane, pointing at the right line. Hand-rolled rather than importing
postcss on purpose — postcss is only a transitive dependency here and the
installed copy already drifts from CI's. It also tests itself: a second case
feeds it each broken shape to prove it does not silently return "fine".
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — green (and it caught a real break)

Final cycle at 8e6d2b4, one flock, SAPLING_MODEL_MODE=function:

35 passed (1.4m)
0 finding(s), 1 suppressed (allowlisted)

Gates: tsc --noEmit clean · npm run lint 0 errors · npx vitest run 58 files / 409 tests.

Worth flagging: the first cycle failed, and no cheaper gate could have caught it

The review-fix commit closed the hero-surface block comment early — the original */ stayed put while a new paragraph was inserted above it, so the tail of the comment became raw stylesheet text. make e2e-up died building the frontend:

CssSyntaxError: globals.css:1007:13: Unclosed string

780 lines below the actual mistake, at the first quote postcss happened to reach.

CI's lint + tsc + vitest job was green on that broken file. Nothing in the fast lane reads globals.css as CSS: eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet. The first thing that parses it is the Next production build.

So this PR also adds src/app/globals.test.ts — a dependency-free structural scan (comments, strings, brace depth) that fails in the fast lane and points at the right line. It's hand-rolled rather than a postcss import because postcss is only a transitive dependency here and the installed copy already drifts from CI's. It tests itself too: a second case feeds it each broken shape to prove it doesn't silently return "fine".

Rebase note

Rebased onto main after #488 landed. Both PRs touch globals.css; the two ordering invariants were re-verified after the merge — .card--hero (:242) still follows .card (:210), and .pending-* (:278) still precedes .anim-d* (:306).

@AndresL230
AndresL230 merged commit b335dfb into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated review pass (Claude Code), originally run against f8eb2ac and re-checked after the force-push to 8e6d2b4. Independently verified at the original head: extraction is genuinely complete (zero residual gradient literals outside the token/fallbacks/test constant), the PR body's drift table is accurate row-for-row against base, .card--hero overrides every .card property at equal-and-later specificity so card adoption is cascade-neutral, testids preserved, tsc clean + full vitest suite green. The e2e-cycle comment and the [P1-E] tick landed since — both good.

Still outstanding

  1. The PR body promises "Before/after screenshots in a comment below," but no comment contains any image. Since this PR declares a deliberate visible change (cool→warm shadow hue, unified alphas, success modal gaining radius+inset), the visual sign-off it leans on isn't actually on the record — post the screenshots or amend the body.

Minor

  1. HeroCard.test.tsx — the anti-drift walk still scans only .ts/.tsx (its own comment at ~:65 notes globals.css "is not walked (only .ts/.tsx)"). A future inlined copy of the gradient in a new.css file (e.g. a CSS module) would evade the guard. The legitimate fallback literals are an argument for excluding app/globals.css by path, not the whole .css extension.

Note for future edits:.card--hero currently overrides everything .card declares, so the pairing is purely nominal today — any future addition to .card (padding, hover elevation, transition) will silently restyle all three modals. Worth one sentence in the globals.css comment block.

Verdict: ready to merge once the screenshots are posted (or the body reference removed).

Sign up for freeto 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.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(ui): extract the shared hero-card surface (#288) by AndresL230 · Pull Request #489 · SaplingLearn/Sapling · GitHub
Skip to content

refactor(ui): extract the shared hero-card surface (#288) - #489

Merged
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card
Jul 31, 2026
Merged

refactor(ui): extract the shared hero-card surface (#288)#489
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

The warm gradient behind the sign-in and beta modals was inlined as a literal at five sites, and had already drifted at four of them. --surface-hero / --surface-hero-shadow sat in globals.css with zero consumers.

SiteRoleRadiusShadow
page.tsx:756beta success modal20rgba(15,23,42,0.15), no inset
page.tsx:785beta signup shell24rgba(15,23,42,0.12) + inset
page.tsx:819modal left panel (inner)background only
page.tsx:871modal right panel (inner)background only
SignInModal.tsx:226sign-in modal24rgba(15,23,42,0.18) + inset

What replaced it

  • .card--hero — a named variant of .card, used as class="card card--hero" (declared after .card so it wins).
  • .hero-surface — the gradient alone, for the two inner panels, which must not restate the parent's border/radius/shadow.
  • <HeroCard> — a thin wrapper that owns those two classes and forwards ref, className and every other prop. Callers keep their own layout and dialog semantics.

Both classes are now the tokens' only consumers.

This is a visible change, and an intended one

Adopting the token moves the shadow hue from cool slate rgba(15,23,42) to the warm rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and gives the beta success modal the 24px radius and inset highlight its two siblings already had. Before/after screenshots in a comment below.

On the literal fallbacks

var(--surface-hero, <literal>) in both rules is deliberate. The tokens are scoped to .public-surface, .landing-page. I verified every consumer sits inside that subtree today — SignInModal is mounted only from the landing page, and nothing here portals to document.body — so the fallback is defensive, not load-bearing. For a shared component it is the cheap guard against the first mount that isn't.

Deliberately not folded in

The close button and the logo/wordmark row. The close buttons are not duplicated verbatim after all — they differ in offset (18 vs 14) and only one carries a testid. The wordmark appears at 20 sites repo-wide with no shared component, so extracting it for 2 of them would leave a half-migration; #111 touches every icon site anyway, which is the coherent place for it.

Tests

HeroCard.test.tsx pins the class contract and prop/ref forwarding, and adds two anti-drift guards: no inlined copy of the gradient may exist anywhere in src/, and both CSS rules must keep a literal fallback (including the inset half, or token and fallback would render as two different surfaces).

data-testid="signin-modal" and "signin-close" are unchanged; no testid surface was added, so neither half of the registry needed updating.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 56 files, 403 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #288

@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bd63ae22-9f71-498c-a826-dc974e36b26e

📥 Commits

Reviewing files that changed from the base of the PR and between 9bbacc6 and 8e6d2b4.

📒 Files selected for processing (7)
  • docs/superpowers/followups/2026-06-30-token-unification-followups.md
  • frontend/src/app/(public)/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/globals.test.ts
  • frontend/src/components/HeroCard.test.tsx
  • frontend/src/components/HeroCard.tsx
  • frontend/src/components/SignInModal.tsx

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging8e6d2b4Commit Preview URL

Branch Preview URL
Jul 31 2026, 03:17 AM

AndresL230and others added 2 commits July 30, 2026 20:10
The warm gradient behind the sign-in and beta modals was inlined as a
literal at five sites, and had already drifted at four of them: three
different shadow alphas (0.12 / 0.15 / 0.18), two radii (20 / 24), and one
card missing the inset highlight entirely. Meanwhile --surface-hero and
--surface-hero-shadow sat in globals.css with zero consumers.
Adds `.card--hero` (a named variant of .card, used as `card card--hero`) and
`.hero-surface` (the gradient alone, for panels nested inside a hero card
that must not restate its border, radius or shadow), plus a thin <HeroCard>
wrapper that owns those classes and forwards everything else. Both classes
are now the tokens' only consumers.
This is a VISIBLE change, not a pure refactor, and an intended one: adopting
the token moves the shadow hue from cool slate rgba(15,23,42) to the warm
rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and
gives the beta success modal the 24px radius and inset highlight its two
siblings already had.
The literal fallbacks in both CSS rules are deliberate. The tokens are
scoped to `.public-surface, .landing-page`; every consumer sits inside that
subtree today (verified — SignInModal is mounted only from the landing page,
and nothing here portals to document.body), so the fallback is defensive
rather than load-bearing. For a shared component it is the cheap guard
against the first mount that isn't.
data-testid="signin-modal" and "signin-close" are unchanged.
Deliberately NOT folded in: the close button and the logo/wordmark row. The
close buttons are not actually duplicated verbatim — they differ in offset
(18 vs 14) and only one carries a testid — and the wordmark appears at 20
sites repo-wide with no shared component, so extracting it for 2 of them
would leave a half-migration. #111 touches every icon site anyway; that is
the coherent place for it.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tick [P1-E] in the token-unification follow-ups. This PR is what completes
it, and the repo's convention is to fold that tick into the same PR — #486
existed purely to backfill a tick missed the same way.
Name the shadow hue change for what it is. The comments described the
adoption as de-drifting "three shadow alphas, two radii, one missing inset",
which is accurate but incomplete in a misleading direction: the shadow's base
COLOUR was the one value that had not drifted — all five sites agreed on
slate rgba(15,23,42). The token is rgba(19,38,16) = --sap-900, the base the
app's other shadows use, so adopting it re-tints these shadows rather than
reconciling them. Deliberate, and called out in the PR body, but a reader of
the code would have assumed the colour was untouched. Now stated in
globals.css and HeroCard.tsx.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cturally (#288)
The review-fix commit closed the hero-surface block comment early: the `*/`
that used to end the whole comment stayed put while a new paragraph was
inserted above it, so the rest of the original comment became raw stylesheet
text. postcss reported it as "Unclosed string" at line 1007 — 780 lines below
the actual mistake, at the first quote it happened to reach.
It got that far because nothing in the fast lane reads this file as CSS.
eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet;
CI's `lint + tsc + vitest` job went green on the broken file. The first thing
that actually parses globals.css is the Next production build, which is why
the local e2e cycle caught it and everything cheaper did not.
Adds src/app/globals.test.ts: a dependency-free structural scan (comments,
strings, brace depth) that fails on exactly this class of breakage, in the
fast lane, pointing at the right line. Hand-rolled rather than importing
postcss on purpose — postcss is only a transitive dependency here and the
installed copy already drifts from CI's. It also tests itself: a second case
feeds it each broken shape to prove it does not silently return "fine".
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — green (and it caught a real break)

Final cycle at 8e6d2b4, one flock, SAPLING_MODEL_MODE=function:

35 passed (1.4m)
0 finding(s), 1 suppressed (allowlisted)

Gates: tsc --noEmit clean · npm run lint 0 errors · npx vitest run 58 files / 409 tests.

Worth flagging: the first cycle failed, and no cheaper gate could have caught it

The review-fix commit closed the hero-surface block comment early — the original */ stayed put while a new paragraph was inserted above it, so the tail of the comment became raw stylesheet text. make e2e-up died building the frontend:

CssSyntaxError: globals.css:1007:13: Unclosed string

780 lines below the actual mistake, at the first quote postcss happened to reach.

CI's lint + tsc + vitest job was green on that broken file. Nothing in the fast lane reads globals.css as CSS: eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet. The first thing that parses it is the Next production build.

So this PR also adds src/app/globals.test.ts — a dependency-free structural scan (comments, strings, brace depth) that fails in the fast lane and points at the right line. It's hand-rolled rather than a postcss import because postcss is only a transitive dependency here and the installed copy already drifts from CI's. It tests itself too: a second case feeds it each broken shape to prove it doesn't silently return "fine".

Rebase note

Rebased onto main after #488 landed. Both PRs touch globals.css; the two ordering invariants were re-verified after the merge — .card--hero (:242) still follows .card (:210), and .pending-* (:278) still precedes .anim-d* (:306).

@AndresL230
AndresL230 merged commit b335dfb into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated review pass (Claude Code), originally run against f8eb2ac and re-checked after the force-push to 8e6d2b4. Independently verified at the original head: extraction is genuinely complete (zero residual gradient literals outside the token/fallbacks/test constant), the PR body's drift table is accurate row-for-row against base, .card--hero overrides every .card property at equal-and-later specificity so card adoption is cascade-neutral, testids preserved, tsc clean + full vitest suite green. The e2e-cycle comment and the [P1-E] tick landed since — both good.

Still outstanding

  1. The PR body promises "Before/after screenshots in a comment below," but no comment contains any image. Since this PR declares a deliberate visible change (cool→warm shadow hue, unified alphas, success modal gaining radius+inset), the visual sign-off it leans on isn't actually on the record — post the screenshots or amend the body.

Minor

  1. HeroCard.test.tsx — the anti-drift walk still scans only .ts/.tsx (its own comment at ~:65 notes globals.css "is not walked (only .ts/.tsx)"). A future inlined copy of the gradient in a new.css file (e.g. a CSS module) would evade the guard. The legitimate fallback literals are an argument for excluding app/globals.css by path, not the whole .css extension.

Note for future edits:.card--hero currently overrides everything .card declares, so the pairing is purely nominal today — any future addition to .card (padding, hover elevation, transition) will silently restyle all three modals. Worth one sentence in the globals.css comment block.

Verdict: ready to merge once the screenshots are posted (or the body reference removed).

Sign up for freeto 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.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(ui): extract the shared hero-card surface (#288) by AndresL230 · Pull Request #489 · SaplingLearn/Sapling · GitHub
Skip to content

refactor(ui): extract the shared hero-card surface (#288) - #489

Merged
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card
Jul 31, 2026
Merged

refactor(ui): extract the shared hero-card surface (#288)#489
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

The warm gradient behind the sign-in and beta modals was inlined as a literal at five sites, and had already drifted at four of them. --surface-hero / --surface-hero-shadow sat in globals.css with zero consumers.

SiteRoleRadiusShadow
page.tsx:756beta success modal20rgba(15,23,42,0.15), no inset
page.tsx:785beta signup shell24rgba(15,23,42,0.12) + inset
page.tsx:819modal left panel (inner)background only
page.tsx:871modal right panel (inner)background only
SignInModal.tsx:226sign-in modal24rgba(15,23,42,0.18) + inset

What replaced it

  • .card--hero — a named variant of .card, used as class="card card--hero" (declared after .card so it wins).
  • .hero-surface — the gradient alone, for the two inner panels, which must not restate the parent's border/radius/shadow.
  • <HeroCard> — a thin wrapper that owns those two classes and forwards ref, className and every other prop. Callers keep their own layout and dialog semantics.

Both classes are now the tokens' only consumers.

This is a visible change, and an intended one

Adopting the token moves the shadow hue from cool slate rgba(15,23,42) to the warm rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and gives the beta success modal the 24px radius and inset highlight its two siblings already had. Before/after screenshots in a comment below.

On the literal fallbacks

var(--surface-hero, <literal>) in both rules is deliberate. The tokens are scoped to .public-surface, .landing-page. I verified every consumer sits inside that subtree today — SignInModal is mounted only from the landing page, and nothing here portals to document.body — so the fallback is defensive, not load-bearing. For a shared component it is the cheap guard against the first mount that isn't.

Deliberately not folded in

The close button and the logo/wordmark row. The close buttons are not duplicated verbatim after all — they differ in offset (18 vs 14) and only one carries a testid. The wordmark appears at 20 sites repo-wide with no shared component, so extracting it for 2 of them would leave a half-migration; #111 touches every icon site anyway, which is the coherent place for it.

Tests

HeroCard.test.tsx pins the class contract and prop/ref forwarding, and adds two anti-drift guards: no inlined copy of the gradient may exist anywhere in src/, and both CSS rules must keep a literal fallback (including the inset half, or token and fallback would render as two different surfaces).

data-testid="signin-modal" and "signin-close" are unchanged; no testid surface was added, so neither half of the registry needed updating.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 56 files, 403 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #288

@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bd63ae22-9f71-498c-a826-dc974e36b26e

📥 Commits

Reviewing files that changed from the base of the PR and between 9bbacc6 and 8e6d2b4.

📒 Files selected for processing (7)
  • docs/superpowers/followups/2026-06-30-token-unification-followups.md
  • frontend/src/app/(public)/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/globals.test.ts
  • frontend/src/components/HeroCard.test.tsx
  • frontend/src/components/HeroCard.tsx
  • frontend/src/components/SignInModal.tsx

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging8e6d2b4Commit Preview URL

Branch Preview URL
Jul 31 2026, 03:17 AM

AndresL230and others added 2 commits July 30, 2026 20:10
The warm gradient behind the sign-in and beta modals was inlined as a
literal at five sites, and had already drifted at four of them: three
different shadow alphas (0.12 / 0.15 / 0.18), two radii (20 / 24), and one
card missing the inset highlight entirely. Meanwhile --surface-hero and
--surface-hero-shadow sat in globals.css with zero consumers.
Adds `.card--hero` (a named variant of .card, used as `card card--hero`) and
`.hero-surface` (the gradient alone, for panels nested inside a hero card
that must not restate its border, radius or shadow), plus a thin <HeroCard>
wrapper that owns those classes and forwards everything else. Both classes
are now the tokens' only consumers.
This is a VISIBLE change, not a pure refactor, and an intended one: adopting
the token moves the shadow hue from cool slate rgba(15,23,42) to the warm
rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and
gives the beta success modal the 24px radius and inset highlight its two
siblings already had.
The literal fallbacks in both CSS rules are deliberate. The tokens are
scoped to `.public-surface, .landing-page`; every consumer sits inside that
subtree today (verified — SignInModal is mounted only from the landing page,
and nothing here portals to document.body), so the fallback is defensive
rather than load-bearing. For a shared component it is the cheap guard
against the first mount that isn't.
data-testid="signin-modal" and "signin-close" are unchanged.
Deliberately NOT folded in: the close button and the logo/wordmark row. The
close buttons are not actually duplicated verbatim — they differ in offset
(18 vs 14) and only one carries a testid — and the wordmark appears at 20
sites repo-wide with no shared component, so extracting it for 2 of them
would leave a half-migration. #111 touches every icon site anyway; that is
the coherent place for it.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tick [P1-E] in the token-unification follow-ups. This PR is what completes
it, and the repo's convention is to fold that tick into the same PR — #486
existed purely to backfill a tick missed the same way.
Name the shadow hue change for what it is. The comments described the
adoption as de-drifting "three shadow alphas, two radii, one missing inset",
which is accurate but incomplete in a misleading direction: the shadow's base
COLOUR was the one value that had not drifted — all five sites agreed on
slate rgba(15,23,42). The token is rgba(19,38,16) = --sap-900, the base the
app's other shadows use, so adopting it re-tints these shadows rather than
reconciling them. Deliberate, and called out in the PR body, but a reader of
the code would have assumed the colour was untouched. Now stated in
globals.css and HeroCard.tsx.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cturally (#288)
The review-fix commit closed the hero-surface block comment early: the `*/`
that used to end the whole comment stayed put while a new paragraph was
inserted above it, so the rest of the original comment became raw stylesheet
text. postcss reported it as "Unclosed string" at line 1007 — 780 lines below
the actual mistake, at the first quote it happened to reach.
It got that far because nothing in the fast lane reads this file as CSS.
eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet;
CI's `lint + tsc + vitest` job went green on the broken file. The first thing
that actually parses globals.css is the Next production build, which is why
the local e2e cycle caught it and everything cheaper did not.
Adds src/app/globals.test.ts: a dependency-free structural scan (comments,
strings, brace depth) that fails on exactly this class of breakage, in the
fast lane, pointing at the right line. Hand-rolled rather than importing
postcss on purpose — postcss is only a transitive dependency here and the
installed copy already drifts from CI's. It also tests itself: a second case
feeds it each broken shape to prove it does not silently return "fine".
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — green (and it caught a real break)

Final cycle at 8e6d2b4, one flock, SAPLING_MODEL_MODE=function:

35 passed (1.4m)
0 finding(s), 1 suppressed (allowlisted)

Gates: tsc --noEmit clean · npm run lint 0 errors · npx vitest run 58 files / 409 tests.

Worth flagging: the first cycle failed, and no cheaper gate could have caught it

The review-fix commit closed the hero-surface block comment early — the original */ stayed put while a new paragraph was inserted above it, so the tail of the comment became raw stylesheet text. make e2e-up died building the frontend:

CssSyntaxError: globals.css:1007:13: Unclosed string

780 lines below the actual mistake, at the first quote postcss happened to reach.

CI's lint + tsc + vitest job was green on that broken file. Nothing in the fast lane reads globals.css as CSS: eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet. The first thing that parses it is the Next production build.

So this PR also adds src/app/globals.test.ts — a dependency-free structural scan (comments, strings, brace depth) that fails in the fast lane and points at the right line. It's hand-rolled rather than a postcss import because postcss is only a transitive dependency here and the installed copy already drifts from CI's. It tests itself too: a second case feeds it each broken shape to prove it doesn't silently return "fine".

Rebase note

Rebased onto main after #488 landed. Both PRs touch globals.css; the two ordering invariants were re-verified after the merge — .card--hero (:242) still follows .card (:210), and .pending-* (:278) still precedes .anim-d* (:306).

@AndresL230
AndresL230 merged commit b335dfb into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated review pass (Claude Code), originally run against f8eb2ac and re-checked after the force-push to 8e6d2b4. Independently verified at the original head: extraction is genuinely complete (zero residual gradient literals outside the token/fallbacks/test constant), the PR body's drift table is accurate row-for-row against base, .card--hero overrides every .card property at equal-and-later specificity so card adoption is cascade-neutral, testids preserved, tsc clean + full vitest suite green. The e2e-cycle comment and the [P1-E] tick landed since — both good.

Still outstanding

  1. The PR body promises "Before/after screenshots in a comment below," but no comment contains any image. Since this PR declares a deliberate visible change (cool→warm shadow hue, unified alphas, success modal gaining radius+inset), the visual sign-off it leans on isn't actually on the record — post the screenshots or amend the body.

Minor

  1. HeroCard.test.tsx — the anti-drift walk still scans only .ts/.tsx (its own comment at ~:65 notes globals.css "is not walked (only .ts/.tsx)"). A future inlined copy of the gradient in a new.css file (e.g. a CSS module) would evade the guard. The legitimate fallback literals are an argument for excluding app/globals.css by path, not the whole .css extension.

Note for future edits:.card--hero currently overrides everything .card declares, so the pairing is purely nominal today — any future addition to .card (padding, hover elevation, transition) will silently restyle all three modals. Worth one sentence in the globals.css comment block.

Verdict: ready to merge once the screenshots are posted (or the body reference removed).

Sign up for freeto 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.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' refactor(ui): extract the shared hero-card surface (#288) by AndresL230 · Pull Request #489 · SaplingLearn/Sapling · GitHub
Skip to content

refactor(ui): extract the shared hero-card surface (#288) - #489

Merged
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card
Jul 31, 2026
Merged

refactor(ui): extract the shared hero-card surface (#288)#489
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

The warm gradient behind the sign-in and beta modals was inlined as a literal at five sites, and had already drifted at four of them. --surface-hero / --surface-hero-shadow sat in globals.css with zero consumers.

SiteRoleRadiusShadow
page.tsx:756beta success modal20rgba(15,23,42,0.15), no inset
page.tsx:785beta signup shell24rgba(15,23,42,0.12) + inset
page.tsx:819modal left panel (inner)background only
page.tsx:871modal right panel (inner)background only
SignInModal.tsx:226sign-in modal24rgba(15,23,42,0.18) + inset

What replaced it

  • .card--hero — a named variant of .card, used as class="card card--hero" (declared after .card so it wins).
  • .hero-surface — the gradient alone, for the two inner panels, which must not restate the parent's border/radius/shadow.
  • <HeroCard> — a thin wrapper that owns those two classes and forwards ref, className and every other prop. Callers keep their own layout and dialog semantics.

Both classes are now the tokens' only consumers.

This is a visible change, and an intended one

Adopting the token moves the shadow hue from cool slate rgba(15,23,42) to the warm rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and gives the beta success modal the 24px radius and inset highlight its two siblings already had. Before/after screenshots in a comment below.

On the literal fallbacks

var(--surface-hero, <literal>) in both rules is deliberate. The tokens are scoped to .public-surface, .landing-page. I verified every consumer sits inside that subtree today — SignInModal is mounted only from the landing page, and nothing here portals to document.body — so the fallback is defensive, not load-bearing. For a shared component it is the cheap guard against the first mount that isn't.

Deliberately not folded in

The close button and the logo/wordmark row. The close buttons are not duplicated verbatim after all — they differ in offset (18 vs 14) and only one carries a testid. The wordmark appears at 20 sites repo-wide with no shared component, so extracting it for 2 of them would leave a half-migration; #111 touches every icon site anyway, which is the coherent place for it.

Tests

HeroCard.test.tsx pins the class contract and prop/ref forwarding, and adds two anti-drift guards: no inlined copy of the gradient may exist anywhere in src/, and both CSS rules must keep a literal fallback (including the inset half, or token and fallback would render as two different surfaces).

data-testid="signin-modal" and "signin-close" are unchanged; no testid surface was added, so neither half of the registry needed updating.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 56 files, 403 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #288

@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bd63ae22-9f71-498c-a826-dc974e36b26e

📥 Commits

Reviewing files that changed from the base of the PR and between 9bbacc6 and 8e6d2b4.

📒 Files selected for processing (7)
  • docs/superpowers/followups/2026-06-30-token-unification-followups.md
  • frontend/src/app/(public)/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/globals.test.ts
  • frontend/src/components/HeroCard.test.tsx
  • frontend/src/components/HeroCard.tsx
  • frontend/src/components/SignInModal.tsx

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging8e6d2b4Commit Preview URL

Branch Preview URL
Jul 31 2026, 03:17 AM

AndresL230and others added 2 commits July 30, 2026 20:10
The warm gradient behind the sign-in and beta modals was inlined as a
literal at five sites, and had already drifted at four of them: three
different shadow alphas (0.12 / 0.15 / 0.18), two radii (20 / 24), and one
card missing the inset highlight entirely. Meanwhile --surface-hero and
--surface-hero-shadow sat in globals.css with zero consumers.
Adds `.card--hero` (a named variant of .card, used as `card card--hero`) and
`.hero-surface` (the gradient alone, for panels nested inside a hero card
that must not restate its border, radius or shadow), plus a thin <HeroCard>
wrapper that owns those classes and forwards everything else. Both classes
are now the tokens' only consumers.
This is a VISIBLE change, not a pure refactor, and an intended one: adopting
the token moves the shadow hue from cool slate rgba(15,23,42) to the warm
rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and
gives the beta success modal the 24px radius and inset highlight its two
siblings already had.
The literal fallbacks in both CSS rules are deliberate. The tokens are
scoped to `.public-surface, .landing-page`; every consumer sits inside that
subtree today (verified — SignInModal is mounted only from the landing page,
and nothing here portals to document.body), so the fallback is defensive
rather than load-bearing. For a shared component it is the cheap guard
against the first mount that isn't.
data-testid="signin-modal" and "signin-close" are unchanged.
Deliberately NOT folded in: the close button and the logo/wordmark row. The
close buttons are not actually duplicated verbatim — they differ in offset
(18 vs 14) and only one carries a testid — and the wordmark appears at 20
sites repo-wide with no shared component, so extracting it for 2 of them
would leave a half-migration. #111 touches every icon site anyway; that is
the coherent place for it.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tick [P1-E] in the token-unification follow-ups. This PR is what completes
it, and the repo's convention is to fold that tick into the same PR — #486
existed purely to backfill a tick missed the same way.
Name the shadow hue change for what it is. The comments described the
adoption as de-drifting "three shadow alphas, two radii, one missing inset",
which is accurate but incomplete in a misleading direction: the shadow's base
COLOUR was the one value that had not drifted — all five sites agreed on
slate rgba(15,23,42). The token is rgba(19,38,16) = --sap-900, the base the
app's other shadows use, so adopting it re-tints these shadows rather than
reconciling them. Deliberate, and called out in the PR body, but a reader of
the code would have assumed the colour was untouched. Now stated in
globals.css and HeroCard.tsx.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cturally (#288)
The review-fix commit closed the hero-surface block comment early: the `*/`
that used to end the whole comment stayed put while a new paragraph was
inserted above it, so the rest of the original comment became raw stylesheet
text. postcss reported it as "Unclosed string" at line 1007 — 780 lines below
the actual mistake, at the first quote it happened to reach.
It got that far because nothing in the fast lane reads this file as CSS.
eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet;
CI's `lint + tsc + vitest` job went green on the broken file. The first thing
that actually parses globals.css is the Next production build, which is why
the local e2e cycle caught it and everything cheaper did not.
Adds src/app/globals.test.ts: a dependency-free structural scan (comments,
strings, brace depth) that fails on exactly this class of breakage, in the
fast lane, pointing at the right line. Hand-rolled rather than importing
postcss on purpose — postcss is only a transitive dependency here and the
installed copy already drifts from CI's. It also tests itself: a second case
feeds it each broken shape to prove it does not silently return "fine".
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — green (and it caught a real break)

Final cycle at 8e6d2b4, one flock, SAPLING_MODEL_MODE=function:

35 passed (1.4m)
0 finding(s), 1 suppressed (allowlisted)

Gates: tsc --noEmit clean · npm run lint 0 errors · npx vitest run 58 files / 409 tests.

Worth flagging: the first cycle failed, and no cheaper gate could have caught it

The review-fix commit closed the hero-surface block comment early — the original */ stayed put while a new paragraph was inserted above it, so the tail of the comment became raw stylesheet text. make e2e-up died building the frontend:

CssSyntaxError: globals.css:1007:13: Unclosed string

780 lines below the actual mistake, at the first quote postcss happened to reach.

CI's lint + tsc + vitest job was green on that broken file. Nothing in the fast lane reads globals.css as CSS: eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet. The first thing that parses it is the Next production build.

So this PR also adds src/app/globals.test.ts — a dependency-free structural scan (comments, strings, brace depth) that fails in the fast lane and points at the right line. It's hand-rolled rather than a postcss import because postcss is only a transitive dependency here and the installed copy already drifts from CI's. It tests itself too: a second case feeds it each broken shape to prove it doesn't silently return "fine".

Rebase note

Rebased onto main after #488 landed. Both PRs touch globals.css; the two ordering invariants were re-verified after the merge — .card--hero (:242) still follows .card (:210), and .pending-* (:278) still precedes .anim-d* (:306).

@AndresL230
AndresL230 merged commit b335dfb into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated review pass (Claude Code), originally run against f8eb2ac and re-checked after the force-push to 8e6d2b4. Independently verified at the original head: extraction is genuinely complete (zero residual gradient literals outside the token/fallbacks/test constant), the PR body's drift table is accurate row-for-row against base, .card--hero overrides every .card property at equal-and-later specificity so card adoption is cascade-neutral, testids preserved, tsc clean + full vitest suite green. The e2e-cycle comment and the [P1-E] tick landed since — both good.

Still outstanding

  1. The PR body promises "Before/after screenshots in a comment below," but no comment contains any image. Since this PR declares a deliberate visible change (cool→warm shadow hue, unified alphas, success modal gaining radius+inset), the visual sign-off it leans on isn't actually on the record — post the screenshots or amend the body.

Minor

  1. HeroCard.test.tsx — the anti-drift walk still scans only .ts/.tsx (its own comment at ~:65 notes globals.css "is not walked (only .ts/.tsx)"). A future inlined copy of the gradient in a new.css file (e.g. a CSS module) would evade the guard. The legitimate fallback literals are an argument for excluding app/globals.css by path, not the whole .css extension.

Note for future edits:.card--hero currently overrides everything .card declares, so the pairing is purely nominal today — any future addition to .card (padding, hover elevation, transition) will silently restyle all three modals. Worth one sentence in the globals.css comment block.

Verdict: ready to merge once the screenshots are posted (or the body reference removed).

Sign up for freeto 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.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(ui): extract the shared hero-card surface (#288) by AndresL230 · Pull Request #489 · SaplingLearn/Sapling · GitHub
Skip to content

refactor(ui): extract the shared hero-card surface (#288) - #489

Merged
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card
Jul 31, 2026
Merged

refactor(ui): extract the shared hero-card surface (#288)#489
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

The warm gradient behind the sign-in and beta modals was inlined as a literal at five sites, and had already drifted at four of them. --surface-hero / --surface-hero-shadow sat in globals.css with zero consumers.

SiteRoleRadiusShadow
page.tsx:756beta success modal20rgba(15,23,42,0.15), no inset
page.tsx:785beta signup shell24rgba(15,23,42,0.12) + inset
page.tsx:819modal left panel (inner)background only
page.tsx:871modal right panel (inner)background only
SignInModal.tsx:226sign-in modal24rgba(15,23,42,0.18) + inset

What replaced it

  • .card--hero — a named variant of .card, used as class="card card--hero" (declared after .card so it wins).
  • .hero-surface — the gradient alone, for the two inner panels, which must not restate the parent's border/radius/shadow.
  • <HeroCard> — a thin wrapper that owns those two classes and forwards ref, className and every other prop. Callers keep their own layout and dialog semantics.

Both classes are now the tokens' only consumers.

This is a visible change, and an intended one

Adopting the token moves the shadow hue from cool slate rgba(15,23,42) to the warm rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and gives the beta success modal the 24px radius and inset highlight its two siblings already had. Before/after screenshots in a comment below.

On the literal fallbacks

var(--surface-hero, <literal>) in both rules is deliberate. The tokens are scoped to .public-surface, .landing-page. I verified every consumer sits inside that subtree today — SignInModal is mounted only from the landing page, and nothing here portals to document.body — so the fallback is defensive, not load-bearing. For a shared component it is the cheap guard against the first mount that isn't.

Deliberately not folded in

The close button and the logo/wordmark row. The close buttons are not duplicated verbatim after all — they differ in offset (18 vs 14) and only one carries a testid. The wordmark appears at 20 sites repo-wide with no shared component, so extracting it for 2 of them would leave a half-migration; #111 touches every icon site anyway, which is the coherent place for it.

Tests

HeroCard.test.tsx pins the class contract and prop/ref forwarding, and adds two anti-drift guards: no inlined copy of the gradient may exist anywhere in src/, and both CSS rules must keep a literal fallback (including the inset half, or token and fallback would render as two different surfaces).

data-testid="signin-modal" and "signin-close" are unchanged; no testid surface was added, so neither half of the registry needed updating.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 56 files, 403 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #288

@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bd63ae22-9f71-498c-a826-dc974e36b26e

📥 Commits

Reviewing files that changed from the base of the PR and between 9bbacc6 and 8e6d2b4.

📒 Files selected for processing (7)
  • docs/superpowers/followups/2026-06-30-token-unification-followups.md
  • frontend/src/app/(public)/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/globals.test.ts
  • frontend/src/components/HeroCard.test.tsx
  • frontend/src/components/HeroCard.tsx
  • frontend/src/components/SignInModal.tsx

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging8e6d2b4Commit Preview URL

Branch Preview URL
Jul 31 2026, 03:17 AM

AndresL230and others added 2 commits July 30, 2026 20:10
The warm gradient behind the sign-in and beta modals was inlined as a
literal at five sites, and had already drifted at four of them: three
different shadow alphas (0.12 / 0.15 / 0.18), two radii (20 / 24), and one
card missing the inset highlight entirely. Meanwhile --surface-hero and
--surface-hero-shadow sat in globals.css with zero consumers.
Adds `.card--hero` (a named variant of .card, used as `card card--hero`) and
`.hero-surface` (the gradient alone, for panels nested inside a hero card
that must not restate its border, radius or shadow), plus a thin <HeroCard>
wrapper that owns those classes and forwards everything else. Both classes
are now the tokens' only consumers.
This is a VISIBLE change, not a pure refactor, and an intended one: adopting
the token moves the shadow hue from cool slate rgba(15,23,42) to the warm
rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and
gives the beta success modal the 24px radius and inset highlight its two
siblings already had.
The literal fallbacks in both CSS rules are deliberate. The tokens are
scoped to `.public-surface, .landing-page`; every consumer sits inside that
subtree today (verified — SignInModal is mounted only from the landing page,
and nothing here portals to document.body), so the fallback is defensive
rather than load-bearing. For a shared component it is the cheap guard
against the first mount that isn't.
data-testid="signin-modal" and "signin-close" are unchanged.
Deliberately NOT folded in: the close button and the logo/wordmark row. The
close buttons are not actually duplicated verbatim — they differ in offset
(18 vs 14) and only one carries a testid — and the wordmark appears at 20
sites repo-wide with no shared component, so extracting it for 2 of them
would leave a half-migration. #111 touches every icon site anyway; that is
the coherent place for it.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tick [P1-E] in the token-unification follow-ups. This PR is what completes
it, and the repo's convention is to fold that tick into the same PR — #486
existed purely to backfill a tick missed the same way.
Name the shadow hue change for what it is. The comments described the
adoption as de-drifting "three shadow alphas, two radii, one missing inset",
which is accurate but incomplete in a misleading direction: the shadow's base
COLOUR was the one value that had not drifted — all five sites agreed on
slate rgba(15,23,42). The token is rgba(19,38,16) = --sap-900, the base the
app's other shadows use, so adopting it re-tints these shadows rather than
reconciling them. Deliberate, and called out in the PR body, but a reader of
the code would have assumed the colour was untouched. Now stated in
globals.css and HeroCard.tsx.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cturally (#288)
The review-fix commit closed the hero-surface block comment early: the `*/`
that used to end the whole comment stayed put while a new paragraph was
inserted above it, so the rest of the original comment became raw stylesheet
text. postcss reported it as "Unclosed string" at line 1007 — 780 lines below
the actual mistake, at the first quote it happened to reach.
It got that far because nothing in the fast lane reads this file as CSS.
eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet;
CI's `lint + tsc + vitest` job went green on the broken file. The first thing
that actually parses globals.css is the Next production build, which is why
the local e2e cycle caught it and everything cheaper did not.
Adds src/app/globals.test.ts: a dependency-free structural scan (comments,
strings, brace depth) that fails on exactly this class of breakage, in the
fast lane, pointing at the right line. Hand-rolled rather than importing
postcss on purpose — postcss is only a transitive dependency here and the
installed copy already drifts from CI's. It also tests itself: a second case
feeds it each broken shape to prove it does not silently return "fine".
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — green (and it caught a real break)

Final cycle at 8e6d2b4, one flock, SAPLING_MODEL_MODE=function:

35 passed (1.4m)
0 finding(s), 1 suppressed (allowlisted)

Gates: tsc --noEmit clean · npm run lint 0 errors · npx vitest run 58 files / 409 tests.

Worth flagging: the first cycle failed, and no cheaper gate could have caught it

The review-fix commit closed the hero-surface block comment early — the original */ stayed put while a new paragraph was inserted above it, so the tail of the comment became raw stylesheet text. make e2e-up died building the frontend:

CssSyntaxError: globals.css:1007:13: Unclosed string

780 lines below the actual mistake, at the first quote postcss happened to reach.

CI's lint + tsc + vitest job was green on that broken file. Nothing in the fast lane reads globals.css as CSS: eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet. The first thing that parses it is the Next production build.

So this PR also adds src/app/globals.test.ts — a dependency-free structural scan (comments, strings, brace depth) that fails in the fast lane and points at the right line. It's hand-rolled rather than a postcss import because postcss is only a transitive dependency here and the installed copy already drifts from CI's. It tests itself too: a second case feeds it each broken shape to prove it doesn't silently return "fine".

Rebase note

Rebased onto main after #488 landed. Both PRs touch globals.css; the two ordering invariants were re-verified after the merge — .card--hero (:242) still follows .card (:210), and .pending-* (:278) still precedes .anim-d* (:306).

@AndresL230
AndresL230 merged commit b335dfb into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated review pass (Claude Code), originally run against f8eb2ac and re-checked after the force-push to 8e6d2b4. Independently verified at the original head: extraction is genuinely complete (zero residual gradient literals outside the token/fallbacks/test constant), the PR body's drift table is accurate row-for-row against base, .card--hero overrides every .card property at equal-and-later specificity so card adoption is cascade-neutral, testids preserved, tsc clean + full vitest suite green. The e2e-cycle comment and the [P1-E] tick landed since — both good.

Still outstanding

  1. The PR body promises "Before/after screenshots in a comment below," but no comment contains any image. Since this PR declares a deliberate visible change (cool→warm shadow hue, unified alphas, success modal gaining radius+inset), the visual sign-off it leans on isn't actually on the record — post the screenshots or amend the body.

Minor

  1. HeroCard.test.tsx — the anti-drift walk still scans only .ts/.tsx (its own comment at ~:65 notes globals.css "is not walked (only .ts/.tsx)"). A future inlined copy of the gradient in a new.css file (e.g. a CSS module) would evade the guard. The legitimate fallback literals are an argument for excluding app/globals.css by path, not the whole .css extension.

Note for future edits:.card--hero currently overrides everything .card declares, so the pairing is purely nominal today — any future addition to .card (padding, hover elevation, transition) will silently restyle all three modals. Worth one sentence in the globals.css comment block.

Verdict: ready to merge once the screenshots are posted (or the body reference removed).

Sign up for freeto 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.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(ui): extract the shared hero-card surface (#288) by AndresL230 · Pull Request #489 · SaplingLearn/Sapling · GitHub
Skip to content

refactor(ui): extract the shared hero-card surface (#288) - #489

Merged
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card
Jul 31, 2026
Merged

refactor(ui): extract the shared hero-card surface (#288)#489
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

The warm gradient behind the sign-in and beta modals was inlined as a literal at five sites, and had already drifted at four of them. --surface-hero / --surface-hero-shadow sat in globals.css with zero consumers.

SiteRoleRadiusShadow
page.tsx:756beta success modal20rgba(15,23,42,0.15), no inset
page.tsx:785beta signup shell24rgba(15,23,42,0.12) + inset
page.tsx:819modal left panel (inner)background only
page.tsx:871modal right panel (inner)background only
SignInModal.tsx:226sign-in modal24rgba(15,23,42,0.18) + inset

What replaced it

  • .card--hero — a named variant of .card, used as class="card card--hero" (declared after .card so it wins).
  • .hero-surface — the gradient alone, for the two inner panels, which must not restate the parent's border/radius/shadow.
  • <HeroCard> — a thin wrapper that owns those two classes and forwards ref, className and every other prop. Callers keep their own layout and dialog semantics.

Both classes are now the tokens' only consumers.

This is a visible change, and an intended one

Adopting the token moves the shadow hue from cool slate rgba(15,23,42) to the warm rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and gives the beta success modal the 24px radius and inset highlight its two siblings already had. Before/after screenshots in a comment below.

On the literal fallbacks

var(--surface-hero, <literal>) in both rules is deliberate. The tokens are scoped to .public-surface, .landing-page. I verified every consumer sits inside that subtree today — SignInModal is mounted only from the landing page, and nothing here portals to document.body — so the fallback is defensive, not load-bearing. For a shared component it is the cheap guard against the first mount that isn't.

Deliberately not folded in

The close button and the logo/wordmark row. The close buttons are not duplicated verbatim after all — they differ in offset (18 vs 14) and only one carries a testid. The wordmark appears at 20 sites repo-wide with no shared component, so extracting it for 2 of them would leave a half-migration; #111 touches every icon site anyway, which is the coherent place for it.

Tests

HeroCard.test.tsx pins the class contract and prop/ref forwarding, and adds two anti-drift guards: no inlined copy of the gradient may exist anywhere in src/, and both CSS rules must keep a literal fallback (including the inset half, or token and fallback would render as two different surfaces).

data-testid="signin-modal" and "signin-close" are unchanged; no testid surface was added, so neither half of the registry needed updating.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 56 files, 403 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #288

@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bd63ae22-9f71-498c-a826-dc974e36b26e

📥 Commits

Reviewing files that changed from the base of the PR and between 9bbacc6 and 8e6d2b4.

📒 Files selected for processing (7)
  • docs/superpowers/followups/2026-06-30-token-unification-followups.md
  • frontend/src/app/(public)/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/globals.test.ts
  • frontend/src/components/HeroCard.test.tsx
  • frontend/src/components/HeroCard.tsx
  • frontend/src/components/SignInModal.tsx

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging8e6d2b4Commit Preview URL

Branch Preview URL
Jul 31 2026, 03:17 AM

AndresL230and others added 2 commits July 30, 2026 20:10
The warm gradient behind the sign-in and beta modals was inlined as a
literal at five sites, and had already drifted at four of them: three
different shadow alphas (0.12 / 0.15 / 0.18), two radii (20 / 24), and one
card missing the inset highlight entirely. Meanwhile --surface-hero and
--surface-hero-shadow sat in globals.css with zero consumers.
Adds `.card--hero` (a named variant of .card, used as `card card--hero`) and
`.hero-surface` (the gradient alone, for panels nested inside a hero card
that must not restate its border, radius or shadow), plus a thin <HeroCard>
wrapper that owns those classes and forwards everything else. Both classes
are now the tokens' only consumers.
This is a VISIBLE change, not a pure refactor, and an intended one: adopting
the token moves the shadow hue from cool slate rgba(15,23,42) to the warm
rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and
gives the beta success modal the 24px radius and inset highlight its two
siblings already had.
The literal fallbacks in both CSS rules are deliberate. The tokens are
scoped to `.public-surface, .landing-page`; every consumer sits inside that
subtree today (verified — SignInModal is mounted only from the landing page,
and nothing here portals to document.body), so the fallback is defensive
rather than load-bearing. For a shared component it is the cheap guard
against the first mount that isn't.
data-testid="signin-modal" and "signin-close" are unchanged.
Deliberately NOT folded in: the close button and the logo/wordmark row. The
close buttons are not actually duplicated verbatim — they differ in offset
(18 vs 14) and only one carries a testid — and the wordmark appears at 20
sites repo-wide with no shared component, so extracting it for 2 of them
would leave a half-migration. #111 touches every icon site anyway; that is
the coherent place for it.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tick [P1-E] in the token-unification follow-ups. This PR is what completes
it, and the repo's convention is to fold that tick into the same PR — #486
existed purely to backfill a tick missed the same way.
Name the shadow hue change for what it is. The comments described the
adoption as de-drifting "three shadow alphas, two radii, one missing inset",
which is accurate but incomplete in a misleading direction: the shadow's base
COLOUR was the one value that had not drifted — all five sites agreed on
slate rgba(15,23,42). The token is rgba(19,38,16) = --sap-900, the base the
app's other shadows use, so adopting it re-tints these shadows rather than
reconciling them. Deliberate, and called out in the PR body, but a reader of
the code would have assumed the colour was untouched. Now stated in
globals.css and HeroCard.tsx.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cturally (#288)
The review-fix commit closed the hero-surface block comment early: the `*/`
that used to end the whole comment stayed put while a new paragraph was
inserted above it, so the rest of the original comment became raw stylesheet
text. postcss reported it as "Unclosed string" at line 1007 — 780 lines below
the actual mistake, at the first quote it happened to reach.
It got that far because nothing in the fast lane reads this file as CSS.
eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet;
CI's `lint + tsc + vitest` job went green on the broken file. The first thing
that actually parses globals.css is the Next production build, which is why
the local e2e cycle caught it and everything cheaper did not.
Adds src/app/globals.test.ts: a dependency-free structural scan (comments,
strings, brace depth) that fails on exactly this class of breakage, in the
fast lane, pointing at the right line. Hand-rolled rather than importing
postcss on purpose — postcss is only a transitive dependency here and the
installed copy already drifts from CI's. It also tests itself: a second case
feeds it each broken shape to prove it does not silently return "fine".
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — green (and it caught a real break)

Final cycle at 8e6d2b4, one flock, SAPLING_MODEL_MODE=function:

35 passed (1.4m)
0 finding(s), 1 suppressed (allowlisted)

Gates: tsc --noEmit clean · npm run lint 0 errors · npx vitest run 58 files / 409 tests.

Worth flagging: the first cycle failed, and no cheaper gate could have caught it

The review-fix commit closed the hero-surface block comment early — the original */ stayed put while a new paragraph was inserted above it, so the tail of the comment became raw stylesheet text. make e2e-up died building the frontend:

CssSyntaxError: globals.css:1007:13: Unclosed string

780 lines below the actual mistake, at the first quote postcss happened to reach.

CI's lint + tsc + vitest job was green on that broken file. Nothing in the fast lane reads globals.css as CSS: eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet. The first thing that parses it is the Next production build.

So this PR also adds src/app/globals.test.ts — a dependency-free structural scan (comments, strings, brace depth) that fails in the fast lane and points at the right line. It's hand-rolled rather than a postcss import because postcss is only a transitive dependency here and the installed copy already drifts from CI's. It tests itself too: a second case feeds it each broken shape to prove it doesn't silently return "fine".

Rebase note

Rebased onto main after #488 landed. Both PRs touch globals.css; the two ordering invariants were re-verified after the merge — .card--hero (:242) still follows .card (:210), and .pending-* (:278) still precedes .anim-d* (:306).

@AndresL230
AndresL230 merged commit b335dfb into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated review pass (Claude Code), originally run against f8eb2ac and re-checked after the force-push to 8e6d2b4. Independently verified at the original head: extraction is genuinely complete (zero residual gradient literals outside the token/fallbacks/test constant), the PR body's drift table is accurate row-for-row against base, .card--hero overrides every .card property at equal-and-later specificity so card adoption is cascade-neutral, testids preserved, tsc clean + full vitest suite green. The e2e-cycle comment and the [P1-E] tick landed since — both good.

Still outstanding

  1. The PR body promises "Before/after screenshots in a comment below," but no comment contains any image. Since this PR declares a deliberate visible change (cool→warm shadow hue, unified alphas, success modal gaining radius+inset), the visual sign-off it leans on isn't actually on the record — post the screenshots or amend the body.

Minor

  1. HeroCard.test.tsx — the anti-drift walk still scans only .ts/.tsx (its own comment at ~:65 notes globals.css "is not walked (only .ts/.tsx)"). A future inlined copy of the gradient in a new.css file (e.g. a CSS module) would evade the guard. The legitimate fallback literals are an argument for excluding app/globals.css by path, not the whole .css extension.

Note for future edits:.card--hero currently overrides everything .card declares, so the pairing is purely nominal today — any future addition to .card (padding, hover elevation, transition) will silently restyle all three modals. Worth one sentence in the globals.css comment block.

Verdict: ready to merge once the screenshots are posted (or the body reference removed).

Sign up for freeto 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.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); refactor(ui): extract the shared hero-card surface (#288) by AndresL230 · Pull Request #489 · SaplingLearn/Sapling · GitHub
Skip to content

refactor(ui): extract the shared hero-card surface (#288) - #489

Merged
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card
Jul 31, 2026
Merged

refactor(ui): extract the shared hero-card surface (#288)#489
AndresL230 merged 3 commits into
mainfrom
feat/288-hero-card

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

The warm gradient behind the sign-in and beta modals was inlined as a literal at five sites, and had already drifted at four of them. --surface-hero / --surface-hero-shadow sat in globals.css with zero consumers.

SiteRoleRadiusShadow
page.tsx:756beta success modal20rgba(15,23,42,0.15), no inset
page.tsx:785beta signup shell24rgba(15,23,42,0.12) + inset
page.tsx:819modal left panel (inner)background only
page.tsx:871modal right panel (inner)background only
SignInModal.tsx:226sign-in modal24rgba(15,23,42,0.18) + inset

What replaced it

  • .card--hero — a named variant of .card, used as class="card card--hero" (declared after .card so it wins).
  • .hero-surface — the gradient alone, for the two inner panels, which must not restate the parent's border/radius/shadow.
  • <HeroCard> — a thin wrapper that owns those two classes and forwards ref, className and every other prop. Callers keep their own layout and dialog semantics.

Both classes are now the tokens' only consumers.

This is a visible change, and an intended one

Adopting the token moves the shadow hue from cool slate rgba(15,23,42) to the warm rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and gives the beta success modal the 24px radius and inset highlight its two siblings already had. Before/after screenshots in a comment below.

On the literal fallbacks

var(--surface-hero, <literal>) in both rules is deliberate. The tokens are scoped to .public-surface, .landing-page. I verified every consumer sits inside that subtree today — SignInModal is mounted only from the landing page, and nothing here portals to document.body — so the fallback is defensive, not load-bearing. For a shared component it is the cheap guard against the first mount that isn't.

Deliberately not folded in

The close button and the logo/wordmark row. The close buttons are not duplicated verbatim after all — they differ in offset (18 vs 14) and only one carries a testid. The wordmark appears at 20 sites repo-wide with no shared component, so extracting it for 2 of them would leave a half-migration; #111 touches every icon site anyway, which is the coherent place for it.

Tests

HeroCard.test.tsx pins the class contract and prop/ref forwarding, and adds two anti-drift guards: no inlined copy of the gradient may exist anywhere in src/, and both CSS rules must keep a literal fallback (including the inset half, or token and fallback would render as two different surfaces).

data-testid="signin-modal" and "signin-close" are unchanged; no testid surface was added, so neither half of the registry needed updating.

Gates

  • tsc --noEmit — clean
  • npm run lint — 0 errors (36 pre-existing warnings)
  • npx vitest run — 56 files, 403 tests passed
  • Full local e2e cycle — recorded in a comment below

part of #288

@supabase

supabaseBot commented Jul 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bd63ae22-9f71-498c-a826-dc974e36b26e

📥 Commits

Reviewing files that changed from the base of the PR and between 9bbacc6 and 8e6d2b4.

📒 Files selected for processing (7)
  • docs/superpowers/followups/2026-06-30-token-unification-followups.md
  • frontend/src/app/(public)/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/globals.test.ts
  • frontend/src/components/HeroCard.test.tsx
  • frontend/src/components/HeroCard.tsx
  • frontend/src/components/SignInModal.tsx

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging8e6d2b4Commit Preview URL

Branch Preview URL
Jul 31 2026, 03:17 AM

AndresL230and others added 2 commits July 30, 2026 20:10
The warm gradient behind the sign-in and beta modals was inlined as a
literal at five sites, and had already drifted at four of them: three
different shadow alphas (0.12 / 0.15 / 0.18), two radii (20 / 24), and one
card missing the inset highlight entirely. Meanwhile --surface-hero and
--surface-hero-shadow sat in globals.css with zero consumers.
Adds `.card--hero` (a named variant of .card, used as `card card--hero`) and
`.hero-surface` (the gradient alone, for panels nested inside a hero card
that must not restate its border, radius or shadow), plus a thin <HeroCard>
wrapper that owns those classes and forwards everything else. Both classes
are now the tokens' only consumers.
This is a VISIBLE change, not a pure refactor, and an intended one: adopting
the token moves the shadow hue from cool slate rgba(15,23,42) to the warm
rgba(19,38,16) the rest of the app uses, unifies the alphas at 0.12, and
gives the beta success modal the 24px radius and inset highlight its two
siblings already had.
The literal fallbacks in both CSS rules are deliberate. The tokens are
scoped to `.public-surface, .landing-page`; every consumer sits inside that
subtree today (verified — SignInModal is mounted only from the landing page,
and nothing here portals to document.body), so the fallback is defensive
rather than load-bearing. For a shared component it is the cheap guard
against the first mount that isn't.
data-testid="signin-modal" and "signin-close" are unchanged.
Deliberately NOT folded in: the close button and the logo/wordmark row. The
close buttons are not actually duplicated verbatim — they differ in offset
(18 vs 14) and only one carries a testid — and the wordmark appears at 20
sites repo-wide with no shared component, so extracting it for 2 of them
would leave a half-migration. #111 touches every icon site anyway; that is
the coherent place for it.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tick [P1-E] in the token-unification follow-ups. This PR is what completes
it, and the repo's convention is to fold that tick into the same PR — #486
existed purely to backfill a tick missed the same way.
Name the shadow hue change for what it is. The comments described the
adoption as de-drifting "three shadow alphas, two radii, one missing inset",
which is accurate but incomplete in a misleading direction: the shadow's base
COLOUR was the one value that had not drifted — all five sites agreed on
slate rgba(15,23,42). The token is rgba(19,38,16) = --sap-900, the base the
app's other shadows use, so adopting it re-tints these shadows rather than
reconciling them. Deliberate, and called out in the PR body, but a reader of
the code would have assumed the colour was untouched. Now stated in
globals.css and HeroCard.tsx.
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cturally (#288)
The review-fix commit closed the hero-surface block comment early: the `*/`
that used to end the whole comment stayed put while a new paragraph was
inserted above it, so the rest of the original comment became raw stylesheet
text. postcss reported it as "Unclosed string" at line 1007 — 780 lines below
the actual mistake, at the first quote it happened to reach.
It got that far because nothing in the fast lane reads this file as CSS.
eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet;
CI's `lint + tsc + vitest` job went green on the broken file. The first thing
that actually parses globals.css is the Next production build, which is why
the local e2e cycle caught it and everything cheaper did not.
Adds src/app/globals.test.ts: a dependency-free structural scan (comments,
strings, brace depth) that fails on exactly this class of breakage, in the
fast lane, pointing at the right line. Hand-rolled rather than importing
postcss on purpose — postcss is only a transitive dependency here and the
installed copy already drifts from CI's. It also tests itself: a second case
feeds it each broken shape to prove it does not silently return "fine".
part of #288
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Local e2e cycle — green (and it caught a real break)

Final cycle at 8e6d2b4, one flock, SAPLING_MODEL_MODE=function:

35 passed (1.4m)
0 finding(s), 1 suppressed (allowlisted)

Gates: tsc --noEmit clean · npm run lint 0 errors · npx vitest run 58 files / 409 tests.

Worth flagging: the first cycle failed, and no cheaper gate could have caught it

The review-fix commit closed the hero-surface block comment early — the original */ stayed put while a new paragraph was inserted above it, so the tail of the comment became raw stylesheet text. make e2e-up died building the frontend:

CssSyntaxError: globals.css:1007:13: Unclosed string

780 lines below the actual mistake, at the first quote postcss happened to reach.

CI's lint + tsc + vitest job was green on that broken file. Nothing in the fast lane reads globals.css as CSS: eslint lints JS/TS, tsc checks types, vitest never imports the stylesheet. The first thing that parses it is the Next production build.

So this PR also adds src/app/globals.test.ts — a dependency-free structural scan (comments, strings, brace depth) that fails in the fast lane and points at the right line. It's hand-rolled rather than a postcss import because postcss is only a transitive dependency here and the installed copy already drifts from CI's. It tests itself too: a second case feeds it each broken shape to prove it doesn't silently return "fine".

Rebase note

Rebased onto main after #488 landed. Both PRs touch globals.css; the two ordering invariants were re-verified after the merge — .card--hero (:242) still follows .card (:210), and .pending-* (:278) still precedes .anim-d* (:306).

@AndresL230
AndresL230 merged commit b335dfb into mainJul 31, 2026
7 checks passed
@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Member

Automated review pass (Claude Code), originally run against f8eb2ac and re-checked after the force-push to 8e6d2b4. Independently verified at the original head: extraction is genuinely complete (zero residual gradient literals outside the token/fallbacks/test constant), the PR body's drift table is accurate row-for-row against base, .card--hero overrides every .card property at equal-and-later specificity so card adoption is cascade-neutral, testids preserved, tsc clean + full vitest suite green. The e2e-cycle comment and the [P1-E] tick landed since — both good.

Still outstanding

  1. The PR body promises "Before/after screenshots in a comment below," but no comment contains any image. Since this PR declares a deliberate visible change (cool→warm shadow hue, unified alphas, success modal gaining radius+inset), the visual sign-off it leans on isn't actually on the record — post the screenshots or amend the body.

Minor

  1. HeroCard.test.tsx — the anti-drift walk still scans only .ts/.tsx (its own comment at ~:65 notes globals.css "is not walked (only .ts/.tsx)"). A future inlined copy of the gradient in a new.css file (e.g. a CSS module) would evade the guard. The legitimate fallback literals are an argument for excluding app/globals.css by path, not the whole .css extension.

Note for future edits:.card--hero currently overrides everything .card declares, so the pairing is purely nominal today — any future addition to .card (padding, hover elevation, transition) will silently restyle all three modals. Worth one sentence in the globals.css comment block.

Verdict: ready to merge once the screenshots are posted (or the body reference removed).

Sign up for freeto 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.

2 participants

@AndresL230@Jose-Gael-Cruz-Lopez