Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cd1de17
docs(specs): website redesign — grid-as-hero with alpenglow drawer
blove May 2, 2026
7c3c384
docs(plans): website redesign — grid-as-hero with alpenglow drawer
blove May 2, 2026
d393dc1
feat(tokens): switch website to Alpenglow palette (light mode)
blove May 2, 2026
c4553b1
feat(website): TrailMarker component (green/blue/black/double-black)
blove May 2, 2026
9961fe2
feat(website): MountainFooter — Cascade silhouette + chairlift
blove May 2, 2026
3ce793c
feat(website): useDrawer — state machine for overlay drawer
blove May 2, 2026
882b089
feat(website): Drawer + DrawerHandle with CSS-upgrade pattern
blove May 2, 2026
52c5380
feat(website): hero event-log dataset (canned, multilingual)
blove May 2, 2026
5925eed
feat(website): replay engine for hero event log
blove May 2, 2026
8748e20
feat(website): HeroGrid streaming demo
blove May 2, 2026
44e3408
feat(website): trail markers in ComparisonTable adapter columns
blove May 2, 2026
a950103
feat(website): FeatureGrid reduced to 4 cards with trail markers
blove May 2, 2026
56a09b4
feat(website): consolidate PositioningStrip + Problem into ReceiptsBand
blove May 2, 2026
7312247
feat(website): CtaSection sharpened to install + GitHub
blove May 2, 2026
bb38b2c
feat(website): RouteAwareNav as transparent top-bar over hero
blove May 2, 2026
2c58bed
style(website): HowItWorks + CodeExample under Alpenglow palette
blove May 2, 2026
610b627
feat(website): assemble new homepage layout (HeroGrid + Drawer)
blove May 2, 2026
5063a21
chore(website): remove obsolete components from old layout
blove May 2, 2026
126faeb
test(website): smoke spec for new hero/drawer/footer shape
blove May 2, 2026
05f8dbc
docs(website): update README for grid-as-hero + drawer layout
blove May 2, 2026
eb8943a
fix(website): wire layout/page/HeroGrid for new homepage shape
blove May 2, 2026
4bbeec6
chore(website): lint fixes + prettier pass
blove May 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 47 additions & 30 deletions apps/website/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @pretable/app-website

The pretable marketing landing page. Cool-slate AI-startup direction, scroll-driven narrative. Two route trees: `/` (landing) and `/docs/*` (MDX-driven documentation surface).
The pretable marketing landing page. Grid-as-hero layout: a full-viewport live streaming grid is the first thing visitors see, with marketing content in a slide-up drawer beneath it. Two route trees: `/` (landing) and `/docs/*` (MDX-driven documentation surface).

This README is **living documentation** of the visual system as it exists today. It supersedes the proposal at `docs/superpowers/specs/2026-04-21-pretable-visual-system-design.md`.

Expand Down Expand Up @@ -38,62 +38,79 @@ Three variable fonts, all loaded via `@fontsource-variable/*`:
- **Inter Variable** — sans body. Canonical: `--pt-font-sans`. Tailwind alias: `font-sans`.
- **JetBrains Mono Variable** — code, eyebrow labels, grid cells. Canonical: `--pt-font-mono`. Tailwind alias: `font-mono`.

## Page gradient + ambient layer
## Palette

The body element ships a fixed-position vertical gradient (`var(--pt-bg-page)` → indigo midtone → near-black → `--pt-bg-page`) via `app/globals.css`. Two notes:
Light mode by default. The **Alpenglow palette** is defined in `app/styles/cool-slate-tokens.css`:

- `body { position: relative }` is required so `<LandingAmbient />`'s `absolute inset-0` wrapper anchors to the document, not the viewport.
- `background-attachment: fixed` keeps the gradient locked while the page scrolls.
| Role | Token | Value |
| -------------- | ---------------- | ---------- |
| Page bg | `--pt-bg-page` | warm cream |
| Accent | `--pt-accent` | dusk peach |
| Cool highlight | `--pt-cool` | cobalt |
| Drawer bg | `--pt-drawer-bg` | dark slate |

## Section anatomy
## Page layout

`app/page.tsx` renders nine sections in order:
`app/page.tsx` renders the full page in this order:

| # | Component | Role |
| --- | ------------------- | -------------------------------------------------------- |
| 1 | `Hero` | Headline + subhead. Above the fold. No scroll animation. |
| 2 | `PlaygroundSection` | Live `@pretable/react` grid. Above-the-fold proof. |
| 3 | `Problem` | The wedge: read-heavy grids stall in competitor libs. |
| 4 | `Solution` | Pretable's answer to the wedge. |
| 5 | `ReceiptsBand` | Headline metric strip ("receipts, not claims"). |
| 6 | `ComparisonTable` | Cell-by-cell receipts vs. competitors. |
| 7 | `FeatureGrid` | Six feature cards. |
| 8 | `CodeExample` | Single-import code snippet (shiki). |
| 9 | `CtaSection` | Closing CTA. Links to repo / install / next surface. |
1. **`<RouteAwareNav>`** — top navigation bar (shared with `/docs`).
2. **`<HeroGrid>`** — full-bleed live streaming demo. The grid is the hero: a real `@pretable/react` instance running a live receipts feed, full-viewport.
3. **`<DrawerHandle>`** — pill pinned to the bottom of the viewport; click or swipe up to open the drawer.
4. **`<Drawer>`** — overlay drawer containing six content sections in order:

Sections 3–9 are wrapped in `<ScrollReveal>`. Sections 1–2 are not (they're visible on first paint).
| # | Component | Role |
| --- | ----------------- | ----------------------------------------------------------------------- |
| 1 | `ReceiptsBand` | Numbers + positioning cards + problem callout ("receipts, not claims"). |
| 2 | `ComparisonTable` | Cell-by-cell adapter comparison with trail markers per adapter. |
| 3 | `HowItWorks` | Architecture explainer. |
| 4 | `CodeExample` | Single-import code snippet (shiki). |
| 5 | `FeatureGrid` | Four feature cards with trail markers. |
| 6 | `CtaSection` | Install command + GitHub link. |

## Narrative scaffolding
Sections 2–6 inside the drawer are wrapped in `<ScrollReveal>`. `ReceiptsBand` is rendered bare (visible on drawer open).

Two systems span the page:
5. **`<MountainFooter>`** — mountain silhouette footer. This is the only ski/mountain motif on the page; the hero and drawer do not repeat it.

### `<ScrollReveal>` (client component)
### DOM-first SEO

`app/components/ScrollReveal.tsx` — IntersectionObserver one-shot pattern. When a wrapped section first crosses 20% visibility, it animates from `opacity: 0; translateY(24px)` → `opacity: 1; translateY(0)` over 700ms with `ease-out`. After the first reveal, the observer disconnects — sections never re-animate on scroll-back. Respects `prefers-reduced-motion: reduce` (drops the translate, keeps opacity).
Full marketing content is always rendered server-side; JavaScript hydration upgrades the layout to an overlay drawer at viewport widths ≥ 768 px. At narrower widths (< 768 px), the drawer renders as a natural scroll page — no overlay, no `DrawerHandle` toggle, all sections in document flow.

### Responsive and accessibility behaviour

- **Mobile (< 768 px):** natural scroll page, no drawer overlay.
- **`prefers-reduced-motion`:** drawer slide animation is disabled; hero replay falls back to a static 50-row snapshot instead of the live streaming animation.

### `<LandingAmbient />` (server component)
## Trail markers

`app/components/LandingAmbient.tsx` — six absolute-positioned, blurred radial-gradient divs at `-z-40`, behind everything else. Color arc cool → indigo → cyan → amber → amber → cyan, mirroring the page narrative (entry, problem cold beat, solution warmth, proof zone, proof zone, CTA crescendo).
`<TrailMarker>` is used in `ComparisonTable` and `FeatureGrid` as ski-difficulty metaphors. Four variants:

`<AmbientBlob />` (`app/components/AmbientBlob.tsx`) is a small standalone primitive — a single blurred radial-gradient div — used by `Hero` for its in-section glow. Independent of `LandingAmbient`.
| Variant | Symbol | Meaning |
| -------------- | ------------ | ------------- |
| `green` | circle | easiest |
| `blue` | square | intermediate |
| `black` | diamond | expert |
| `double-black` | two diamonds | most advanced |

The blob `top` values are tuned to the current rendered section heights. If a section is added, removed, or substantially resized, **re-tune the blob positions** so the colors still land behind their intended sections. The component file has a comment block walking through the workflow; read it before adjusting.
## Narrative scaffolding

### `<ScrollReveal>` (client component)

`app/components/ScrollReveal.tsx` — IntersectionObserver one-shot pattern. When a wrapped section first crosses 20% visibility, it animates from `opacity: 0; translateY(24px)` → `opacity: 1; translateY(0)` over 700ms with `ease-out`. After the first reveal, the observer disconnects — sections never re-animate on scroll-back. Respects `prefers-reduced-motion: reduce` (drops the translate, keeps opacity).

## Adding a new section

1. Create `app/components/Foo.tsx` as a server component (default). Use `"use client"` only if the section needs hooks or browser APIs.
2. Import tokens via Tailwind class names (`text-text-primary`, `bg-bg-card`, etc.) — no inline color hex values.
3. Render in `app/page.tsx`. If the section sits below the fold, wrap it in `<ScrollReveal>`; if above the fold, render bare.
3. Render in `app/page.tsx` inside `<Drawer>`. Wrap it in `<ScrollReveal>` unless it should be visible immediately on drawer open.
4. Add a smoke test at `__tests__/components/Foo.test.tsx` using the existing pattern (render + one assertion).
5. If the section meaningfully changes page height, re-tune `LandingAmbient`'s blob `top` values.

## Testing

`apps/website/__tests__/` holds Vitest smoke tests only — one per top-level component plus the home page. Each test renders the component and asserts something stable (a heading exists, a wrapper renders, the code block is present). No interaction tests, no snapshots, no visual regression. The goal is to catch import-level breakage in CI, not to assert visual correctness.

Run locally: `pnpm --filter @pretable/app-website test`.

`apps/website/e2e/` holds Playwright **production smoke** specs. They run against the deployed origin (`BASE_URL`, defaults to `https://pretable.vercel.app`) and assert the hero, the live grid section, and the `/docs` route render. The spec is excluded from Vitest discovery via `vitest.config.ts`.
`apps/website/e2e/` holds Playwright **production smoke** specs. They run against the deployed origin (`BASE_URL`, defaults to `https://pretable.vercel.app`) and assert the hero grid (`[data-pretable-scroll-viewport]`), the drawer handle (`[data-testid="drawer-handle"]`), the mountain footer tagline, and the `/docs` route. The spec is excluded from Vitest discovery via `vitest.config.ts`.

Run locally against prod: `pnpm --filter @pretable/app-website smoke`.

Expand Down
18 changes: 17 additions & 1 deletion apps/website/__tests__/components/ComparisonTable.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cleanup, render } from "@testing-library/react";
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, expect, it } from "vitest";

import { ComparisonTable } from "../../app/components/ComparisonTable";
Expand All @@ -13,3 +13,19 @@ it("renders a comparison table with at least one row", () => {
expect((container.textContent ?? "").trim().length).toBeGreaterThan(0);
expect(container.firstChild).toBeInTheDocument();
});

it("renders trail markers for each adapter", () => {
render(<ComparisonTable />);
expect(
screen.getByRole("img", { name: /recommended path/i }),
).toBeInTheDocument();
expect(
screen.getByRole("img", { name: /familiar but slower/i }),
).toBeInTheDocument();
expect(
screen.getByRole("img", { name: /powerful but diy/i }),
).toBeInTheDocument();
expect(
screen.getByRole("img", { name: /broken at scale/i }),
).toBeInTheDocument();
});
14 changes: 10 additions & 4 deletions apps/website/__tests__/components/CtaSection.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cleanup, render } from "@testing-library/react";
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, expect, it } from "vitest";

import { CtaSection } from "../../app/components/CtaSection";
Expand All @@ -7,7 +7,13 @@ afterEach(() => {
cleanup();
});

it("renders the CTA section with at least one link", () => {
const { container } = render(<CtaSection />);
expect(container.querySelector("a")).toBeInTheDocument();
it("renders the install command as primary CTA", () => {
render(<CtaSection />);
expect(screen.getByText("npm install @pretable/react")).toBeInTheDocument();
});

it("renders a GitHub link as secondary CTA", () => {
render(<CtaSection />);
const link = screen.getByRole("link", { name: /github/i });
expect(link.getAttribute("href")).toContain("github.com");
});
18 changes: 11 additions & 7 deletions apps/website/__tests__/components/FeatureGrid.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cleanup, render } from "@testing-library/react";
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, expect, it } from "vitest";

import { FeatureGrid } from "../../app/components/FeatureGrid";
Expand All @@ -7,10 +7,14 @@ afterEach(() => {
cleanup();
});

it("renders a feature grid with multiple feature blocks", () => {
const { container } = render(<FeatureGrid />);
// Assert at least two heading-bearing children. The grid renders ≥6 features
// so two is a conservative floor that won't churn if a feature is renamed.
const headings = container.querySelectorAll("h3, h4");
expect(headings.length).toBeGreaterThanOrEqual(2);
it("renders four feature cards", () => {
render(<FeatureGrid />);
const cards = screen.getAllByRole("listitem");
expect(cards).toHaveLength(4);
});

it("renders a trail marker on each feature card", () => {
render(<FeatureGrid />);
const markers = screen.getAllByRole("img");
expect(markers.length).toBeGreaterThanOrEqual(4);
});
13 changes: 0 additions & 13 deletions apps/website/__tests__/components/Hero.test.tsx

This file was deleted.

16 changes: 0 additions & 16 deletions apps/website/__tests__/components/LandingAmbient.test.tsx

This file was deleted.

14 changes: 0 additions & 14 deletions apps/website/__tests__/components/PlaygroundSection.test.tsx

This file was deleted.

38 changes: 0 additions & 38 deletions apps/website/__tests__/components/PositioningStrip.test.tsx

This file was deleted.

13 changes: 0 additions & 13 deletions apps/website/__tests__/components/Problem.test.tsx

This file was deleted.

19 changes: 18 additions & 1 deletion apps/website/__tests__/components/ReceiptsBand.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cleanup, render } from "@testing-library/react";
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, expect, it } from "vitest";

import { ReceiptsBand } from "../../app/components/ReceiptsBand";
Expand All @@ -11,3 +11,20 @@ it("renders the receipts band with content", () => {
const { container } = render(<ReceiptsBand />);
expect((container.textContent ?? "").trim().length).toBeGreaterThan(0);
});

it("renders the receipts headline numbers", () => {
render(<ReceiptsBand />);
expect(screen.getByText("4×")).toBeInTheDocument();
expect(screen.getByText("16ms")).toBeInTheDocument();
});

it("renders the four positioning cards", () => {
render(<ReceiptsBand />);
expect(screen.getByText(/performance/i)).toBeInTheDocument();
expect(screen.getByText(/ai-native/i)).toBeInTheDocument();
});

it("renders the problem callout", () => {
render(<ReceiptsBand />);
expect(screen.getByText(/clips wrapped content/i)).toBeInTheDocument();
});
35 changes: 0 additions & 35 deletions apps/website/__tests__/components/TrustStrip.test.tsx

This file was deleted.

38 changes: 0 additions & 38 deletions apps/website/__tests__/components/UseCases.test.tsx

This file was deleted.

Loading
Loading