feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine - #523

Closed
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port
Closed

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine#523
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Collaborator

Ports Sapling Landing v4.dc.html (3,329 lines) into the Next.js app. Draft — the engine layer is in, the components are not. Nothing renders yet; this is the foundation.

What's here

LayerFiles
Tokensglobals.css — a .landing-v4 scope, 10 palette values, 4 tier colours, all 35 keyframes
Assetskofi-symbol.png, journal-founding.png, journal-ai-homework.png
Datalib/landing/{course,content,color}.ts
Enginelib/landing/engine/ — 11 modules, one master tick

Scoped to .landing-v4 so it sits alongside the existing .landing-page surface rather than replacing it. The current landing keeps working.

Decisions worth reviewing

The greens are not the brand tokens.#0C5638 / #0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and --brand-forest-bright (#2D8F5C). Kept literal per the design brief; please don't consolidate them. Same reasoning for XTIER.unexplored (#9a9a9a), which is byte-identical to --state-neutral today but is a distinct concept — this follows the existing --fallback-muted precedent.

No animation library, deliberately. Checked the obvious swaps against the source's numerics:

  • d3-force for the drag-field sim (already a dependency) — forces are expressible and d3's default velocityDecay even matches, but this integrator uses a linear alpha ramp instead of exponential decay, single-pass collide, plus an idle drift and anchor spring. Same forces, different settling.
  • three / react-force-graph-3d for Act I — it's a hand-rolled fl / (fl + z) projection on 2D canvas with painter's-algorithm sorting. WebGL changes node edges and label metrics.
  • framer-motion useScroll/useSpring — scroll is a double-lerp (0.050.08) with a jump bypass. A spring is a different transfer function.

The source's imperative core is the efficient implementation: put() memoises every style write onto el.__p_<prop>, cv() caches canvas context and size. A library layer here is slower, not faster.

Three rAF loops, not one (the brief said one): master tick, sim tick, hero canvas. Each gates itself on visibility. measure.ts is the only module that reads layout, throttled to 600ms — with one deliberate exception, sim.step's per-frame getBoundingClientRect visibility check, ported as-is rather than "fixed" into different behaviour.

Verification

tsc --noEmit, eslint src/lib/landing/, and a lightningcss parse of globals.css all clean, run against main's dependency set. No keyframe collisions.

Not verified: anything visual. There's no mount point yet, so "clean" means it compiles — not that it looks right.

Still to come

Section components (~1,500 lines of source markup), the route, then the first real visual comparison against the design.

🤖 Generated with Claude Code

Darkest-Teddyand others added 3 commits August 3, 2026 22:11
Ports the design-token layer from `Sapling Landing v4.dc.html` into a
.landing-v4 scope so it can be built alongside the existing .landing-page
surface rather than replacing it in place.
- 10 palette values + the 4 mastery-tier colours, as literal source values.
These are deliberately NOT aliased onto the app's brand tokens: #0C5638 /
#0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and
--brand-forest-bright (#2D8F5C).
- All 35 keyframes verbatim, including the six s1* intro-lockup frames whose
per-keyframe animation-timing-function declarations are the effect —
collapsing them onto one shared curve destroys the leaf weight.
- The three responsive rules and the scrollbar treatment.
Fonts bind to the existing next/font variables; DM Sans, Spectral, Playfair
Display and JetBrains Mono are exactly the four families the source loads, so
no extra webfont request is added. prefers-reduced-motion is not repeated —
the existing global block is already a superset of the source's rule.
Assets recovered from the design project: kofi-symbol.png (321x258) plus the
two journal photos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The single hard-coded course graph the whole landing page is built on. This
is not decorative filler — Act I, explore mode and the gallery all render
this same data, so it is ported node for node: 7 units, 42 concepts, every
authored 3D position, mastery score and blurb.
- course.ts XTIER / XTIER_LABEL / XTIER_ORDER, COURSE, nodeUses()
- content.ts SCRAMBLE, LAB_KIND, GAL, FAQS, POST_META, POSTS
- color.ts the interpolation maths the scroll engine runs per frame
XTIER.unexplored (#9a9a9a) is byte-identical to --state-neutral today but is
kept literal, following the same reasoning as --fallback-muted in globals.css:
it is a distinct concept, so a future change to the app's neutral state must
not silently move a landing-page mastery tier.
lerpTint keeps its hue-borrowing rule — grey has no hue of its own, so it
borrows the destination's and lets saturation carry the change, which is what
stops an unexplored node washing through a dead desaturated midpoint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The imperative core of the landing page. Kept out of React state and out of
an animation library on purpose: put() memoises every style write onto
el.__p_<prop> so an unchanged value never touches the DOM, and cv() caches
canvas context + size so no frame does a layout read. Wrapping this in
framer-motion would add a layer and make it slower, not faster.
Three loops, matching the source:
- LandingEngine.tick drives all three acts, the section nav, the ambient
canvas, the marquee and the plant field
- LandingEngine.simTick the drag-field force sim, gated on cluster visibility
- hero.ts the hero point cloud, gated past one viewport of scroll
measure.ts is the only module that reads layout, throttled to 600ms. Scroll
position is a double-lerp (raw -> 0.05 -> 0.08) with a bypass so programmatic
nav jumps land 1:1 instead of being smoothed into a glide.
sim.ts is hand-rolled rather than delegated to d3-force, which is already a
dependency. The forces are expressible in d3 and its default velocityDecay
even matches, but this integrator uses a linear alpha ramp instead of d3's
exponential decay, resolves collide in a single pass, and adds an idle drift
plus an anchor spring. Same forces, different settling.
graphView.ts draws the scroll view and explore mode from one projection.
Entering explore continues each node's grey-to-colour reveal from wherever it
had reached rather than restarting it, which is why it doesn't flash.
No mount point yet — components and the route follow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 16f66de7-6a90-4f6b-923e-ee0fc9998103

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@supabase

supabaseBot commented Aug 4, 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 ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 4, 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-staging862bc48Commit Preview URL

Branch Preview URL
Aug 04 2026, 01:39 PM

First mounted sections, on their own /v4 route so the existing landing keeps
working and the two can be compared side by side.
useLandingV4 is the React/engine bridge. React owns the discrete state (open
card, tutor mode, open FAQ); the engine owns everything per-frame and writes
straight to the DOM. State the engine reads is mirrored into a ref so its
getters never close over a stale render.
- IntroOverlay: the six s1* animations, 2.4s with `both` fill. Ordering is
causal — wordmark, stem, bud at the moment the stem tops out, then each leaf
from the joint the stem already passed, rule last.
- Navbar: fades in with the hero, auto-hides on downward scroll (imperative,
off the scroll listener rather than state), collapses under 1180px, hides in
explore mode.
- Hero: the four .floating-card surfaces are positioned in CSS but transformed
by the engine per frame, so they carry data-base-rot / data-float-delay /
data-float-dur instead of CSS animations.
The design's `style-hover` attribute has no inline equivalent in React, so
those 62 rules become real CSS under .landing-v4 — same values.
Refs are declared flat rather than in an object: React's refs lint rule
rejects `ref={obj.someRef}` in JSX.
Marketing links point at /about (exists) plus /team, /wiki, /gallery, /news
and /faq, which are the design's intended routes and are not built yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant

@Darkest-Teddy
, '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" + '
Skip to content

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine - #523

Closed
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port
Closed

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine#523
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Collaborator

Ports Sapling Landing v4.dc.html (3,329 lines) into the Next.js app. Draft — the engine layer is in, the components are not. Nothing renders yet; this is the foundation.

What's here

LayerFiles
Tokensglobals.css — a .landing-v4 scope, 10 palette values, 4 tier colours, all 35 keyframes
Assetskofi-symbol.png, journal-founding.png, journal-ai-homework.png
Datalib/landing/{course,content,color}.ts
Enginelib/landing/engine/ — 11 modules, one master tick

Scoped to .landing-v4 so it sits alongside the existing .landing-page surface rather than replacing it. The current landing keeps working.

Decisions worth reviewing

The greens are not the brand tokens.#0C5638 / #0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and --brand-forest-bright (#2D8F5C). Kept literal per the design brief; please don't consolidate them. Same reasoning for XTIER.unexplored (#9a9a9a), which is byte-identical to --state-neutral today but is a distinct concept — this follows the existing --fallback-muted precedent.

No animation library, deliberately. Checked the obvious swaps against the source's numerics:

  • d3-force for the drag-field sim (already a dependency) — forces are expressible and d3's default velocityDecay even matches, but this integrator uses a linear alpha ramp instead of exponential decay, single-pass collide, plus an idle drift and anchor spring. Same forces, different settling.
  • three / react-force-graph-3d for Act I — it's a hand-rolled fl / (fl + z) projection on 2D canvas with painter's-algorithm sorting. WebGL changes node edges and label metrics.
  • framer-motion useScroll/useSpring — scroll is a double-lerp (0.050.08) with a jump bypass. A spring is a different transfer function.

The source's imperative core is the efficient implementation: put() memoises every style write onto el.__p_<prop>, cv() caches canvas context and size. A library layer here is slower, not faster.

Three rAF loops, not one (the brief said one): master tick, sim tick, hero canvas. Each gates itself on visibility. measure.ts is the only module that reads layout, throttled to 600ms — with one deliberate exception, sim.step's per-frame getBoundingClientRect visibility check, ported as-is rather than "fixed" into different behaviour.

Verification

tsc --noEmit, eslint src/lib/landing/, and a lightningcss parse of globals.css all clean, run against main's dependency set. No keyframe collisions.

Not verified: anything visual. There's no mount point yet, so "clean" means it compiles — not that it looks right.

Still to come

Section components (~1,500 lines of source markup), the route, then the first real visual comparison against the design.

🤖 Generated with Claude Code

Darkest-Teddyand others added 3 commits August 3, 2026 22:11
Ports the design-token layer from `Sapling Landing v4.dc.html` into a
.landing-v4 scope so it can be built alongside the existing .landing-page
surface rather than replacing it in place.
- 10 palette values + the 4 mastery-tier colours, as literal source values.
These are deliberately NOT aliased onto the app's brand tokens: #0C5638 /
#0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and
--brand-forest-bright (#2D8F5C).
- All 35 keyframes verbatim, including the six s1* intro-lockup frames whose
per-keyframe animation-timing-function declarations are the effect —
collapsing them onto one shared curve destroys the leaf weight.
- The three responsive rules and the scrollbar treatment.
Fonts bind to the existing next/font variables; DM Sans, Spectral, Playfair
Display and JetBrains Mono are exactly the four families the source loads, so
no extra webfont request is added. prefers-reduced-motion is not repeated —
the existing global block is already a superset of the source's rule.
Assets recovered from the design project: kofi-symbol.png (321x258) plus the
two journal photos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The single hard-coded course graph the whole landing page is built on. This
is not decorative filler — Act I, explore mode and the gallery all render
this same data, so it is ported node for node: 7 units, 42 concepts, every
authored 3D position, mastery score and blurb.
- course.ts XTIER / XTIER_LABEL / XTIER_ORDER, COURSE, nodeUses()
- content.ts SCRAMBLE, LAB_KIND, GAL, FAQS, POST_META, POSTS
- color.ts the interpolation maths the scroll engine runs per frame
XTIER.unexplored (#9a9a9a) is byte-identical to --state-neutral today but is
kept literal, following the same reasoning as --fallback-muted in globals.css:
it is a distinct concept, so a future change to the app's neutral state must
not silently move a landing-page mastery tier.
lerpTint keeps its hue-borrowing rule — grey has no hue of its own, so it
borrows the destination's and lets saturation carry the change, which is what
stops an unexplored node washing through a dead desaturated midpoint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The imperative core of the landing page. Kept out of React state and out of
an animation library on purpose: put() memoises every style write onto
el.__p_<prop> so an unchanged value never touches the DOM, and cv() caches
canvas context + size so no frame does a layout read. Wrapping this in
framer-motion would add a layer and make it slower, not faster.
Three loops, matching the source:
- LandingEngine.tick drives all three acts, the section nav, the ambient
canvas, the marquee and the plant field
- LandingEngine.simTick the drag-field force sim, gated on cluster visibility
- hero.ts the hero point cloud, gated past one viewport of scroll
measure.ts is the only module that reads layout, throttled to 600ms. Scroll
position is a double-lerp (raw -> 0.05 -> 0.08) with a bypass so programmatic
nav jumps land 1:1 instead of being smoothed into a glide.
sim.ts is hand-rolled rather than delegated to d3-force, which is already a
dependency. The forces are expressible in d3 and its default velocityDecay
even matches, but this integrator uses a linear alpha ramp instead of d3's
exponential decay, resolves collide in a single pass, and adds an idle drift
plus an anchor spring. Same forces, different settling.
graphView.ts draws the scroll view and explore mode from one projection.
Entering explore continues each node's grey-to-colour reveal from wherever it
had reached rather than restarting it, which is why it doesn't flash.
No mount point yet — components and the route follow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 16f66de7-6a90-4f6b-923e-ee0fc9998103

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@supabase

supabaseBot commented Aug 4, 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 ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 4, 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-staging862bc48Commit Preview URL

Branch Preview URL
Aug 04 2026, 01:39 PM

First mounted sections, on their own /v4 route so the existing landing keeps
working and the two can be compared side by side.
useLandingV4 is the React/engine bridge. React owns the discrete state (open
card, tutor mode, open FAQ); the engine owns everything per-frame and writes
straight to the DOM. State the engine reads is mirrored into a ref so its
getters never close over a stale render.
- IntroOverlay: the six s1* animations, 2.4s with `both` fill. Ordering is
causal — wordmark, stem, bud at the moment the stem tops out, then each leaf
from the joint the stem already passed, rule last.
- Navbar: fades in with the hero, auto-hides on downward scroll (imperative,
off the scroll listener rather than state), collapses under 1180px, hides in
explore mode.
- Hero: the four .floating-card surfaces are positioned in CSS but transformed
by the engine per frame, so they carry data-base-rot / data-float-delay /
data-float-dur instead of CSS animations.
The design's `style-hover` attribute has no inline equivalent in React, so
those 62 rules become real CSS under .landing-v4 — same values.
Refs are declared flat rather than in an object: React's refs lint rule
rejects `ref={obj.someRef}` in JSX.
Marketing links point at /about (exists) plus /team, /wiki, /gallery, /news
and /faq, which are the design's intended routes and are not built yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant

@Darkest-Teddy
, '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('^' + ".*" + '
Skip to content

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine - #523

Closed
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port
Closed

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine#523
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Collaborator

Ports Sapling Landing v4.dc.html (3,329 lines) into the Next.js app. Draft — the engine layer is in, the components are not. Nothing renders yet; this is the foundation.

What's here

LayerFiles
Tokensglobals.css — a .landing-v4 scope, 10 palette values, 4 tier colours, all 35 keyframes
Assetskofi-symbol.png, journal-founding.png, journal-ai-homework.png
Datalib/landing/{course,content,color}.ts
Enginelib/landing/engine/ — 11 modules, one master tick

Scoped to .landing-v4 so it sits alongside the existing .landing-page surface rather than replacing it. The current landing keeps working.

Decisions worth reviewing

The greens are not the brand tokens.#0C5638 / #0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and --brand-forest-bright (#2D8F5C). Kept literal per the design brief; please don't consolidate them. Same reasoning for XTIER.unexplored (#9a9a9a), which is byte-identical to --state-neutral today but is a distinct concept — this follows the existing --fallback-muted precedent.

No animation library, deliberately. Checked the obvious swaps against the source's numerics:

  • d3-force for the drag-field sim (already a dependency) — forces are expressible and d3's default velocityDecay even matches, but this integrator uses a linear alpha ramp instead of exponential decay, single-pass collide, plus an idle drift and anchor spring. Same forces, different settling.
  • three / react-force-graph-3d for Act I — it's a hand-rolled fl / (fl + z) projection on 2D canvas with painter's-algorithm sorting. WebGL changes node edges and label metrics.
  • framer-motion useScroll/useSpring — scroll is a double-lerp (0.050.08) with a jump bypass. A spring is a different transfer function.

The source's imperative core is the efficient implementation: put() memoises every style write onto el.__p_<prop>, cv() caches canvas context and size. A library layer here is slower, not faster.

Three rAF loops, not one (the brief said one): master tick, sim tick, hero canvas. Each gates itself on visibility. measure.ts is the only module that reads layout, throttled to 600ms — with one deliberate exception, sim.step's per-frame getBoundingClientRect visibility check, ported as-is rather than "fixed" into different behaviour.

Verification

tsc --noEmit, eslint src/lib/landing/, and a lightningcss parse of globals.css all clean, run against main's dependency set. No keyframe collisions.

Not verified: anything visual. There's no mount point yet, so "clean" means it compiles — not that it looks right.

Still to come

Section components (~1,500 lines of source markup), the route, then the first real visual comparison against the design.

🤖 Generated with Claude Code

Darkest-Teddyand others added 3 commits August 3, 2026 22:11
Ports the design-token layer from `Sapling Landing v4.dc.html` into a
.landing-v4 scope so it can be built alongside the existing .landing-page
surface rather than replacing it in place.
- 10 palette values + the 4 mastery-tier colours, as literal source values.
These are deliberately NOT aliased onto the app's brand tokens: #0C5638 /
#0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and
--brand-forest-bright (#2D8F5C).
- All 35 keyframes verbatim, including the six s1* intro-lockup frames whose
per-keyframe animation-timing-function declarations are the effect —
collapsing them onto one shared curve destroys the leaf weight.
- The three responsive rules and the scrollbar treatment.
Fonts bind to the existing next/font variables; DM Sans, Spectral, Playfair
Display and JetBrains Mono are exactly the four families the source loads, so
no extra webfont request is added. prefers-reduced-motion is not repeated —
the existing global block is already a superset of the source's rule.
Assets recovered from the design project: kofi-symbol.png (321x258) plus the
two journal photos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The single hard-coded course graph the whole landing page is built on. This
is not decorative filler — Act I, explore mode and the gallery all render
this same data, so it is ported node for node: 7 units, 42 concepts, every
authored 3D position, mastery score and blurb.
- course.ts XTIER / XTIER_LABEL / XTIER_ORDER, COURSE, nodeUses()
- content.ts SCRAMBLE, LAB_KIND, GAL, FAQS, POST_META, POSTS
- color.ts the interpolation maths the scroll engine runs per frame
XTIER.unexplored (#9a9a9a) is byte-identical to --state-neutral today but is
kept literal, following the same reasoning as --fallback-muted in globals.css:
it is a distinct concept, so a future change to the app's neutral state must
not silently move a landing-page mastery tier.
lerpTint keeps its hue-borrowing rule — grey has no hue of its own, so it
borrows the destination's and lets saturation carry the change, which is what
stops an unexplored node washing through a dead desaturated midpoint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The imperative core of the landing page. Kept out of React state and out of
an animation library on purpose: put() memoises every style write onto
el.__p_<prop> so an unchanged value never touches the DOM, and cv() caches
canvas context + size so no frame does a layout read. Wrapping this in
framer-motion would add a layer and make it slower, not faster.
Three loops, matching the source:
- LandingEngine.tick drives all three acts, the section nav, the ambient
canvas, the marquee and the plant field
- LandingEngine.simTick the drag-field force sim, gated on cluster visibility
- hero.ts the hero point cloud, gated past one viewport of scroll
measure.ts is the only module that reads layout, throttled to 600ms. Scroll
position is a double-lerp (raw -> 0.05 -> 0.08) with a bypass so programmatic
nav jumps land 1:1 instead of being smoothed into a glide.
sim.ts is hand-rolled rather than delegated to d3-force, which is already a
dependency. The forces are expressible in d3 and its default velocityDecay
even matches, but this integrator uses a linear alpha ramp instead of d3's
exponential decay, resolves collide in a single pass, and adds an idle drift
plus an anchor spring. Same forces, different settling.
graphView.ts draws the scroll view and explore mode from one projection.
Entering explore continues each node's grey-to-colour reveal from wherever it
had reached rather than restarting it, which is why it doesn't flash.
No mount point yet — components and the route follow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 16f66de7-6a90-4f6b-923e-ee0fc9998103

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@supabase

supabaseBot commented Aug 4, 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 ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 4, 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-staging862bc48Commit Preview URL

Branch Preview URL
Aug 04 2026, 01:39 PM

First mounted sections, on their own /v4 route so the existing landing keeps
working and the two can be compared side by side.
useLandingV4 is the React/engine bridge. React owns the discrete state (open
card, tutor mode, open FAQ); the engine owns everything per-frame and writes
straight to the DOM. State the engine reads is mirrored into a ref so its
getters never close over a stale render.
- IntroOverlay: the six s1* animations, 2.4s with `both` fill. Ordering is
causal — wordmark, stem, bud at the moment the stem tops out, then each leaf
from the joint the stem already passed, rule last.
- Navbar: fades in with the hero, auto-hides on downward scroll (imperative,
off the scroll listener rather than state), collapses under 1180px, hides in
explore mode.
- Hero: the four .floating-card surfaces are positioned in CSS but transformed
by the engine per frame, so they carry data-base-rot / data-float-delay /
data-float-dur instead of CSS animations.
The design's `style-hover` attribute has no inline equivalent in React, so
those 62 rules become real CSS under .landing-v4 — same values.
Refs are declared flat rather than in an object: React's refs lint rule
rejects `ref={obj.someRef}` in JSX.
Marketing links point at /about (exists) plus /team, /wiki, /gallery, /news
and /faq, which are the design's intended routes and are not built yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant

@Darkest-Teddy
, '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('^' + ".*" + '
Skip to content

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine - #523

Closed
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port
Closed

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine#523
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Collaborator

Ports Sapling Landing v4.dc.html (3,329 lines) into the Next.js app. Draft — the engine layer is in, the components are not. Nothing renders yet; this is the foundation.

What's here

LayerFiles
Tokensglobals.css — a .landing-v4 scope, 10 palette values, 4 tier colours, all 35 keyframes
Assetskofi-symbol.png, journal-founding.png, journal-ai-homework.png
Datalib/landing/{course,content,color}.ts
Enginelib/landing/engine/ — 11 modules, one master tick

Scoped to .landing-v4 so it sits alongside the existing .landing-page surface rather than replacing it. The current landing keeps working.

Decisions worth reviewing

The greens are not the brand tokens.#0C5638 / #0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and --brand-forest-bright (#2D8F5C). Kept literal per the design brief; please don't consolidate them. Same reasoning for XTIER.unexplored (#9a9a9a), which is byte-identical to --state-neutral today but is a distinct concept — this follows the existing --fallback-muted precedent.

No animation library, deliberately. Checked the obvious swaps against the source's numerics:

  • d3-force for the drag-field sim (already a dependency) — forces are expressible and d3's default velocityDecay even matches, but this integrator uses a linear alpha ramp instead of exponential decay, single-pass collide, plus an idle drift and anchor spring. Same forces, different settling.
  • three / react-force-graph-3d for Act I — it's a hand-rolled fl / (fl + z) projection on 2D canvas with painter's-algorithm sorting. WebGL changes node edges and label metrics.
  • framer-motion useScroll/useSpring — scroll is a double-lerp (0.050.08) with a jump bypass. A spring is a different transfer function.

The source's imperative core is the efficient implementation: put() memoises every style write onto el.__p_<prop>, cv() caches canvas context and size. A library layer here is slower, not faster.

Three rAF loops, not one (the brief said one): master tick, sim tick, hero canvas. Each gates itself on visibility. measure.ts is the only module that reads layout, throttled to 600ms — with one deliberate exception, sim.step's per-frame getBoundingClientRect visibility check, ported as-is rather than "fixed" into different behaviour.

Verification

tsc --noEmit, eslint src/lib/landing/, and a lightningcss parse of globals.css all clean, run against main's dependency set. No keyframe collisions.

Not verified: anything visual. There's no mount point yet, so "clean" means it compiles — not that it looks right.

Still to come

Section components (~1,500 lines of source markup), the route, then the first real visual comparison against the design.

🤖 Generated with Claude Code

Darkest-Teddyand others added 3 commits August 3, 2026 22:11
Ports the design-token layer from `Sapling Landing v4.dc.html` into a
.landing-v4 scope so it can be built alongside the existing .landing-page
surface rather than replacing it in place.
- 10 palette values + the 4 mastery-tier colours, as literal source values.
These are deliberately NOT aliased onto the app's brand tokens: #0C5638 /
#0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and
--brand-forest-bright (#2D8F5C).
- All 35 keyframes verbatim, including the six s1* intro-lockup frames whose
per-keyframe animation-timing-function declarations are the effect —
collapsing them onto one shared curve destroys the leaf weight.
- The three responsive rules and the scrollbar treatment.
Fonts bind to the existing next/font variables; DM Sans, Spectral, Playfair
Display and JetBrains Mono are exactly the four families the source loads, so
no extra webfont request is added. prefers-reduced-motion is not repeated —
the existing global block is already a superset of the source's rule.
Assets recovered from the design project: kofi-symbol.png (321x258) plus the
two journal photos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The single hard-coded course graph the whole landing page is built on. This
is not decorative filler — Act I, explore mode and the gallery all render
this same data, so it is ported node for node: 7 units, 42 concepts, every
authored 3D position, mastery score and blurb.
- course.ts XTIER / XTIER_LABEL / XTIER_ORDER, COURSE, nodeUses()
- content.ts SCRAMBLE, LAB_KIND, GAL, FAQS, POST_META, POSTS
- color.ts the interpolation maths the scroll engine runs per frame
XTIER.unexplored (#9a9a9a) is byte-identical to --state-neutral today but is
kept literal, following the same reasoning as --fallback-muted in globals.css:
it is a distinct concept, so a future change to the app's neutral state must
not silently move a landing-page mastery tier.
lerpTint keeps its hue-borrowing rule — grey has no hue of its own, so it
borrows the destination's and lets saturation carry the change, which is what
stops an unexplored node washing through a dead desaturated midpoint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The imperative core of the landing page. Kept out of React state and out of
an animation library on purpose: put() memoises every style write onto
el.__p_<prop> so an unchanged value never touches the DOM, and cv() caches
canvas context + size so no frame does a layout read. Wrapping this in
framer-motion would add a layer and make it slower, not faster.
Three loops, matching the source:
- LandingEngine.tick drives all three acts, the section nav, the ambient
canvas, the marquee and the plant field
- LandingEngine.simTick the drag-field force sim, gated on cluster visibility
- hero.ts the hero point cloud, gated past one viewport of scroll
measure.ts is the only module that reads layout, throttled to 600ms. Scroll
position is a double-lerp (raw -> 0.05 -> 0.08) with a bypass so programmatic
nav jumps land 1:1 instead of being smoothed into a glide.
sim.ts is hand-rolled rather than delegated to d3-force, which is already a
dependency. The forces are expressible in d3 and its default velocityDecay
even matches, but this integrator uses a linear alpha ramp instead of d3's
exponential decay, resolves collide in a single pass, and adds an idle drift
plus an anchor spring. Same forces, different settling.
graphView.ts draws the scroll view and explore mode from one projection.
Entering explore continues each node's grey-to-colour reveal from wherever it
had reached rather than restarting it, which is why it doesn't flash.
No mount point yet — components and the route follow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 16f66de7-6a90-4f6b-923e-ee0fc9998103

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@supabase

supabaseBot commented Aug 4, 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 ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 4, 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-staging862bc48Commit Preview URL

Branch Preview URL
Aug 04 2026, 01:39 PM

First mounted sections, on their own /v4 route so the existing landing keeps
working and the two can be compared side by side.
useLandingV4 is the React/engine bridge. React owns the discrete state (open
card, tutor mode, open FAQ); the engine owns everything per-frame and writes
straight to the DOM. State the engine reads is mirrored into a ref so its
getters never close over a stale render.
- IntroOverlay: the six s1* animations, 2.4s with `both` fill. Ordering is
causal — wordmark, stem, bud at the moment the stem tops out, then each leaf
from the joint the stem already passed, rule last.
- Navbar: fades in with the hero, auto-hides on downward scroll (imperative,
off the scroll listener rather than state), collapses under 1180px, hides in
explore mode.
- Hero: the four .floating-card surfaces are positioned in CSS but transformed
by the engine per frame, so they carry data-base-rot / data-float-delay /
data-float-dur instead of CSS animations.
The design's `style-hover` attribute has no inline equivalent in React, so
those 62 rules become real CSS under .landing-v4 — same values.
Refs are declared flat rather than in an object: React's refs lint rule
rejects `ref={obj.someRef}` in JSX.
Marketing links point at /about (exists) plus /team, /wiki, /gallery, /news
and /faq, which are the design's intended routes and are not built yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant

@Darkest-Teddy
, '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" + '
Skip to content

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine - #523

Closed
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port
Closed

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine#523
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Collaborator

Ports Sapling Landing v4.dc.html (3,329 lines) into the Next.js app. Draft — the engine layer is in, the components are not. Nothing renders yet; this is the foundation.

What's here

LayerFiles
Tokensglobals.css — a .landing-v4 scope, 10 palette values, 4 tier colours, all 35 keyframes
Assetskofi-symbol.png, journal-founding.png, journal-ai-homework.png
Datalib/landing/{course,content,color}.ts
Enginelib/landing/engine/ — 11 modules, one master tick

Scoped to .landing-v4 so it sits alongside the existing .landing-page surface rather than replacing it. The current landing keeps working.

Decisions worth reviewing

The greens are not the brand tokens.#0C5638 / #0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and --brand-forest-bright (#2D8F5C). Kept literal per the design brief; please don't consolidate them. Same reasoning for XTIER.unexplored (#9a9a9a), which is byte-identical to --state-neutral today but is a distinct concept — this follows the existing --fallback-muted precedent.

No animation library, deliberately. Checked the obvious swaps against the source's numerics:

  • d3-force for the drag-field sim (already a dependency) — forces are expressible and d3's default velocityDecay even matches, but this integrator uses a linear alpha ramp instead of exponential decay, single-pass collide, plus an idle drift and anchor spring. Same forces, different settling.
  • three / react-force-graph-3d for Act I — it's a hand-rolled fl / (fl + z) projection on 2D canvas with painter's-algorithm sorting. WebGL changes node edges and label metrics.
  • framer-motion useScroll/useSpring — scroll is a double-lerp (0.050.08) with a jump bypass. A spring is a different transfer function.

The source's imperative core is the efficient implementation: put() memoises every style write onto el.__p_<prop>, cv() caches canvas context and size. A library layer here is slower, not faster.

Three rAF loops, not one (the brief said one): master tick, sim tick, hero canvas. Each gates itself on visibility. measure.ts is the only module that reads layout, throttled to 600ms — with one deliberate exception, sim.step's per-frame getBoundingClientRect visibility check, ported as-is rather than "fixed" into different behaviour.

Verification

tsc --noEmit, eslint src/lib/landing/, and a lightningcss parse of globals.css all clean, run against main's dependency set. No keyframe collisions.

Not verified: anything visual. There's no mount point yet, so "clean" means it compiles — not that it looks right.

Still to come

Section components (~1,500 lines of source markup), the route, then the first real visual comparison against the design.

🤖 Generated with Claude Code

Darkest-Teddyand others added 3 commits August 3, 2026 22:11
Ports the design-token layer from `Sapling Landing v4.dc.html` into a
.landing-v4 scope so it can be built alongside the existing .landing-page
surface rather than replacing it in place.
- 10 palette values + the 4 mastery-tier colours, as literal source values.
These are deliberately NOT aliased onto the app's brand tokens: #0C5638 /
#0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and
--brand-forest-bright (#2D8F5C).
- All 35 keyframes verbatim, including the six s1* intro-lockup frames whose
per-keyframe animation-timing-function declarations are the effect —
collapsing them onto one shared curve destroys the leaf weight.
- The three responsive rules and the scrollbar treatment.
Fonts bind to the existing next/font variables; DM Sans, Spectral, Playfair
Display and JetBrains Mono are exactly the four families the source loads, so
no extra webfont request is added. prefers-reduced-motion is not repeated —
the existing global block is already a superset of the source's rule.
Assets recovered from the design project: kofi-symbol.png (321x258) plus the
two journal photos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The single hard-coded course graph the whole landing page is built on. This
is not decorative filler — Act I, explore mode and the gallery all render
this same data, so it is ported node for node: 7 units, 42 concepts, every
authored 3D position, mastery score and blurb.
- course.ts XTIER / XTIER_LABEL / XTIER_ORDER, COURSE, nodeUses()
- content.ts SCRAMBLE, LAB_KIND, GAL, FAQS, POST_META, POSTS
- color.ts the interpolation maths the scroll engine runs per frame
XTIER.unexplored (#9a9a9a) is byte-identical to --state-neutral today but is
kept literal, following the same reasoning as --fallback-muted in globals.css:
it is a distinct concept, so a future change to the app's neutral state must
not silently move a landing-page mastery tier.
lerpTint keeps its hue-borrowing rule — grey has no hue of its own, so it
borrows the destination's and lets saturation carry the change, which is what
stops an unexplored node washing through a dead desaturated midpoint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The imperative core of the landing page. Kept out of React state and out of
an animation library on purpose: put() memoises every style write onto
el.__p_<prop> so an unchanged value never touches the DOM, and cv() caches
canvas context + size so no frame does a layout read. Wrapping this in
framer-motion would add a layer and make it slower, not faster.
Three loops, matching the source:
- LandingEngine.tick drives all three acts, the section nav, the ambient
canvas, the marquee and the plant field
- LandingEngine.simTick the drag-field force sim, gated on cluster visibility
- hero.ts the hero point cloud, gated past one viewport of scroll
measure.ts is the only module that reads layout, throttled to 600ms. Scroll
position is a double-lerp (raw -> 0.05 -> 0.08) with a bypass so programmatic
nav jumps land 1:1 instead of being smoothed into a glide.
sim.ts is hand-rolled rather than delegated to d3-force, which is already a
dependency. The forces are expressible in d3 and its default velocityDecay
even matches, but this integrator uses a linear alpha ramp instead of d3's
exponential decay, resolves collide in a single pass, and adds an idle drift
plus an anchor spring. Same forces, different settling.
graphView.ts draws the scroll view and explore mode from one projection.
Entering explore continues each node's grey-to-colour reveal from wherever it
had reached rather than restarting it, which is why it doesn't flash.
No mount point yet — components and the route follow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 16f66de7-6a90-4f6b-923e-ee0fc9998103

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@supabase

supabaseBot commented Aug 4, 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 ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 4, 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-staging862bc48Commit Preview URL

Branch Preview URL
Aug 04 2026, 01:39 PM

First mounted sections, on their own /v4 route so the existing landing keeps
working and the two can be compared side by side.
useLandingV4 is the React/engine bridge. React owns the discrete state (open
card, tutor mode, open FAQ); the engine owns everything per-frame and writes
straight to the DOM. State the engine reads is mirrored into a ref so its
getters never close over a stale render.
- IntroOverlay: the six s1* animations, 2.4s with `both` fill. Ordering is
causal — wordmark, stem, bud at the moment the stem tops out, then each leaf
from the joint the stem already passed, rule last.
- Navbar: fades in with the hero, auto-hides on downward scroll (imperative,
off the scroll listener rather than state), collapses under 1180px, hides in
explore mode.
- Hero: the four .floating-card surfaces are positioned in CSS but transformed
by the engine per frame, so they carry data-base-rot / data-float-delay /
data-float-dur instead of CSS animations.
The design's `style-hover` attribute has no inline equivalent in React, so
those 62 rules become real CSS under .landing-v4 — same values.
Refs are declared flat rather than in an object: React's refs lint rule
rejects `ref={obj.someRef}` in JSX.
Marketing links point at /about (exists) plus /team, /wiki, /gallery, /news
and /faq, which are the design's intended routes and are not built yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant

@Darkest-Teddy
, '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('^' + ".*" + '
Skip to content

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine - #523

Closed
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port
Closed

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine#523
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Collaborator

Ports Sapling Landing v4.dc.html (3,329 lines) into the Next.js app. Draft — the engine layer is in, the components are not. Nothing renders yet; this is the foundation.

What's here

LayerFiles
Tokensglobals.css — a .landing-v4 scope, 10 palette values, 4 tier colours, all 35 keyframes
Assetskofi-symbol.png, journal-founding.png, journal-ai-homework.png
Datalib/landing/{course,content,color}.ts
Enginelib/landing/engine/ — 11 modules, one master tick

Scoped to .landing-v4 so it sits alongside the existing .landing-page surface rather than replacing it. The current landing keeps working.

Decisions worth reviewing

The greens are not the brand tokens.#0C5638 / #0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and --brand-forest-bright (#2D8F5C). Kept literal per the design brief; please don't consolidate them. Same reasoning for XTIER.unexplored (#9a9a9a), which is byte-identical to --state-neutral today but is a distinct concept — this follows the existing --fallback-muted precedent.

No animation library, deliberately. Checked the obvious swaps against the source's numerics:

  • d3-force for the drag-field sim (already a dependency) — forces are expressible and d3's default velocityDecay even matches, but this integrator uses a linear alpha ramp instead of exponential decay, single-pass collide, plus an idle drift and anchor spring. Same forces, different settling.
  • three / react-force-graph-3d for Act I — it's a hand-rolled fl / (fl + z) projection on 2D canvas with painter's-algorithm sorting. WebGL changes node edges and label metrics.
  • framer-motion useScroll/useSpring — scroll is a double-lerp (0.050.08) with a jump bypass. A spring is a different transfer function.

The source's imperative core is the efficient implementation: put() memoises every style write onto el.__p_<prop>, cv() caches canvas context and size. A library layer here is slower, not faster.

Three rAF loops, not one (the brief said one): master tick, sim tick, hero canvas. Each gates itself on visibility. measure.ts is the only module that reads layout, throttled to 600ms — with one deliberate exception, sim.step's per-frame getBoundingClientRect visibility check, ported as-is rather than "fixed" into different behaviour.

Verification

tsc --noEmit, eslint src/lib/landing/, and a lightningcss parse of globals.css all clean, run against main's dependency set. No keyframe collisions.

Not verified: anything visual. There's no mount point yet, so "clean" means it compiles — not that it looks right.

Still to come

Section components (~1,500 lines of source markup), the route, then the first real visual comparison against the design.

🤖 Generated with Claude Code

Darkest-Teddyand others added 3 commits August 3, 2026 22:11
Ports the design-token layer from `Sapling Landing v4.dc.html` into a
.landing-v4 scope so it can be built alongside the existing .landing-page
surface rather than replacing it in place.
- 10 palette values + the 4 mastery-tier colours, as literal source values.
These are deliberately NOT aliased onto the app's brand tokens: #0C5638 /
#0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and
--brand-forest-bright (#2D8F5C).
- All 35 keyframes verbatim, including the six s1* intro-lockup frames whose
per-keyframe animation-timing-function declarations are the effect —
collapsing them onto one shared curve destroys the leaf weight.
- The three responsive rules and the scrollbar treatment.
Fonts bind to the existing next/font variables; DM Sans, Spectral, Playfair
Display and JetBrains Mono are exactly the four families the source loads, so
no extra webfont request is added. prefers-reduced-motion is not repeated —
the existing global block is already a superset of the source's rule.
Assets recovered from the design project: kofi-symbol.png (321x258) plus the
two journal photos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The single hard-coded course graph the whole landing page is built on. This
is not decorative filler — Act I, explore mode and the gallery all render
this same data, so it is ported node for node: 7 units, 42 concepts, every
authored 3D position, mastery score and blurb.
- course.ts XTIER / XTIER_LABEL / XTIER_ORDER, COURSE, nodeUses()
- content.ts SCRAMBLE, LAB_KIND, GAL, FAQS, POST_META, POSTS
- color.ts the interpolation maths the scroll engine runs per frame
XTIER.unexplored (#9a9a9a) is byte-identical to --state-neutral today but is
kept literal, following the same reasoning as --fallback-muted in globals.css:
it is a distinct concept, so a future change to the app's neutral state must
not silently move a landing-page mastery tier.
lerpTint keeps its hue-borrowing rule — grey has no hue of its own, so it
borrows the destination's and lets saturation carry the change, which is what
stops an unexplored node washing through a dead desaturated midpoint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The imperative core of the landing page. Kept out of React state and out of
an animation library on purpose: put() memoises every style write onto
el.__p_<prop> so an unchanged value never touches the DOM, and cv() caches
canvas context + size so no frame does a layout read. Wrapping this in
framer-motion would add a layer and make it slower, not faster.
Three loops, matching the source:
- LandingEngine.tick drives all three acts, the section nav, the ambient
canvas, the marquee and the plant field
- LandingEngine.simTick the drag-field force sim, gated on cluster visibility
- hero.ts the hero point cloud, gated past one viewport of scroll
measure.ts is the only module that reads layout, throttled to 600ms. Scroll
position is a double-lerp (raw -> 0.05 -> 0.08) with a bypass so programmatic
nav jumps land 1:1 instead of being smoothed into a glide.
sim.ts is hand-rolled rather than delegated to d3-force, which is already a
dependency. The forces are expressible in d3 and its default velocityDecay
even matches, but this integrator uses a linear alpha ramp instead of d3's
exponential decay, resolves collide in a single pass, and adds an idle drift
plus an anchor spring. Same forces, different settling.
graphView.ts draws the scroll view and explore mode from one projection.
Entering explore continues each node's grey-to-colour reveal from wherever it
had reached rather than restarting it, which is why it doesn't flash.
No mount point yet — components and the route follow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 16f66de7-6a90-4f6b-923e-ee0fc9998103

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@supabase

supabaseBot commented Aug 4, 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 ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 4, 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-staging862bc48Commit Preview URL

Branch Preview URL
Aug 04 2026, 01:39 PM

First mounted sections, on their own /v4 route so the existing landing keeps
working and the two can be compared side by side.
useLandingV4 is the React/engine bridge. React owns the discrete state (open
card, tutor mode, open FAQ); the engine owns everything per-frame and writes
straight to the DOM. State the engine reads is mirrored into a ref so its
getters never close over a stale render.
- IntroOverlay: the six s1* animations, 2.4s with `both` fill. Ordering is
causal — wordmark, stem, bud at the moment the stem tops out, then each leaf
from the joint the stem already passed, rule last.
- Navbar: fades in with the hero, auto-hides on downward scroll (imperative,
off the scroll listener rather than state), collapses under 1180px, hides in
explore mode.
- Hero: the four .floating-card surfaces are positioned in CSS but transformed
by the engine per frame, so they carry data-base-rot / data-float-delay /
data-float-dur instead of CSS animations.
The design's `style-hover` attribute has no inline equivalent in React, so
those 62 rules become real CSS under .landing-v4 — same values.
Refs are declared flat rather than in an object: React's refs lint rule
rejects `ref={obj.someRef}` in JSX.
Marketing links point at /about (exists) plus /team, /wiki, /gallery, /news
and /faq, which are the design's intended routes and are not built yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant

@Darkest-Teddy
, '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('^' + ".*" + '
Skip to content

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine - #523

Closed
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port
Closed

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine#523
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Collaborator

Ports Sapling Landing v4.dc.html (3,329 lines) into the Next.js app. Draft — the engine layer is in, the components are not. Nothing renders yet; this is the foundation.

What's here

LayerFiles
Tokensglobals.css — a .landing-v4 scope, 10 palette values, 4 tier colours, all 35 keyframes
Assetskofi-symbol.png, journal-founding.png, journal-ai-homework.png
Datalib/landing/{course,content,color}.ts
Enginelib/landing/engine/ — 11 modules, one master tick

Scoped to .landing-v4 so it sits alongside the existing .landing-page surface rather than replacing it. The current landing keeps working.

Decisions worth reviewing

The greens are not the brand tokens.#0C5638 / #0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and --brand-forest-bright (#2D8F5C). Kept literal per the design brief; please don't consolidate them. Same reasoning for XTIER.unexplored (#9a9a9a), which is byte-identical to --state-neutral today but is a distinct concept — this follows the existing --fallback-muted precedent.

No animation library, deliberately. Checked the obvious swaps against the source's numerics:

  • d3-force for the drag-field sim (already a dependency) — forces are expressible and d3's default velocityDecay even matches, but this integrator uses a linear alpha ramp instead of exponential decay, single-pass collide, plus an idle drift and anchor spring. Same forces, different settling.
  • three / react-force-graph-3d for Act I — it's a hand-rolled fl / (fl + z) projection on 2D canvas with painter's-algorithm sorting. WebGL changes node edges and label metrics.
  • framer-motion useScroll/useSpring — scroll is a double-lerp (0.050.08) with a jump bypass. A spring is a different transfer function.

The source's imperative core is the efficient implementation: put() memoises every style write onto el.__p_<prop>, cv() caches canvas context and size. A library layer here is slower, not faster.

Three rAF loops, not one (the brief said one): master tick, sim tick, hero canvas. Each gates itself on visibility. measure.ts is the only module that reads layout, throttled to 600ms — with one deliberate exception, sim.step's per-frame getBoundingClientRect visibility check, ported as-is rather than "fixed" into different behaviour.

Verification

tsc --noEmit, eslint src/lib/landing/, and a lightningcss parse of globals.css all clean, run against main's dependency set. No keyframe collisions.

Not verified: anything visual. There's no mount point yet, so "clean" means it compiles — not that it looks right.

Still to come

Section components (~1,500 lines of source markup), the route, then the first real visual comparison against the design.

🤖 Generated with Claude Code

Darkest-Teddyand others added 3 commits August 3, 2026 22:11
Ports the design-token layer from `Sapling Landing v4.dc.html` into a
.landing-v4 scope so it can be built alongside the existing .landing-page
surface rather than replacing it in place.
- 10 palette values + the 4 mastery-tier colours, as literal source values.
These are deliberately NOT aliased onto the app's brand tokens: #0C5638 /
#0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and
--brand-forest-bright (#2D8F5C).
- All 35 keyframes verbatim, including the six s1* intro-lockup frames whose
per-keyframe animation-timing-function declarations are the effect —
collapsing them onto one shared curve destroys the leaf weight.
- The three responsive rules and the scrollbar treatment.
Fonts bind to the existing next/font variables; DM Sans, Spectral, Playfair
Display and JetBrains Mono are exactly the four families the source loads, so
no extra webfont request is added. prefers-reduced-motion is not repeated —
the existing global block is already a superset of the source's rule.
Assets recovered from the design project: kofi-symbol.png (321x258) plus the
two journal photos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The single hard-coded course graph the whole landing page is built on. This
is not decorative filler — Act I, explore mode and the gallery all render
this same data, so it is ported node for node: 7 units, 42 concepts, every
authored 3D position, mastery score and blurb.
- course.ts XTIER / XTIER_LABEL / XTIER_ORDER, COURSE, nodeUses()
- content.ts SCRAMBLE, LAB_KIND, GAL, FAQS, POST_META, POSTS
- color.ts the interpolation maths the scroll engine runs per frame
XTIER.unexplored (#9a9a9a) is byte-identical to --state-neutral today but is
kept literal, following the same reasoning as --fallback-muted in globals.css:
it is a distinct concept, so a future change to the app's neutral state must
not silently move a landing-page mastery tier.
lerpTint keeps its hue-borrowing rule — grey has no hue of its own, so it
borrows the destination's and lets saturation carry the change, which is what
stops an unexplored node washing through a dead desaturated midpoint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The imperative core of the landing page. Kept out of React state and out of
an animation library on purpose: put() memoises every style write onto
el.__p_<prop> so an unchanged value never touches the DOM, and cv() caches
canvas context + size so no frame does a layout read. Wrapping this in
framer-motion would add a layer and make it slower, not faster.
Three loops, matching the source:
- LandingEngine.tick drives all three acts, the section nav, the ambient
canvas, the marquee and the plant field
- LandingEngine.simTick the drag-field force sim, gated on cluster visibility
- hero.ts the hero point cloud, gated past one viewport of scroll
measure.ts is the only module that reads layout, throttled to 600ms. Scroll
position is a double-lerp (raw -> 0.05 -> 0.08) with a bypass so programmatic
nav jumps land 1:1 instead of being smoothed into a glide.
sim.ts is hand-rolled rather than delegated to d3-force, which is already a
dependency. The forces are expressible in d3 and its default velocityDecay
even matches, but this integrator uses a linear alpha ramp instead of d3's
exponential decay, resolves collide in a single pass, and adds an idle drift
plus an anchor spring. Same forces, different settling.
graphView.ts draws the scroll view and explore mode from one projection.
Entering explore continues each node's grey-to-colour reveal from wherever it
had reached rather than restarting it, which is why it doesn't flash.
No mount point yet — components and the route follow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 16f66de7-6a90-4f6b-923e-ee0fc9998103

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@supabase

supabaseBot commented Aug 4, 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 ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 4, 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-staging862bc48Commit Preview URL

Branch Preview URL
Aug 04 2026, 01:39 PM

First mounted sections, on their own /v4 route so the existing landing keeps
working and the two can be compared side by side.
useLandingV4 is the React/engine bridge. React owns the discrete state (open
card, tutor mode, open FAQ); the engine owns everything per-frame and writes
straight to the DOM. State the engine reads is mirrored into a ref so its
getters never close over a stale render.
- IntroOverlay: the six s1* animations, 2.4s with `both` fill. Ordering is
causal — wordmark, stem, bud at the moment the stem tops out, then each leaf
from the joint the stem already passed, rule last.
- Navbar: fades in with the hero, auto-hides on downward scroll (imperative,
off the scroll listener rather than state), collapses under 1180px, hides in
explore mode.
- Hero: the four .floating-card surfaces are positioned in CSS but transformed
by the engine per frame, so they carry data-base-rot / data-float-delay /
data-float-dur instead of CSS animations.
The design's `style-hover` attribute has no inline equivalent in React, so
those 62 rules become real CSS under .landing-v4 — same values.
Refs are declared flat rather than in an object: React's refs lint rule
rejects `ref={obj.someRef}` in JSX.
Marketing links point at /about (exists) plus /team, /wiki, /gallery, /news
and /faq, which are the design's intended routes and are not built yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant

@Darkest-Teddy
, '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); } })(); })();
Skip to content

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine - #523

Closed
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port
Closed

feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine#523
Darkest-Teddy wants to merge 4 commits into
mainfrom
feat/landing-v4-port

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Collaborator

Ports Sapling Landing v4.dc.html (3,329 lines) into the Next.js app. Draft — the engine layer is in, the components are not. Nothing renders yet; this is the foundation.

What's here

LayerFiles
Tokensglobals.css — a .landing-v4 scope, 10 palette values, 4 tier colours, all 35 keyframes
Assetskofi-symbol.png, journal-founding.png, journal-ai-homework.png
Datalib/landing/{course,content,color}.ts
Enginelib/landing/engine/ — 11 modules, one master tick

Scoped to .landing-v4 so it sits alongside the existing .landing-page surface rather than replacing it. The current landing keeps working.

Decisions worth reviewing

The greens are not the brand tokens.#0C5638 / #0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and --brand-forest-bright (#2D8F5C). Kept literal per the design brief; please don't consolidate them. Same reasoning for XTIER.unexplored (#9a9a9a), which is byte-identical to --state-neutral today but is a distinct concept — this follows the existing --fallback-muted precedent.

No animation library, deliberately. Checked the obvious swaps against the source's numerics:

  • d3-force for the drag-field sim (already a dependency) — forces are expressible and d3's default velocityDecay even matches, but this integrator uses a linear alpha ramp instead of exponential decay, single-pass collide, plus an idle drift and anchor spring. Same forces, different settling.
  • three / react-force-graph-3d for Act I — it's a hand-rolled fl / (fl + z) projection on 2D canvas with painter's-algorithm sorting. WebGL changes node edges and label metrics.
  • framer-motion useScroll/useSpring — scroll is a double-lerp (0.050.08) with a jump bypass. A spring is a different transfer function.

The source's imperative core is the efficient implementation: put() memoises every style write onto el.__p_<prop>, cv() caches canvas context and size. A library layer here is slower, not faster.

Three rAF loops, not one (the brief said one): master tick, sim tick, hero canvas. Each gates itself on visibility. measure.ts is the only module that reads layout, throttled to 600ms — with one deliberate exception, sim.step's per-frame getBoundingClientRect visibility check, ported as-is rather than "fixed" into different behaviour.

Verification

tsc --noEmit, eslint src/lib/landing/, and a lightningcss parse of globals.css all clean, run against main's dependency set. No keyframe collisions.

Not verified: anything visual. There's no mount point yet, so "clean" means it compiles — not that it looks right.

Still to come

Section components (~1,500 lines of source markup), the route, then the first real visual comparison against the design.

🤖 Generated with Claude Code

Darkest-Teddyand others added 3 commits August 3, 2026 22:11
Ports the design-token layer from `Sapling Landing v4.dc.html` into a
.landing-v4 scope so it can be built alongside the existing .landing-page
surface rather than replacing it in place.
- 10 palette values + the 4 mastery-tier colours, as literal source values.
These are deliberately NOT aliased onto the app's brand tokens: #0C5638 /
#0E9E5A are their own scale, distinct from --brand-forest (#1B6C42) and
--brand-forest-bright (#2D8F5C).
- All 35 keyframes verbatim, including the six s1* intro-lockup frames whose
per-keyframe animation-timing-function declarations are the effect —
collapsing them onto one shared curve destroys the leaf weight.
- The three responsive rules and the scrollbar treatment.
Fonts bind to the existing next/font variables; DM Sans, Spectral, Playfair
Display and JetBrains Mono are exactly the four families the source loads, so
no extra webfont request is added. prefers-reduced-motion is not repeated —
the existing global block is already a superset of the source's rule.
Assets recovered from the design project: kofi-symbol.png (321x258) plus the
two journal photos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The single hard-coded course graph the whole landing page is built on. This
is not decorative filler — Act I, explore mode and the gallery all render
this same data, so it is ported node for node: 7 units, 42 concepts, every
authored 3D position, mastery score and blurb.
- course.ts XTIER / XTIER_LABEL / XTIER_ORDER, COURSE, nodeUses()
- content.ts SCRAMBLE, LAB_KIND, GAL, FAQS, POST_META, POSTS
- color.ts the interpolation maths the scroll engine runs per frame
XTIER.unexplored (#9a9a9a) is byte-identical to --state-neutral today but is
kept literal, following the same reasoning as --fallback-muted in globals.css:
it is a distinct concept, so a future change to the app's neutral state must
not silently move a landing-page mastery tier.
lerpTint keeps its hue-borrowing rule — grey has no hue of its own, so it
borrows the destination's and lets saturation carry the change, which is what
stops an unexplored node washing through a dead desaturated midpoint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The imperative core of the landing page. Kept out of React state and out of
an animation library on purpose: put() memoises every style write onto
el.__p_<prop> so an unchanged value never touches the DOM, and cv() caches
canvas context + size so no frame does a layout read. Wrapping this in
framer-motion would add a layer and make it slower, not faster.
Three loops, matching the source:
- LandingEngine.tick drives all three acts, the section nav, the ambient
canvas, the marquee and the plant field
- LandingEngine.simTick the drag-field force sim, gated on cluster visibility
- hero.ts the hero point cloud, gated past one viewport of scroll
measure.ts is the only module that reads layout, throttled to 600ms. Scroll
position is a double-lerp (raw -> 0.05 -> 0.08) with a bypass so programmatic
nav jumps land 1:1 instead of being smoothed into a glide.
sim.ts is hand-rolled rather than delegated to d3-force, which is already a
dependency. The forces are expressible in d3 and its default velocityDecay
even matches, but this integrator uses a linear alpha ramp instead of d3's
exponential decay, resolves collide in a single pass, and adds an idle drift
plus an anchor spring. Same forces, different settling.
graphView.ts draws the scroll view and explore mode from one projection.
Entering explore continues each node's grey-to-colour reveal from wherever it
had reached rather than restarting it, which is why it doesn't flash.
No mount point yet — components and the route follow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 16f66de7-6a90-4f6b-923e-ee0fc9998103

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@supabase

supabaseBot commented Aug 4, 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 ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 4, 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-staging862bc48Commit Preview URL

Branch Preview URL
Aug 04 2026, 01:39 PM

First mounted sections, on their own /v4 route so the existing landing keeps
working and the two can be compared side by side.
useLandingV4 is the React/engine bridge. React owns the discrete state (open
card, tutor mode, open FAQ); the engine owns everything per-frame and writes
straight to the DOM. State the engine reads is mirrored into a ref so its
getters never close over a stale render.
- IntroOverlay: the six s1* animations, 2.4s with `both` fill. Ordering is
causal — wordmark, stem, bud at the moment the stem tops out, then each leaf
from the joint the stem already passed, rule last.
- Navbar: fades in with the hero, auto-hides on downward scroll (imperative,
off the scroll listener rather than state), collapses under 1180px, hides in
explore mode.
- Hero: the four .floating-card surfaces are positioned in CSS but transformed
by the engine per frame, so they carry data-base-rot / data-float-delay /
data-float-dur instead of CSS animations.
The design's `style-hover` attribute has no inline equivalent in React, so
those 62 rules become real CSS under .landing-v4 — same values.
Refs are declared flat rather than in an object: React's refs lint rule
rejects `ref={obj.someRef}` in JSX.
Marketing links point at /about (exists) plus /team, /wiki, /gallery, /news
and /faq, which are the design's intended routes and are not built yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant

@Darkest-Teddy