Uh oh!
There was an error while loading. Please reload this page.
feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine - #523
feat(landing): port Sapling Landing v4 — tokens, data model, scroll engine#523Darkest-Teddy wants to merge 4 commits into
Conversation
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>
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
This pull request has been ignored for the connected project Preview Branches by Supabase. |
Deploying with |
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs | frontend-staging | 862bc48 | Commit 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>
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
globals.css— a.landing-v4scope, 10 palette values, 4 tier colours, all 35 keyframeskofi-symbol.png,journal-founding.png,journal-ai-homework.pnglib/landing/{course,content,color}.tslib/landing/engine/— 11 modules, one master tickScoped to
.landing-v4so it sits alongside the existing.landing-pagesurface rather than replacing it. The current landing keeps working.Decisions worth reviewing
The greens are not the brand tokens.
#0C5638/#0E9E5Aare 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 forXTIER.unexplored(#9a9a9a), which is byte-identical to--state-neutraltoday but is a distinct concept — this follows the existing--fallback-mutedprecedent.No animation library, deliberately. Checked the obvious swaps against the source's numerics:
d3-forcefor the drag-field sim (already a dependency) — forces are expressible and d3's defaultvelocityDecayeven 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-3dfor Act I — it's a hand-rolledfl / (fl + z)projection on 2D canvas with painter's-algorithm sorting. WebGL changes node edges and label metrics.useScroll/useSpring— scroll is a double-lerp (0.05→0.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 ontoel.__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.tsis the only module that reads layout, throttled to 600ms — with one deliberate exception,sim.step's per-framegetBoundingClientRectvisibility check, ported as-is rather than "fixed" into different behaviour.Verification
tsc --noEmit,eslint src/lib/landing/, and a lightningcss parse ofglobals.cssall clean, run againstmain'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