Improve docs landing page
Lint / lint (push) Failing after 44s

This commit is contained in:
Julien Valverdé
2026-07-22 04:24:59 +02:00
parent 4be5b12121
commit 9789969ecf
2 changed files with 793 additions and 78 deletions
+611 -39
View File
@@ -1,80 +1,652 @@
.page { .page {
--home-ink: #102f31;
--home-muted: #506a6c;
--home-teal: #08777b;
--home-teal-bright: #16a3a1;
--home-amber: #e99526;
--home-paper: #fffdf8;
--home-card: rgba(255, 255, 255, 0.76);
--home-line: rgba(16, 47, 49, 0.12);
min-height: calc(100vh - var(--ifm-navbar-height)); min-height: calc(100vh - var(--ifm-navbar-height));
overflow: hidden;
position: relative;
background: background:
radial-gradient(circle at top left, rgba(11, 111, 116, 0.22), transparent 32rem), radial-gradient(circle at 8% 5%, rgba(14, 145, 148, 0.2), transparent 28rem),
radial-gradient(circle at 85% 20%, rgba(244, 166, 54, 0.18), transparent 28rem), radial-gradient(circle at 92% 14%, rgba(240, 159, 48, 0.2), transparent 27rem),
linear-gradient(135deg, #f8f4ea 0%, #eef8f7 48%, #fffaf1 100%); linear-gradient(145deg, #f7f2e7 0%, #edf8f6 48%, #fff8ed 100%);
color: var(--home-ink);
}
.gridBackdrop {
background-image:
linear-gradient(rgba(16, 47, 49, 0.045) 1px, transparent 1px),
linear-gradient(90deg, rgba(16, 47, 49, 0.045) 1px, transparent 1px);
background-size: 48px 48px;
inset: 0;
mask-image: linear-gradient(to bottom, black, transparent 62%);
pointer-events: none;
position: absolute;
} }
.hero { .hero {
padding: 8rem 1rem 5rem; align-items: center;
display: grid;
gap: clamp(3rem, 6vw, 6rem);
grid-template-columns: minmax(0, 0.88fr) minmax(560px, 1.12fr);
min-height: min(820px, calc(100vh - var(--ifm-navbar-height)));
padding-bottom: 6rem;
padding-top: 6rem;
position: relative;
z-index: 1;
} }
.eyebrow { .heroCopy {
color: #0b6f74; max-width: 650px;
font-size: 0.85rem; }
font-weight: 800;
letter-spacing: 0.16em; .eyebrow,
margin-bottom: 1.25rem; .sectionKicker {
align-items: center;
color: var(--home-teal);
display: flex;
font-size: 0.76rem;
font-weight: 850;
gap: 0.65rem;
letter-spacing: 0.18em;
margin: 0 0 1.35rem;
text-transform: uppercase; text-transform: uppercase;
} }
.pulse {
background: var(--home-teal-bright);
border-radius: 50%;
box-shadow: 0 0 0 5px rgba(22, 163, 161, 0.14);
height: 0.55rem;
width: 0.55rem;
}
.hero h1 { .hero h1 {
color: #132c2f; color: var(--home-ink);
font-size: clamp(3rem, 9vw, 6.75rem); font-size: clamp(3.4rem, 6.4vw, 6.4rem);
letter-spacing: -0.08em; font-weight: 780;
line-height: 0.9; letter-spacing: -0.075em;
line-height: 0.92;
margin: 0; margin: 0;
max-width: 880px; }
.hero h1 span {
background: linear-gradient(105deg, var(--home-teal) 5%, #1a9793 48%, var(--home-amber));
background-clip: text;
color: transparent;
display: inline;
-webkit-background-clip: text;
} }
.lede { .lede {
color: #385256; color: var(--home-muted);
font-size: clamp(1.15rem, 2vw, 1.45rem); font-size: clamp(1.08rem, 1.55vw, 1.32rem);
line-height: 1.65; line-height: 1.65;
margin: 2rem 0 0; margin: 2rem 0 0;
max-width: 720px; max-width: 630px;
} }
.actions { .actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1rem; gap: 0.8rem;
margin-top: 2.5rem; margin-top: 2.3rem;
} }
.cards { .primaryAction,
.secondaryAction,
.ctaAction {
align-items: center;
border-radius: 999px;
display: inline-flex;
font-weight: 750;
gap: 0.8rem;
justify-content: center;
padding: 0.88rem 1.35rem;
text-decoration: none !important;
transition: transform 160ms ease, box-shadow 160ms ease, background 160ms ease;
}
.primaryAction {
background: var(--home-ink);
box-shadow: 0 12px 28px rgba(16, 47, 49, 0.2);
color: #f6fffd;
}
.primaryAction:hover {
box-shadow: 0 16px 36px rgba(16, 47, 49, 0.28);
color: #fff;
transform: translateY(-2px);
}
.secondaryAction {
background: rgba(255, 255, 255, 0.58);
border: 1px solid var(--home-line);
color: var(--home-ink);
}
.secondaryAction:hover {
background: rgba(255, 255, 255, 0.9);
color: var(--home-teal);
transform: translateY(-2px);
}
.install {
align-items: center;
color: var(--home-muted);
display: flex;
font-size: 0.84rem;
gap: 0.7rem;
margin-top: 1.4rem;
}
.install span {
color: var(--home-amber);
font-family: var(--ifm-font-family-monospace);
font-weight: 800;
}
.install code {
background: transparent;
border: 0;
color: inherit;
padding: 0;
}
.codeStage {
min-width: 0;
perspective: 1200px;
position: relative;
}
.codeGlow {
background: linear-gradient(115deg, rgba(11, 133, 136, 0.48), rgba(238, 156, 40, 0.38));
filter: blur(55px);
inset: 8% 5%;
opacity: 0.55;
position: absolute;
}
.codeWindow {
background: #101b1e;
border: 1px solid rgba(166, 225, 220, 0.2);
border-radius: 1.25rem;
box-shadow:
0 34px 90px rgba(18, 49, 52, 0.3),
0 2px 0 rgba(255, 255, 255, 0.06) inset;
overflow: hidden;
position: relative;
transform: rotateY(-2deg) rotateX(1deg);
}
.windowBar {
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
color: #b6c9c9;
display: grid; display: grid;
gap: 1.25rem; font-family: var(--ifm-font-family-monospace);
grid-template-columns: repeat(3, minmax(0, 1fr)); font-size: 0.73rem;
padding: 0 1rem 6rem; grid-template-columns: 1fr auto 1fr;
padding: 0.78rem 1rem;
} }
.cards article { .windowDots {
background: rgba(255, 255, 255, 0.74); display: flex;
border: 1px solid rgba(19, 44, 47, 0.12); gap: 0.38rem;
border-radius: 1.5rem;
box-shadow: 0 24px 80px rgba(19, 44, 47, 0.08);
padding: 1.5rem;
} }
.cards h2 { .windowDots span {
color: #132c2f; background: #4f6d6e;
font-size: 1.2rem; border-radius: 50%;
margin-bottom: 0.75rem; height: 0.55rem;
width: 0.55rem;
} }
.cards p { .windowDots span:first-child {
color: #486267; background: #ef8d5b;
}
.windowDots span:nth-child(2) {
background: #e9bc50;
}
.windowDots span:last-child {
background: #65bd92;
}
.windowStatus {
color: #74d2cd;
justify-self: end;
}
.codeWindow :global(.theme-code-block) {
border-radius: 0;
box-shadow: none;
margin: 0; margin: 0;
} }
.codeWindow :global(.theme-code-block pre) {
background: #101b1e !important;
color: #d7e5e4 !important;
font-size: 0.78rem;
line-height: 1.62;
max-height: none;
padding: 1.45rem 1.6rem !important;
}
.codeWindow :global(.token-line),
.codeWindow :global(.token.plain),
.codeWindow :global(.token.imports),
.codeWindow :global(.token.maybe-class-name),
.codeWindow :global(.token.punctuation),
.codeWindow :global(.token.operator),
.codeWindow :global(.token.plain-text),
.codeWindow :global(.token.property-access) {
color: #d7e5e4 !important;
}
.codeWindow :global(.token.keyword) {
color: #c792ea !important;
}
.codeWindow :global(.token.string) {
color: #c3e88d !important;
}
.codeWindow :global(.token.function),
.codeWindow :global(.token.method) {
color: #82aaff !important;
}
.codeWindow :global(.token.tag) {
color: #f07178 !important;
}
.codeWindow :global(.token.builtin) {
color: #ffcb6b !important;
}
.codeLegend {
border-top: 1px solid rgba(255, 255, 255, 0.08);
color: #a9bcbc;
display: grid;
font-size: 0.7rem;
grid-template-columns: repeat(3, 1fr);
}
.codeLegend span {
padding: 0.8rem 0.9rem;
text-align: center;
}
.codeLegend span + span {
border-left: 1px solid rgba(255, 255, 255, 0.08);
}
.codeLegend b {
color: #e8a94b;
font-family: var(--ifm-font-family-monospace);
margin-right: 0.25rem;
}
.featuresSection {
padding-bottom: 7rem;
padding-top: 4rem;
position: relative;
z-index: 1;
}
.sectionHeading {
display: grid;
gap: 0 3rem;
grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.75fr);
margin-bottom: 2.8rem;
}
.sectionHeading .sectionKicker {
grid-column: 1 / -1;
}
.sectionHeading h2,
.cta h2 {
color: var(--home-ink);
font-size: clamp(2.25rem, 4vw, 4rem);
letter-spacing: -0.055em;
line-height: 1;
margin: 0;
}
.sectionHeading > p:last-child {
align-self: end;
color: var(--home-muted);
font-size: 1.03rem;
line-height: 1.65;
margin: 0;
}
.featureGrid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.featureCard {
background: var(--home-card);
border: 1px solid var(--home-line);
border-radius: 1.35rem;
color: inherit;
display: flex;
flex-direction: column;
min-height: 310px;
overflow: hidden;
padding: 1.45rem;
position: relative;
text-decoration: none !important;
transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
}
.featureCard::before {
background: linear-gradient(90deg, var(--home-teal-bright), var(--home-amber));
content: "";
height: 3px;
left: 1.45rem;
opacity: 0;
position: absolute;
right: 1.45rem;
top: 0;
transform: scaleX(0.5);
transition: opacity 180ms ease, transform 180ms ease;
}
.featureCard:hover {
border-color: rgba(8, 119, 123, 0.35);
box-shadow: 0 24px 60px rgba(28, 65, 66, 0.12);
color: inherit;
transform: translateY(-5px);
}
.featureCard:hover::before {
opacity: 1;
transform: scaleX(1);
}
.cardTopline {
align-items: center;
display: flex;
justify-content: space-between;
}
.featureIcon {
align-items: center;
background: rgba(8, 119, 123, 0.1);
border: 1px solid rgba(8, 119, 123, 0.12);
border-radius: 0.8rem;
color: var(--home-teal);
display: flex;
font-family: var(--ifm-font-family-monospace);
font-size: 1rem;
font-weight: 850;
height: 2.7rem;
justify-content: center;
width: 2.7rem;
}
.cardIndex {
color: rgba(16, 47, 49, 0.32);
font-family: var(--ifm-font-family-monospace);
font-size: 0.72rem;
}
.featureCard h3 {
color: var(--home-ink);
font-size: 1.25rem;
letter-spacing: -0.025em;
margin: 2rem 0 0.7rem;
}
.featureCard > p {
color: var(--home-muted);
font-size: 0.94rem;
line-height: 1.65;
margin: 0;
}
.cardFooter {
align-items: center;
display: flex;
justify-content: space-between;
margin-top: auto;
padding-top: 1.5rem;
}
.cardFooter code {
background: rgba(8, 119, 123, 0.08);
border: 0;
color: var(--home-teal);
font-size: 0.7rem;
padding: 0.35rem 0.5rem;
}
.cardFooter > span {
color: var(--home-amber);
font-size: 1.1rem;
transition: transform 180ms ease;
}
.featureCard:hover .cardFooter > span {
transform: translate(3px, -3px);
}
.ctaWrap {
padding-bottom: 7rem;
position: relative;
z-index: 1;
}
.cta {
align-items: center;
background:
radial-gradient(circle at 90% 20%, rgba(233, 149, 38, 0.22), transparent 20rem),
linear-gradient(125deg, #12383a, #10292c);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1.75rem;
box-shadow: 0 28px 80px rgba(16, 47, 49, 0.2);
display: flex;
gap: 3rem;
justify-content: space-between;
overflow: hidden;
padding: clamp(2rem, 5vw, 4rem);
}
.cta .sectionKicker {
color: #79d5d1;
}
.cta h2 {
color: #f7fffd;
}
.cta p:last-child {
color: #b9cccc;
line-height: 1.65;
margin: 1rem 0 0;
max-width: 650px;
}
.ctaAction {
background: #f0a13b;
color: #162d2e;
flex: 0 0 auto;
}
.ctaAction:hover {
box-shadow: 0 14px 35px rgba(0, 0, 0, 0.24);
color: #102526;
transform: translateY(-2px);
}
:global([data-theme="dark"]) .page {
--home-ink: #e9f7f5;
--home-muted: #a9bfbd;
--home-teal: #79d5d1;
--home-teal-bright: #5bc8c5;
--home-amber: #f1ad4e;
--home-card: rgba(20, 42, 45, 0.76);
--home-line: rgba(174, 221, 218, 0.14);
background:
radial-gradient(circle at 8% 5%, rgba(33, 154, 154, 0.17), transparent 28rem),
radial-gradient(circle at 92% 14%, rgba(209, 130, 31, 0.13), transparent 27rem),
linear-gradient(145deg, #0f1b1e 0%, #102426 48%, #161e1e 100%);
}
:global([data-theme="dark"]) .gridBackdrop {
background-image:
linear-gradient(rgba(174, 221, 218, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(174, 221, 218, 0.04) 1px, transparent 1px);
}
:global([data-theme="dark"]) .secondaryAction {
background: rgba(19, 43, 46, 0.75);
}
:global([data-theme="dark"]) .secondaryAction:hover {
background: rgba(27, 57, 60, 0.95);
}
:global([data-theme="dark"]) .primaryAction {
background: #dff8f5;
color: #123638;
}
:global([data-theme="dark"]) .primaryAction:hover {
color: #0b282a;
}
:global([data-theme="dark"]) .cardIndex {
color: rgba(220, 242, 239, 0.35);
}
@media screen and (max-width: 1180px) {
.hero {
gap: 3rem;
grid-template-columns: minmax(0, 0.85fr) minmax(500px, 1.15fr);
}
.hero h1 {
font-size: clamp(3.2rem, 6vw, 5rem);
}
}
@media screen and (max-width: 996px) { @media screen and (max-width: 996px) {
.hero { .hero {
padding-top: 5rem; grid-template-columns: minmax(0, 1fr);
padding-bottom: 4rem;
padding-top: 4.5rem;
} }
.cards { .heroCopy,
grid-template-columns: 1fr; .codeStage,
.codeWindow,
.codeWindow :global(.theme-code-block) {
min-width: 0;
width: 100%;
}
.heroCopy {
max-width: 760px;
}
.codeStage {
margin: 0 auto;
max-width: 760px;
width: 100%;
}
.codeWindow {
transform: none;
}
.featureGrid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media screen and (max-width: 700px) {
.hero {
box-sizing: border-box;
margin-left: 0;
margin-right: 0;
max-width: 100vw;
padding-left: 1.15rem;
padding-right: 1.15rem;
width: 100vw;
}
.heroCopy,
.codeStage {
max-width: calc(100vw - 2.3rem);
}
.hero h1 {
font-size: clamp(3rem, 15vw, 4.3rem);
}
.sectionHeading {
grid-template-columns: 1fr;
}
.sectionHeading > p:last-child {
margin-top: 1.25rem;
}
.featureGrid {
grid-template-columns: 1fr;
}
.featureCard {
min-height: 275px;
}
.codeLegend {
grid-template-columns: 1fr;
}
.codeLegend span + span {
border-left: 0;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.windowStatus {
display: none;
}
.windowBar {
grid-template-columns: 1fr auto;
}
.codeWindow :global(.theme-code-block pre) {
font-size: 0.69rem;
overflow-x: auto;
padding: 1.1rem !important;
}
.cta {
align-items: flex-start;
flex-direction: column;
}
}
@media (prefers-reduced-motion: reduce) {
.featureCard,
.featureCard::before,
.cardFooter > span,
.primaryAction,
.secondaryAction,
.ctaAction {
transition: none;
} }
} }
+182 -39
View File
@@ -1,60 +1,203 @@
import Link from "@docusaurus/Link" import Link from "@docusaurus/Link"
import CodeBlock from "@theme/CodeBlock"
import Layout from "@theme/Layout" import Layout from "@theme/Layout"
import clsx from "clsx" import clsx from "clsx"
import type { ReactNode } from "react" import type { ReactNode } from "react"
import styles from "./index.module.css" import styles from "./index.module.css"
const componentExample = `import { Effect } from "effect"
import { Async, Component } from "effect-view"
import { Users } from "./services"
export const UserCard = Component.make("UserCard")(
function* ({ userId }: { userId: string }) {
const users = yield* Users
const user = yield* Component.useOnChange(
() => users.find(userId),
[userId],
)
yield* Component.useReactEffect(
() => Effect.forkScoped(users.watch(userId)),
[userId],
)
return (
<article>
<h2>{user.name}</h2>
<p>{user.email}</p>
</article>
)
},
).pipe(Async.async)`
const features = [
{
icon: "fx",
title: "Components are Effect programs",
description:
"Yield Effects and services directly from a component body, then return ordinary JSX. Types track every dependency all the way to the runtime boundary.",
tag: "Component.make",
to: "/docs/getting-started#write-your-first-component",
},
{
icon: "R",
title: "One typed runtime",
description:
"Build your application Layer once. Components access HTTP clients, repositories, configuration, tracing, and your own services without React context plumbing.",
tag: "ReactRuntime",
to: "/docs/getting-started#create-the-runtime",
},
{
icon: "S",
title: "Scopes follow React",
description:
"Every component owns an Effect Scope. Resources, subscriptions, and fibers are finalized when their component or dependency lifecycle ends.",
tag: "Scope.Scope",
to: "/docs/getting-started#the-component-root-scope",
},
{
icon: "↔",
title: "Reactive state without a new universe",
description:
"Lens and View connect Effect state to React. Focus large models into small writable values and subscribe only where rendering needs them.",
tag: "Lens + View",
to: "/docs/state-management",
},
{
icon: "Q",
title: "TanStack-shaped server state",
description:
"Reactive keys, caching, stale times, background refresh, mutations, and invalidation—with typed Effect services, Causes, and scoped fibers underneath.",
tag: "Query + Mutation",
to: "/docs/query",
},
{
icon: "Σ",
title: "Forms driven by Schema",
description:
"Keep input-friendly encoded values while application code receives validated, decoded types. Focus one root into reusable field-sized subforms.",
tag: "MutationForm + LensForm",
to: "/docs/forms",
},
] as const
export default function Home(): ReactNode { export default function Home(): ReactNode {
return ( return (
<Layout <Layout
title="effect-fc" title="Effect View"
description="Write React function components with Effect" description="Write React function components as typed Effect programs"
> >
<main className={styles.page}> <main className={styles.page}>
<div className={styles.gridBackdrop} aria-hidden="true" />
<section className={clsx("container", styles.hero)}> <section className={clsx("container", styles.hero)}>
<p className={styles.eyebrow}>Effect for React function components</p> <div className={styles.heroCopy}>
<h1>Write components as Effect programs.</h1> <div className={styles.eyebrow}>
<p className={styles.lede}> <span className={styles.pulse} />
effect-fc gives React 19 components access to Effect services, Effect View for React 19
scopes, subscriptions, and async workflows without giving up normal </div>
React boundaries. <h1>
</p> React components,
<div className={styles.actions}> <span> powered by Effect.</span>
<Link className="button button--primary button--lg" to="/docs/getting-started"> </h1>
Get Started <p className={styles.lede}>
</Link> Bring typed services, scoped resources, reactive state, server
<Link queries, and schema-driven forms into React without hiding either
className="button button--secondary button--lg" framework.
to="https://github.com/Thiladev/effect-fc" </p>
>
GitHub <div className={styles.actions}>
</Link> <Link className={styles.primaryAction} to="/docs/getting-started">
Start building
<span aria-hidden="true"></span>
</Link>
<Link
className={styles.secondaryAction}
to="https://github.com/Thiladev/effect-fc"
>
View on GitHub
</Link>
</div>
<div className={styles.install}>
<span aria-hidden="true">$</span>
<code>npm install effect-view effect@beta</code>
</div>
</div>
<div className={styles.codeStage}>
<div className={styles.codeGlow} aria-hidden="true" />
<div className={styles.codeWindow}>
<div className={styles.windowBar}>
<div className={styles.windowDots} aria-hidden="true">
<span />
<span />
<span />
</div>
<span>UserCard.tsx</span>
<span className={styles.windowStatus}>Effect + JSX</span>
</div>
<CodeBlock language="tsx">{componentExample}</CodeBlock>
<div className={styles.codeLegend}>
<span><b>01</b> Yield services</span>
<span><b>02</b> Own the lifecycle</span>
<span><b>03</b> Return JSX</span>
</div>
</div>
</div> </div>
</section> </section>
<section className={clsx("container", styles.cards)}> <section className={clsx("container", styles.featuresSection)}>
<article> <div className={styles.sectionHeading}>
<h2>Generator components</h2> <p className={styles.sectionKicker}>Why Effect View</p>
<h2>One model from render to resources.</h2>
<p> <p>
Use <code>Component.make</code> to yield Effects and return JSX Effect View adds the pieces React deliberately leaves open while
from the same component body. preserving the React component model you already know.
</p> </p>
</article> </div>
<article>
<h2>Runtime at the edge</h2> <div className={styles.featureGrid}>
<p> {features.map((feature, index) => (
Provide your app layer once with <code>ReactRuntime.Provider</code> <Link
and convert Effect-FC components at React boundaries. className={styles.featureCard}
</p> to={feature.to}
</article> key={feature.title}
<article> >
<h2>Scoped lifecycles</h2> <div className={styles.cardTopline}>
<p> <span className={styles.featureIcon}>{feature.icon}</span>
Tie subscriptions and resources to component scopes so finalizers <span className={styles.cardIndex}>
run when React unmounts. {String(index + 1).padStart(2, "0")}
</p> </span>
</article> </div>
<h3>{feature.title}</h3>
<p>{feature.description}</p>
<div className={styles.cardFooter}>
<code>{feature.tag}</code>
<span aria-hidden="true"></span>
</div>
</Link>
))}
</div>
</section>
<section className={clsx("container", styles.ctaWrap)}>
<div className={styles.cta}>
<div>
<p className={styles.sectionKicker}>Ready when React is</p>
<h2>Start with one Effect component.</h2>
<p>
Add a runtime, cross the React boundary once, and grow into the
rest of the toolkit only when you need it.
</p>
</div>
<Link className={styles.ctaAction} to="/docs/getting-started">
Read the guide <span aria-hidden="true"></span>
</Link>
</div>
</section> </section>
</main> </main>
</Layout> </Layout>