Skip to content
4 changes: 2 additions & 2 deletions website-next/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ import { HomeHero } from "@/src/components/home/HomeHero";
import { LogoCloud } from "@/src/components/home/LogoCloud";
import { NitroPricing } from "@/src/components/home/NitroPricing";
import { ProtocolCards } from "@/src/components/home/ProtocolCards";
import { StackDiagrams } from "@/src/components/home/StackDiagrams";
import { ScrollScenes } from "@/src/components/home/act2/ScrollScenes";
Comment thread
PascalSenn marked this conversation as resolved.
Resolved

export default function Home() {
return (
<>
<HomeHero />
<LogoCloud />
<BuildYourWay />
<StackDiagrams />
<FusionFlow />
<ProtocolCards />
<ScrollScenes />
<NitroPricing />
<GrabADrink />
<div className="px-5 py-8 sm:px-12">
Expand Down
67 changes: 67 additions & 0 deletions website-next/src/components/RevealOnScroll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"use client";

import type { ReactNode } from "react";
import { useLayoutEffect, useRef, useState } from "react";

interface RevealOnScrollProps {
readonly children: ReactNode;
readonly className?: string;
readonly hiddenClassName?: string;
readonly shownClassName?: string;
readonly rootMargin?: string;
readonly threshold?: number;
}

const DEFAULT_HIDDEN_CLASS_NAME = "translate-y-6 opacity-0";
const DEFAULT_SHOWN_CLASS_NAME = "translate-y-0 opacity-100";
const DEFAULT_TRANSITION_CLASS_NAME = "transition-all duration-700 ease-out";

/** Reveals its content once when it first scrolls into view. */
export function RevealOnScroll({
children,
className,
hiddenClassName = DEFAULT_HIDDEN_CLASS_NAME,
shownClassName = DEFAULT_SHOWN_CLASS_NAME,
rootMargin = "0px 0px -10% 0px",
threshold = 0.2,
}: RevealOnScrollProps) {
const ref = useRef<HTMLDivElement>(null);
const [shown, setShown] = useState(false);

useLayoutEffect(() => {
const node = ref.current;
if (node === null) {
return;
}

// polyfill-free fallback for SSR and old browsers. The content will be visible
if (typeof IntersectionObserver === "undefined") {
const id = requestAnimationFrame(() => setShown(true));
return () => cancelAnimationFrame(id);
}

const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
setShown(true);
observer.disconnect();
}
}
},
{ threshold, rootMargin },
);

observer.observe(node);
return () => observer.disconnect();
}, [rootMargin, threshold]);

return (
<div
ref={ref}
className={`${className} ${DEFAULT_TRANSITION_CLASS_NAME} ${shown ? shownClassName : hiddenClassName}`}
Comment thread
Copilot marked this conversation as resolved.
Resolved
>
{children}
</div>
);
}
129 changes: 105 additions & 24 deletions website-next/src/components/home/BuildYourWay.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,119 @@
import { CoffeeTray } from "@/src/icons/CoffeeTray";
import { Fusion } from "@/src/icons/Fusion";
import { HotChocolate } from "@/src/icons/HotChocolate";
import { Mocha } from "@/src/icons/Mocha";
import { Nitro } from "@/src/icons/Nitro";
import { StrawberryShake } from "@/src/icons/StrawberryShake";

/** Brand spectrum (cyan -> violet -> coral), reused from the hero accent. */
import { DrinkIcon } from "./DrinkIcon";

/** Brand spectrum (cyan -> violet -> coral), the card's pitch background. */
const CARD_GRADIENT =
"linear-gradient(100deg,#16b9e4 0%,#7c92c6 36%,#b681a9 64%,#f0786a 100%)";

const PRODUCTS = [
{
label: "Hot Chocolate",
role: "GraphQL Server",
href: "/products/hotchocolate",
Icon: HotChocolate,
},
{
label: "Fusion",
role: "Federation Gateway",
href: "/docs/fusion",
Icon: Fusion,
},
{
label: "Nitro",
role: "Control Plane",
href: "/products/nitro",
Icon: Nitro,
},
{
label: "Strawberry Shake",
role: "GraphQL Client",
href: "/products/strawberryshake",
Icon: StrawberryShake,
},
{
label: "Mocha",
role: "Messaging & Mediator",
href: "/docs/mocha",
Icon: Mocha,
},
] as const;

/**
* Full-bleed gradient feature card introducing the "stack your way" idea: the
* coffee-tray illustration on one side, the headline and supporting copy on the
* other. The card sits on the brand spectrum, so the text switches to the dark
* page ink for contrast. Stacks to a single column on small screens.
* "Stack your way" feature card: one rounded, clipped container holding a gradient
* pitch region on top (the coffee-tray illustration + headline) and a dark product
* drawer directly beneath it. The two share the card's corners with a seam where
* they meet, so the product menu reads as the bottom of the card rather than a
* panel laid over it. Drinks are normalized by {@link DrinkIcon} so they share a
* baseline. Stacks to a single column on small screens.
*/
export function BuildYourWay() {
return (
<section className="mx-auto max-w-6xl px-5 py-8 sm:px-12">
<div
className="relative overflow-hidden rounded-3xl px-8 py-12 text-[#0b1018] sm:px-12 sm:py-14 lg:px-16"
style={{ backgroundImage: CARD_GRADIENT }}
>
<div className="flex flex-col items-center gap-10 md:flex-row md:gap-16">
<CoffeeTray className="h-44 w-auto flex-none drop-shadow-[0_18px_30px_rgba(0,0,0,0.25)] sm:h-52 lg:h-60" />
<div className="max-w-xl text-center lg:text-left">
<h2 className="font-heading text-h4 sm:text-h3 leading-[1.1] font-semibold tracking-[-0.01em]">
Build your way.
<br />
Ship faster.
<br />
Stay in control.
</h2>
<p className="mt-5 text-base/relaxed text-[#0b1018]/75 sm:text-lg/relaxed">
Pick what you need for your stack. Whether you&rsquo;re building a
monolithic or federated GraphQL API, a message-intensive service,
or an application, we&rsquo;ve got the right tools for you.
</p>
<div className="mx-auto max-w-6xl overflow-hidden rounded-3xl shadow-[0_24px_50px_rgba(0,0,0,0.4)]">
{/* Pitch region (top of the card). */}
<div
className="px-8 pt-12 pb-12 text-[#0b1018] sm:px-12 sm:pt-14 lg:px-16"
style={{ backgroundImage: CARD_GRADIENT }}
>
<div className="flex flex-col items-center gap-10 md:flex-row md:gap-16">
<CoffeeTray className="h-44 w-auto flex-none drop-shadow-[0_18px_30px_rgba(0,0,0,0.25)] sm:h-52 lg:h-60" />
<div className="max-w-xl text-center lg:text-left">
<h2 className="font-heading text-h4 sm:text-h3 leading-[1.1] font-semibold">
Start where you are.
<br />
Add what you need.
</h2>
<p className="mt-5 text-base/relaxed text-[#0b1018]/75 sm:text-lg/relaxed">
Pick the pieces that fit what you are building: a monolith, a
federated API, a message-heavy service, or a client application.
We give you tools that work on their own and fit together when
your platform grows.
</p>
</div>
</div>
</div>

{/* Product drawer (bottom of the SAME card). */}
<div className="bg-cc-surface px-5 pt-5 pb-5 sm:px-8 sm:pt-6 sm:pb-6">
<div className="flex items-center gap-3 px-1">
<span className="bg-cc-ink-faint h-px flex-1" />
<span className="text-cc-nav-label font-mono text-[0.65rem] tracking-[0.15em] uppercase">
Products
</span>
<span className="bg-cc-ink-faint h-px flex-1" />
</div>

<nav
aria-label="ChilliCream products"
className="mt-3 grid grid-cols-2 gap-1 sm:grid-cols-3 lg:grid-cols-5"
>
{PRODUCTS.map(({ label, role, href, Icon }) => (
<a
key={label}
href={href}
className="group focus-visible:ring-cc-accent hover:bg-cc-card-bg/60 flex flex-col items-center gap-2 rounded-2xl border border-transparent px-2 py-3 text-center transition-colors duration-200 focus-visible:ring-2 focus-visible:outline-none"
>
Comment thread
PascalSenn marked this conversation as resolved.
Resolved
<DrinkIcon
Icon={Icon}
name={label}
base={56}
className="transition-transform duration-200 group-hover:-translate-y-1"
/>
<span className="text-cc-heading font-heading text-sm leading-tight font-semibold">
{label}
</span>
<span className="text-cc-ink-dim group-hover:text-cc-accent text-[0.7rem] leading-tight transition-colors duration-200">
{role}
</span>
</a>
))}
</nav>
</div>
</div>
</section>
Expand Down
78 changes: 78 additions & 0 deletions website-next/src/components/home/DrinkIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { ComponentType, CSSProperties } from "react";

type Drink = ComponentType<{
readonly className?: string;
readonly style?: CSSProperties;
}>;

/**
* The drink icons are crops from one sprite: they share a bottom edge but have
* different viewBox heights (a coffee cup is 84 tall; the Strawberry Shake
* milkshake is 109; an espresso is 61). Sizing them all to one CSS height shrinks
* the taller drinks' bodies and floats them off the baseline.
*
* DrinkIcon renders every drink at a CONSTANT per-unit scale (so the cup bodies
* match) inside a fixed-height box, bottom-aligned, so a row reads as one shelf
* of drinks with consistent weight, a milkshake simply standing a little taller.
*
* `base` is the body height in px for a normal 84-unit cup; pass the px value the
* take used before (h-10 = 40, h-12 = 48, h-14 = 56, h-16 = 64).
*/

const COFFEE_CUP_UNITS = 84;

/** viewBox height per drink, used to keep one constant px-per-unit scale. */
const DRINK_UNITS: Record<string, number> = {
"hot chocolate": 84,
fusion: 84,
nitro: 84,
mocha: 84,
"strawberry shake": 109,
"green donut": 109,
"cookie crumble": 109,
espresso: 61,
};

/** Tallest drink we account for, so the box always reserves enough headroom. */
const MAX_RATIO = 109 / COFFEE_CUP_UNITS;

function ratioFor(name: string): number {
const units = DRINK_UNITS[name.trim().toLowerCase()] ?? COFFEE_CUP_UNITS;
return units / COFFEE_CUP_UNITS;
}

interface DrinkIconProps {
readonly Icon: Drink;
/** Product name, used to look up the drink's true proportions. */
readonly name: string;
/** Body height in px for a normal cup. */
readonly base: number;
/** Applied to the inner svg (hover transforms, drop-shadow, etc.). */
readonly className?: string;
readonly style?: CSSProperties;
}

export function DrinkIcon({
Icon,
name,
base,
className,
style,
}: DrinkIconProps) {
return (
<span
aria-hidden="true"
className="flex items-end justify-center"
style={{ height: `${base * MAX_RATIO}px` }}
>
<Icon
className={className}
style={{
height: `${base * ratioFor(name)}px`,
width: "auto",
...style,
}}
/>
</span>
);
}
Loading
Loading