diff --git a/CLAUDE.md b/CLAUDE.md index a1748be..f1a9769 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,12 +63,14 @@ App lives in `apps/web`. Social media videos live in `apps/video` (Remotion, sta along the edge of an element, digits drifting off it; glow only on dark surfaces, on light ones just a few blue digits), `BitSeam` (lit top edge of a dark section), `BitGlow` (light following the pointer on `data-glow` surfaces). Reuse them instead of inventing new effects. -- The homepage is a guided story (`apps/web/src/components/story/`): it follows the questions a visitor has - (`story.chapters` in the content files). Each chapter ends with `NextChapter`, the next question as a link - that leads on; `StoryGuide` is the rail with one station per chapter and a travelling dot (a pill on - phones), and the header names the current question. Sections become chapters through `data-chapter` - (`Block` prop `chapter`). Where the visitor is, is computed in `src/domain/chapters.ts`. A new section - on the homepage needs a chapter and a question, or it breaks the thread. +- The homepage is one story with a visible thread (`apps/web/src/components/story/`). Every chapter opens + with one sentence from `story` in the content files; read one after the other they tell the way of a + project from the idea to the first email. `StoryThread` is one line down the whole page that draws itself + as the visitor scrolls: under each sentence it crosses the page, stands a figure on the line and continues + on the other side, with a glowing dot at its head. `Block` prop `chapter` gives a section its sentence and + figure; how much of the line is drawn is decided in `src/domain/thread.ts`. A new section on the homepage + needs a sentence that continues the story, or it breaks the thread. No rails, no "next question" links + (owner rejected both). - `DecodeText` is for monospaced text only; in proportional type the changing glyph widths make text jump. - Never put an in-view trigger on an element that clips itself away (clip-path): a fully clipped element never counts as visible. Observe an unclipped wrapper (see `BitWipe`). diff --git a/apps/web/app/[locale]/page.tsx b/apps/web/app/[locale]/page.tsx index 93a09c6..2774bb7 100644 --- a/apps/web/app/[locale]/page.tsx +++ b/apps/web/app/[locale]/page.tsx @@ -18,11 +18,10 @@ import { } from "@/src/components/illustrations/WebPresentation"; import { WorkShowcase } from "@/src/components/illustrations/WorkShowcase"; import { Reveal } from "@/src/components/Reveal"; -import { NextChapter } from "@/src/components/story/Chapter"; -import { StoryGuide } from "@/src/components/story/StoryGuide"; +import { StoryThread } from "@/src/components/story/StoryThread"; import { SwipeRow } from "@/src/components/SwipeRow"; import { contentFor } from "@/src/content"; -import type { ChapterId, SplitTitle } from "@/src/content/types"; +import type { SplitTitle } from "@/src/content/types"; import { DEFAULT_LOCALE, parseLocale } from "@/src/domain/locale"; import { languageAlternates, pathFor } from "@/src/domain/routes"; @@ -70,28 +69,11 @@ export default async function LandingPage({ params }: Props) { faq, } = content; - /** What makes a section a chapter, and the hand-over to the one after it. */ - const chapter = (id: ChapterId) => { - const at = story.chapters.findIndex((item) => item.id === id); - const own = story.chapters[at]; - const after = story.chapters[at + 1]; - return { - chapter: own && { id: own.id }, - next: after && { - id: after.id, - label: story.next, - question: after.question, - }, - }; - }; - const afterHello = chapter("hello").next; - return (
-

{hello.lead} @@ -119,14 +102,13 @@ export default async function LandingPage({ params }: Props) { {hello.careerLabel}

- {afterHello && }
} > @@ -137,7 +119,7 @@ export default async function LandingPage({ params }: Props) { } > @@ -170,7 +152,7 @@ export default async function LandingPage({ params }: Props) { } > @@ -243,7 +225,7 @@ export default async function LandingPage({ params }: Props) { } > @@ -294,7 +276,7 @@ export default async function LandingPage({ params }: Props) { } > @@ -309,7 +291,7 @@ export default async function LandingPage({ params }: Props) { } > @@ -317,7 +299,8 @@ export default async function LandingPage({ params }: Props) { - + + ); } diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index e4dae52..d1c2e79 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -1339,21 +1339,34 @@ pre:has(code[class*="language-"]) { } } -/* The story guide: the dot that travels along the chapter rail, and the header line. */ -.story-dot { +/* + * The story thread (see StoryThread): one line down the page, indigo on light + * sections and light on dark ones, with a glowing dot at its head. + */ +.story-thread path { + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.story-thread-light { + stroke: #4f46e5; +} +.story-thread-dark { + stroke: #c7d2fe; + stroke-width: 2; +} +.story-thread-dot { + width: 12px; + height: 12px; + margin: -6px 0 0 -6px; + border-radius: 9999px; + background: #fde68a; box-shadow: - 0 0 0 3px rgba(253, 230, 138, 0.35), - 0 0 14px 3px rgba(251, 191, 36, 0.8); - transition: top 200ms linear; -} -.story-now { - animation: story-now-in 400ms cubic-bezier(0.16, 1, 0.3, 1); -} -@keyframes story-now-in { - from { - opacity: 0; - transform: translateY(6px); - } + 0 0 0 4px rgba(253, 230, 138, 0.28), + 0 0 18px 5px rgba(251, 191, 36, 0.85), + 0 0 44px 14px rgba(129, 140, 248, 0.55); + transition: opacity 300ms ease; + will-change: transform; } /* diff --git a/apps/web/src/components/Header.tsx b/apps/web/src/components/Header.tsx index 6013bc3..25bf4a5 100644 --- a/apps/web/src/components/Header.tsx +++ b/apps/web/src/components/Header.tsx @@ -9,7 +9,6 @@ import { DecodeText } from "./bits/DecodeText"; import { ScrollBits } from "./bits/ScrollBits"; import { useContent, useLocale } from "./ContentProvider"; import { LanguageSwitch } from "./LanguageSwitch"; -import { ChapterNow } from "./story/StoryGuide"; /** Header: the logo, the link to the freelance page, the language switch and one action. */ export const Header: React.FC = () => { @@ -62,11 +61,7 @@ export const Header: React.FC = () => { - - } - /> +
= ({ label, title, chapter, - next, id, tone = "light", children, @@ -38,8 +36,7 @@ export const Block: React.FC = ({ const dark = tone === "dark"; return (
= ({
{chapter && (
- +
)} @@ -92,14 +89,6 @@ export const Block: React.FC = ({
{children}
- {next && ( - - )}
); diff --git a/apps/web/src/components/home/ClosingCta.tsx b/apps/web/src/components/home/ClosingCta.tsx index 8c23fae..cec2746 100644 --- a/apps/web/src/components/home/ClosingCta.tsx +++ b/apps/web/src/components/home/ClosingCta.tsx @@ -8,12 +8,13 @@ import { BitHeadline } from "./BitHeadline"; import { ContactComposer } from "./ContactComposer"; /** ClosingCta: the contact section. One address and a message starter, no form to submit. */ -export const ClosingCta: React.FC<{ contact: Content["contact"] }> = ({ - contact, -}) => ( +export const ClosingCta: React.FC<{ + contact: Content["contact"]; + /** The last sentence of the story; the story thread ends in front of it. */ + story?: string; +}> = ({ contact, story }) => (
@@ -29,6 +30,16 @@ export const ClosingCta: React.FC<{ contact: Content["contact"] }> = ({
+ {story && ( +

+

+ )} = ({ id, index, label, title, portraitAlt, children }) => ( +}> = ({ id, index, label, title, portraitAlt, story, children }) => (
{/* Portrait: right side on wide screens, top on small ones. */} @@ -59,6 +61,9 @@ export const PortraitSection: React.FC<{ viewport={{ once: true, margin: "-10%" }} transition={{ duration: 0.8, delay: 0.2, ease: [0.16, 1, 0.3, 1] }} > +

+ {story} +

{index} diff --git a/apps/web/src/components/story/Chapter.tsx b/apps/web/src/components/story/Chapter.tsx deleted file mode 100644 index 10c677b..0000000 --- a/apps/web/src/components/story/Chapter.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from "react"; -import { Reveal } from "@/src/components/Reveal"; - -/** - * The opening of a chapter: the line coming down from the question the chapter - * before ended on. The question itself is not repeated here; it stands directly - * above (see `NextChapter`) and in the header. - */ -export const ChapterThread: React.FC<{ dark?: boolean }> = ({ - dark = false, -}) => ( -