The marketing landing page - #33
Conversation
The app has one surface today and branches on the session to pick it. A marketing landing page is a second surface that has to be reachable while signed in, so something has to choose between them. Two static routes, no parameters, no nesting and no data loading: TanStack Query already owns everything from the server and Zustand owns the canvas, so a router library would arrive with a data layer that has nothing to do and a configuration about as long as this file. `Link` is the part worth care. A hand-rolled one usually calls preventDefault unconditionally and quietly breaks modifier-click, so it claims only a plain left click and leaves the rest to the browser. The test holds Meta down to pin that, using `userEvent.setup()` -- a bare `userEvent.click` is a fresh instance that forgets the held key, and the assertion passes without testing anything. APP_ROUTE goes in the shared package because the API redirects there too.
The landing page's hero is a call graph drawing itself, not a screenshot of one (UI_GUIDE §3.1). Plain SVG rather than React Flow: React Flow wants a measured container and brings pan, zoom and handles a hero has no use for, along with the node.width trap in docs/CANVAS_DECISIONS.md §4. A fixed viewBox scales on its own and cannot drop an edge in silence. The draw is a mask sweeping across the edge layer. Framer's `pathLength` writes an inline stroke-dasharray, which overwrites the pattern that distinguishes solid from dashed from dotted -- all three come out identical, which is the one picture this product must never show (PRD §8). The mask leaves every path's own attributes alone. It also translates rather than resizes, so the animated property is a transform. The graph is laid out by depth and the sweep runs left to right, so the sweep is the stagger: one animated element for the whole orchestrated moment. The fixture carries a ghost node -- an unresolved callee at the edge of the map, dotted, faded and labelled with the name the parser saw. That is signature 2 from UI_GUIDE §3.2, and leading a hero with what the tool cannot do is the point rather than an oversight. `CONFIDENCE` gains `strokeClass` beside `textClass`: a class follows the theme without the component subscribing to it, and Tailwind cannot see a class name built by concatenation. `confidenceColor` stays for the canvas, which needs a raw value for real SVG attributes.
The surface UI_GUIDE §3.1 has specified since Phase 3b and that no phase's exit test touches. It is the one page that takes the maximal spatial treatment -- py-24 and above, nested double-bezel cards -- because matching complexity to the surface is the point, and the canvas stays dense. The page's structural device is the product's own notation. Each section heading carries a hairline rule whose dash pattern is a confidence tier, and each section takes the tier that is true of it: the resolution section is solid, languages is dashed because support past the ECMAScript family genuinely is partial, and the closing section is dotted because what it states are the product's limits. A reader has met the notation three times before ever reaching the canvas. Nothing here calls our API. `useSession` moves behind the canvas route in the next commit, so this page renders whether or not the backend is up -- which is the least a page whose job is to explain the product can do. The one outbound request is a GitHub star count, which is not required: the control degrades to a plain link to the repository, and GitHub rate-limits anonymous callers hard enough that this is the common case. Colour rule for the page, in one line: no colour appears that does not carry its canvas meaning. The accent is the hue that means "known" everywhere else, apricot appears only on a name match, slate only on unresolved. No gradient mesh, no glow, no glass -- §1.3 bans them and §7 cluster 2 is precisely what this would otherwise become. Installed rather than written: `lenis` for smooth scrolling, mounted from Landing alone because the canvas treats the wheel as zoom; and two animate-ui primitives via the shadcn CLI, `effects/fade` for the scroll reveals and `texts/sliding-number` for the star count. They ship importing `motion/react`, which is a second copy of Framer Motion beside the `framer-motion` this project locks, so each generated file is retargeted and says so -- `add --overwrite` reverts it silently. The legend's tier line and the section rule were the same drawing twice, so ConfidenceRule is extracted and the legend now uses it.
An atlas has an index and the file tree is it (UI_GUIDE §3.2), so the page shows one rather than describing it. The counts are the reason it is worth showing: a file's weight on this canvas is how many functions it holds, and that is the number the sidebar puts beside every path. The tree is this repository's own shape, because a made-up src/components/Button.tsx would say nothing about a tool built to read a polyglot monorepo. `components-base-files` installed from animate-ui rather than hand-written, and then edited, which is the model for a generated file. Three edits: - The upstream right-hand slot is a git status dot in hardcoded green, amber and red. There is no diff to report here and no colour outside tokens.ts, so the slot became `meta` and carries the function count. - The imports were `motion/react` and `@base-ui-components/react`, second copies of the framer-motion and @base-ui/react this project locks. - This project's Base UI is a major ahead of the one it was generated against: Accordion's value is `AccordionValue<unknown>` rather than `string | string[]`, and onValueChange takes an event-details argument. The types are taken from the component rather than restated, so the next Base UI upgrade surfaces here as a type error and not as a wrong cast. The one place an array is assumed is narrowed, not cast. Each edited file says so at the top, because `add --overwrite` reverts them silently.
`App` becomes the route switch and the session branching moves into `AppRoute` unchanged. `useSession` moves with it, which is the point of the split rather than a side effect: the landing page now issues no request and renders with the API down, which is the least a page whose job is to explain the product can do. Anything that is not `/app` is the landing page; there is no 404 because there is nothing else to be. The OAuth callback redirected to `env.WEB_APP_URL`, a bare origin. That is now the marketing page, so it redirects to APP_ROUTE under it -- signing someone in and then showing them the pitch for the product they just signed in to is the failure the test asserts against. `.env` is unchanged: WEB_APP_URL stays an origin and the path is joined here. App.test.tsx pushes APP_ROUTE before the session cases, which at jsdom's default `/` would otherwise be asserting against the marketing page.
Four things, all found by looking at it in a browser rather than at the code. The viewBox was 560x360 for a drawing 268 units tall, and an SVG scales to fit its own empty space, so the graph came out small and floated in the middle of its bezel. The height is now measured off the lowest baseline plus the ghost's caption plus a margin. The four surfaces each carried their own max width -- the title block and the sections at 64rem, the hero at 72rem -- so the wordmark did not line up with the headline under it, and the whole page read as a column floating in a field of ground. One shared LANDING_SHELL at 104rem, and the prose inside caps itself instead. The headline was sized to wrap to five lines in the column it ended up with. It lands on three now. The em dash is gone from the visible copy, here and in the sections.
The scale is a two-colour press: two spot inks and a neutral. A printer with two plates and paper has exactly three things to say, which is exactly how many answers resolution has. Ultramarine is the fact, fired clay is the report, and the ash is what neither plate covered. Read as ink weight rather than as temperature, so it survives being drawn as a hairline on a canvas the reader is zoomed out of. Nothing was wrong with Ember/Vellum. It passed both palette rules, it read clearly, and it shipped through Phases 3b to 5. It goes because cyan on near-black is the accent every developer tool already uses, and UI_GUIDE §7 exists to keep this project off the look its whole category has converged on. The structure it established is untouched: three tiers, one accent doing double duty as focus and links, and `unresolved` quiet and achromatic. Both palette rules still hold with margin, and confidence.test.ts is what says so rather than this message. Unresolved is ash at 10-11% saturation in the ground's own hue family, nowhere near the red band. The tightest contrast in either theme is unresolved on light paper at 4.03:1, against a 3:1 floor. UI_GUIDE §1.1 is rewritten to the new tables and §7.1 records Ember/Vellum as a direction that shipped and was replaced, not as a mistake.
"Start it with pnpm dev" assumes the only person who reaches this screen is running the stack and has forgotten a process. Once the web app can be deployed on its own -- which is a likely outcome, since the API spawns the Go parser and needs writable disk, so it cannot go anywhere serverless -- the other reader is a stranger who followed a link, clicked through from the landing page, and cannot run pnpm anything. Names what is missing, keeps the local instruction, and offers a way onward in both directions: the source for someone who wants to run it, and the landing page for someone who just wants to know what it is.
UI_GUIDE §3.1 said the page was unopened; it has shipped. §3.1a now carries what it is and what it deliberately is not, including the two traps a later change would otherwise rediscover: `pathLength` flattening the three tiers, and Lenis taking the wheel away from the canvas if it is ever mounted at the app root. TASKLIST records what was verified in a browser and, separately, what was not: requestAnimationFrame ran at roughly one frame per half-second in the available window and resize_window was ignored, so the draw animation and the mobile breakpoints are unverified rather than working. NFR-4 is written down as next, with the four faults named.
ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Free Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (50)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughChangesThe web app now serves a landing page at Landing page composition Routing and OAuth integration Animated component foundation Validation and design updates Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to This PR adds the marketing landing page, separates Sequence Diagram(s)sequenceDiagram
participant Visitor
participant App
participant Landing
participant Router
participant AppRoute
Visitor->>App: Open /
App->>Landing: Render landing page
Landing->>Landing: Render sections and animations
Visitor->>Router: Activate OpenAtlas
Router->>App: Navigate to /app
App->>AppRoute: Render application route
Warning Some tools did not complete. Review the errors below. 🔧 ESLint
apps/api/src/auth/routes.test.tsESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox. apps/api/src/auth/routes.tsESLint skipped: the matched ESLint configuration already failed (missing-dependency). apps/web/package.jsonESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.
Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/settings/billing. Comment |
docs/UI_GUIDE.md§3.1 has specified a marketing landing page since Phase 3b and named it thebranch after that gate. Phases 4 and 5 went first, so it was overdue rather than upcoming. This is
it, plus the routing it needed and a palette change it turned into.
What is here
Routing. There was no router.
/is the landing page and/appis the canvas; anything elsefalls to the landing page and there is no 404.
lib/router.tsxis about thirty lines overpushStaterather than a router library — two static routes, no parameters, no loaders, andTanStack Query and Zustand already own all the state a router's data layer would want. The part
that gets hand-rolled routers wrong is
Link, so it claims only a plain left click and the testholds Meta down to prove it.
useSessionmoved behind/app. That is the point of the split rather than a side effect: thelanding page issues no request to our API and renders with the backend down. The OAuth callback
now redirects to
APP_ROUTEunderWEB_APP_URLinstead of the bare origin, which was about tosign people in and then show them the pitch for the product they had just signed in to.
The hero is a live drawing graph, not a screenshot. Plain SVG rather than React Flow, which
wants a measured container and brings the
node.widthtrap fromCANVAS_DECISIONS.md§4. The drawis a mask sweeping across the edge layer — not an animated
pathLength, which writes an inlinestroke-dasharrayand flattens solid, dashed and dotted into one pattern.HeroGraph.test.tsxasserts three distinct dash values for exactly that reason. The graph leads with a ghost node: an
unresolved callee at the map's edge, labelled with the name the parser saw. Leading a marketing
hero with what the tool cannot do is deliberate, because that admission is the product.
The page's structural device is the product's notation. Every section rule is a confidence
tier's dash pattern, and each section takes the tier that is true of it: solid over resolution and
the pipeline, dashed over coverage because support past the ECMAScript family genuinely is partial,
dotted over the limits. A reader has met the notation three times before reaching the canvas.
ConfidenceRuledraws it and the legend was refactored onto the same component.One colour rule: no colour appears that does not carry its canvas meaning. No gradient mesh, no
glow, no glass, no bento grid, no sticky bar and no floating pill — §1.3 and §7.
Installed, not written. Three animate-ui components through the shadcn CLI (
effects/fade,texts/sliding-number,components-base-files) andlenisfor smooth scrolling, mounted fromLandingalone because at the app root it would take the wheel away from the canvas, where thewheel means zoom. Every generated file was edited and says so at the top, because
add --overwritereverts it silently: they ship importing
motion/reactand@base-ui-components/react, secondcopies of the
framer-motionand@base-ui/reactthis repo locks, and the files component's gitstatus slot carried hardcoded green, amber and red that became a function count instead.
The palette changed, product-wide
Ember/Vellum is gone. It passed both palette rules and read clearly; it goes because cyan on
near-black is the accent every developer tool already uses, which §7 exists to keep us off.
Ultramarine and Letterpress are a two-colour press: two spot inks and a neutral. Ultramarine
is the fact, fired clay is the report, ash is what neither plate covered. The structure is
untouched — three tiers, one accent doing double duty,
unresolvedquiet and achromatic.confidence.test.tsis what says the rules still hold, not this description: unresolved is ash at10–11% saturation nowhere near the red band, and the tightest contrast in either theme is 4.03:1
against a 3:1 floor.
Verified
make test(127 api / 170 web / Go against Postgres),make lint,make typecheck,make go-vet— all clean. Both taste greps empty: no hex in any
.tsx, no blur/gradient/glow in the landingdirectory outside comments saying we do not use them.
By hand in real Chrome: the whole page in both themes at desktop width, the finished graph with all
three edge styles distinguishable, the index tree, and
/appstill resolving its session.Not verified, and I would rather say so
requestAnimationFrameran at about one frame per half-second in thebrowser window available to me, so the sweep and the node stagger never advanced. I inspected the
finished state by pinning it. The animation needs your eyes.
resize_windowwas ignored by that environment. The breakpoints arewritten and there is no horizontal overflow at the width I could reach, but nobody has seen the
page at 375px.
Notes
The landing page does not advertise
docker compose up, because it does not work yet. Thatline ships with NFR-4, which is the next branch:
apps/webhas no Dockerfile, there is noworkerservice so a webhook enqueues a job nothing consumes, the API image runs
node dist/index.jsagainst
packages/sharedexports pointing at.ts, and theparserservice sets bothnetwork_mode: noneanddepends_onPostgres health.SessionUnavailablewas rewritten for the reader who actually sees it. Once the web app can bedeployed on its own — likely, since the API spawns the Go parser and needs writable disk, so it
cannot go anywhere serverless — a stranger following a link reaches that screen and cannot run
pnpmanything.Summary by CodeRabbit
New Features
Bug Fixes
Documentation