T45c Site · Home: a full-screen hero, five live features beside their miniatures, counts on the beat, the chapters as a city, a closing call; smooth scroll, reveals from scroll, a fade between routes; the OG image from the new hero
This commit is contained in:
1 parent
7fd05df4dc
commit
b47f2267e4
10 files changed
+355
-49
No files matched your search
@@ -0,0 +1,37 @@
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import { chapters } from "../data";
|
||||
import { Iso } from "../iso/Iso";
|
||||
import { city } from "../iso/models";
|
||||
|
||||
const CITY = city(chapters.map((c) => c.stories.length));
|
||||
|
||||
/** Chapters as plinths; a plinth and its name light together. */
|
||||
export function City() {
|
||||
const navigate = useNavigate();
|
||||
// Plinth i + 1 is chapter i; 0 is the table.
|
||||
const [at, setAt] = useState<number | null>(null);
|
||||
const open = (i: number) => navigate({ to: "/components/$page/", params: { page: chapters[i].slug } });
|
||||
return (
|
||||
<section className="city">
|
||||
<h2 className="rise">{at === null ? `${chapters.length} chapters.` : `${chapters[at].title}.`}</h2>
|
||||
<Iso
|
||||
model={CITY}
|
||||
label="Every chapter a plinth, as tall as its stories are many"
|
||||
className="city-iso"
|
||||
onHover={(i) => setAt(i === null ? null : i - 1)}
|
||||
onPick={(i) => open(i - 1)}
|
||||
lift={at === null ? null : at + 1}
|
||||
/>
|
||||
<ol className="city-names" onMouseLeave={() => setAt(null)}>
|
||||
{chapters.map((c, i) => (
|
||||
<li key={c.slug} className={at === i ? "on" : undefined}>
|
||||
<Link to="/components/$page/" params={{ page: c.slug }} onMouseEnter={() => setAt(i)} onFocus={() => setAt(i)} onBlur={() => setAt(null)}>
|
||||
{c.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+115
-14
@@ -1,25 +1,126 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { useEffect } from "react";
|
||||
import { chapters, componentCount } from "../data";
|
||||
import { Iso } from "../iso/Iso";
|
||||
import { app } from "../iso/models";
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { all, chapters, componentCount, REPO } from "../data";
|
||||
import { Iso, useStill } from "../iso/Iso";
|
||||
import { BEAT, strike } from "../iso/marimba";
|
||||
import { app, bars, calendar, candles, chat, editor } from "../iso/models";
|
||||
import { Live } from "../Live";
|
||||
import { City } from "./City";
|
||||
|
||||
const APP = app();
|
||||
|
||||
const FEATURES = [
|
||||
{ title: "Markets.", line: "Candles, books and tapes, redrawn every frame.", model: candles(), page: "finance", story: "candlestickchart-ohlcchart-heikinashichart-linequotechart-volumechart-charttypeswitcher-timerangeselector-intervalselector" },
|
||||
{ title: "Charts.", line: "Areas, bars and stacks that answer the pointer.", model: bars(), page: "charts", story: "areachart-stackedarea-barchart" },
|
||||
{ title: "Conversations.", line: "Bubbles, avatars and the day between them.", model: chat(), page: "chat", story: "chatcontainer-messagelist-messagebubble-messageavatar-messageheader-messagefooter-dateseparator-scrolltobottombutton" },
|
||||
{ title: "Code.", line: "Folds, minimap, brackets and many cursors.", model: editor(), page: "editor", story: "codeeditor-editorgutter-linenumbers-foldingcontrol-minimap-ruler-indentguide-bracketmatcher-rainbowbrackets-multicursor-selectionhighlight-stickyscroll-breadcrumb-inlayhints-codelens-inlinedecoration" },
|
||||
{ title: "Time.", line: "A month of days, its events in place.", model: calendar(), page: "calendar", story: "calendarmonthview" },
|
||||
].map((f) => {
|
||||
const story = chapters.find((c) => c.slug === f.page)?.stories.find((s) => s.slug === f.story);
|
||||
if (!story) throw new Error(`home: no story ${f.page}/${f.story}`);
|
||||
return { ...f, name: story.title };
|
||||
});
|
||||
|
||||
/** Mounts its children only while near the view. */
|
||||
function Near({ className, children }: { className: string; children: ReactNode }) {
|
||||
const box = useRef<HTMLDivElement>(null);
|
||||
const [near, setNear] = useState(false);
|
||||
useEffect(() => {
|
||||
const seen = new IntersectionObserver(([e]) => setNear(e.isIntersecting), { rootMargin: "100% 0px" });
|
||||
seen.observe(box.current!);
|
||||
return () => seen.disconnect();
|
||||
}, []);
|
||||
return (
|
||||
<div ref={box} className={className}>
|
||||
{near && children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Counts up in sixteen beats once it shows. */
|
||||
function Count({ to }: { to: number }) {
|
||||
const box = useRef<HTMLSpanElement>(null);
|
||||
const [n, setN] = useState(to);
|
||||
const still = useStill();
|
||||
useEffect(() => {
|
||||
if (still) return setN(to);
|
||||
let frame = 0;
|
||||
const seen = new IntersectionObserver(
|
||||
([e]) => {
|
||||
if (!e.isIntersecting) return;
|
||||
seen.disconnect();
|
||||
const start = performance.now();
|
||||
let step = -1;
|
||||
const tick = (now: number) => {
|
||||
const next = Math.min(16, Math.floor((now - start) / BEAT));
|
||||
if (next !== step) {
|
||||
step = next;
|
||||
setN(Math.round(to * (1 - (1 - step / 16) ** 3)));
|
||||
strike(step, 0.4);
|
||||
}
|
||||
if (step < 16) frame = requestAnimationFrame(tick);
|
||||
};
|
||||
frame = requestAnimationFrame(tick);
|
||||
},
|
||||
{ threshold: 0.6 },
|
||||
);
|
||||
seen.observe(box.current!);
|
||||
return () => {
|
||||
seen.disconnect();
|
||||
cancelAnimationFrame(frame);
|
||||
};
|
||||
}, [to, still]);
|
||||
return <span ref={box}>{n}</span>;
|
||||
}
|
||||
|
||||
export function Home() {
|
||||
useEffect(() => {
|
||||
document.title = "Ely · GPUI components";
|
||||
}, []);
|
||||
return (
|
||||
<section className="hero">
|
||||
<Iso model={APP} label="A miniature app growing cell by cell" className="hero-iso" />
|
||||
<h1>Components for GPUI.</h1>
|
||||
<p>
|
||||
{componentCount} components in {chapters.length} chapters, live in the browser.
|
||||
</p>
|
||||
<Link to="/components/" className="button">
|
||||
Browse
|
||||
</Link>
|
||||
</section>
|
||||
<>
|
||||
<section className="hero">
|
||||
<Iso model={APP} label="A miniature app growing cell by cell" className="hero-iso" />
|
||||
<h1>Components for GPUI.</h1>
|
||||
<p>
|
||||
{componentCount} components in {chapters.length} chapters, live in the browser.
|
||||
</p>
|
||||
<Link to="/components/" className="button">
|
||||
Browse
|
||||
</Link>
|
||||
</section>
|
||||
{FEATURES.map((f) => (
|
||||
<section className="feature" key={f.page}>
|
||||
<div className="feature-words">
|
||||
<h2 className="rise">{f.title}</h2>
|
||||
<p className="rise">{f.line}</p>
|
||||
</div>
|
||||
<Iso model={f.model} label={`A miniature of ${f.title.slice(0, -1).toLowerCase()}`} className="feature-iso" />
|
||||
<Near className="feature-live">
|
||||
<Live page={f.page} story={f.story} title={f.name} />
|
||||
</Near>
|
||||
</section>
|
||||
))}
|
||||
<section className="tally">
|
||||
{[
|
||||
[componentCount, "components"],
|
||||
[all.length, "stories"],
|
||||
[chapters.length, "chapters"],
|
||||
].map(([n, label]) => (
|
||||
<p className="rise" key={label}>
|
||||
<Count to={n as number} />
|
||||
<span>{label}</span>
|
||||
</p>
|
||||
))}
|
||||
</section>
|
||||
<City />
|
||||
<section className="close">
|
||||
<h2 className="rise">Build with Ely.</h2>
|
||||
<code className="rise">ely-gpui-component = {`{ git = "${REPO}" }`}</code>
|
||||
<Link to="/components/" className="button rise">
|
||||
Browse
|
||||
</Link>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user