T45d Site · Components: one search over chapters, stories and components (Slash, Enter, Escape), the index in hairline rows, the story beside a sticky chapter rail, arrows between stories
This commit is contained in:
1 parent
9ea811d4e8
commit
8712fd0389
5 files changed
+173
-48
No files matched your search
@@ -1,22 +1,35 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { useEffect } from "react";
|
||||
import { chapters, pad } from "../data";
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { pad, search } from "../data";
|
||||
import { Find } from "../Find";
|
||||
|
||||
export function Chapters() {
|
||||
const [query, setQuery] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const found = search(query);
|
||||
useEffect(() => {
|
||||
document.title = "Components · Ely";
|
||||
}, []);
|
||||
const first = () => found[0] && navigate({ to: "/components/$page/$story/", params: { page: found[0].slug, story: found[0].stories[0].slug } });
|
||||
return (
|
||||
<section className="chapters">
|
||||
<h1>Components</h1>
|
||||
<h1>Components.</h1>
|
||||
<Find className="find" query={query} onQuery={setQuery} onEnter={first} />
|
||||
{found.length === 0 && <p className="none">Nothing matches “{query}”.</p>}
|
||||
<ol>
|
||||
{chapters.map((c) => (
|
||||
{found.map((c) => (
|
||||
<li key={c.slug}>
|
||||
<Link to="/components/$page/$story/" params={{ page: c.slug, story: c.stories[0].slug }}>
|
||||
<span className="n">{pad(c.number)}</span>
|
||||
<span className="name">{c.title}</span>
|
||||
<span className="count">{c.stories.length}</span>
|
||||
<span className="n">{pad(c.number)}</span>
|
||||
<Link className="name" to="/components/$page/$story/" params={{ page: c.slug, story: c.stories[0].slug }}>
|
||||
{c.title}
|
||||
</Link>
|
||||
<span className="titles">
|
||||
{c.stories.map((s) => (
|
||||
<Link key={s.slug} to="/components/$page/$story/" params={{ page: c.slug, story: s.slug }}>
|
||||
{s.title}
|
||||
</Link>
|
||||
))}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
@@ -1,46 +1,92 @@
|
||||
import { Link, useParams } from "@tanstack/react-router";
|
||||
import { useEffect } from "react";
|
||||
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Live } from "../Live";
|
||||
import { Capture, Windows, isCaptured } from "../Native";
|
||||
import { all, chapters } from "../data";
|
||||
import { all, chapters, search } from "../data";
|
||||
import { Find, typing } from "../Find";
|
||||
import { NotFound } from "./NotFound";
|
||||
|
||||
export function Story() {
|
||||
const { page, story } = useParams({ strict: false });
|
||||
const navigate = useNavigate();
|
||||
const [query, setQuery] = useState("");
|
||||
const chapter = chapters.find((c) => c.slug === page);
|
||||
const found = chapter?.stories.find((s) => s.slug === story);
|
||||
const at = all.findIndex((e) => e.story === found);
|
||||
const [prev, next] = at < 0 ? [] : [all[at - 1], all[at + 1]];
|
||||
|
||||
useEffect(() => {
|
||||
if (chapter && found) document.title = `${found.title} · ${chapter.title} · Ely`;
|
||||
}, [chapter, found]);
|
||||
|
||||
useEffect(() => {
|
||||
document.querySelector('.side [aria-current="page"]')?.scrollIntoView({ block: "nearest" });
|
||||
}, [page, story]);
|
||||
|
||||
useEffect(() => {
|
||||
const step = (e: KeyboardEvent) => {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey || typing(e)) return;
|
||||
const to = e.key === "ArrowLeft" ? prev : e.key === "ArrowRight" ? next : undefined;
|
||||
if (to) navigate({ to: "/components/$page/$story/", params: { page: to.chapter.slug, story: to.story.slug } });
|
||||
};
|
||||
addEventListener("keydown", step);
|
||||
return () => removeEventListener("keydown", step);
|
||||
}, [prev, next, navigate]);
|
||||
|
||||
if (!chapter || !found) return <NotFound what={`No story at ${page}/${story}.`} />;
|
||||
const at = all.findIndex((e) => e.story === found);
|
||||
const [prev, next] = [all[at - 1], all[at + 1]];
|
||||
const shown = search(query);
|
||||
const first = () => shown[0] && navigate({ to: "/components/$page/$story/", params: { page: shown[0].slug, story: shown[0].stories[0].slug } });
|
||||
return (
|
||||
<article className="story">
|
||||
<Link to="/components/" className="crumb">
|
||||
{chapter.title}
|
||||
</Link>
|
||||
<h1>{found.title}</h1>
|
||||
{isCaptured(chapter.slug, found.slug) ? (
|
||||
<Capture page={chapter.slug} story={found.slug} title={found.title} />
|
||||
) : (
|
||||
<>
|
||||
<Live key={`${chapter.slug}/${found.slug}`} page={chapter.slug} story={found.slug} title={found.title} />
|
||||
<Windows page={chapter.slug} story={found.slug} title={found.title} />
|
||||
</>
|
||||
)}
|
||||
<nav className="pager" aria-label="More stories">
|
||||
{prev && (
|
||||
<Link to="/components/$page/$story/" params={{ page: prev.chapter.slug, story: prev.story.slug }}>
|
||||
{prev.story.title}
|
||||
</Link>
|
||||
<div className="reader">
|
||||
<aside className="side" data-lenis-prevent>
|
||||
<Find className="find small" query={query} onQuery={setQuery} onEnter={first} />
|
||||
<nav aria-label="Chapters">
|
||||
{shown.map((c) => (
|
||||
<div key={c.slug}>
|
||||
<Link className="side-chapter" to="/components/$page/$story/" params={{ page: c.slug, story: c.stories[0].slug }} data-on={c.slug === chapter.slug || undefined}>
|
||||
{c.title}
|
||||
</Link>
|
||||
{(query || c.slug === chapter.slug) && (
|
||||
<ol>
|
||||
{c.stories.map((s) => (
|
||||
<li key={s.slug}>
|
||||
<Link to="/components/$page/$story/" params={{ page: c.slug, story: s.slug }} activeProps={{ "aria-current": "page" }}>
|
||||
{s.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
<article className="story">
|
||||
<Link to="/components/" className="crumb">
|
||||
{chapter.title}
|
||||
</Link>
|
||||
<h1>{found.title}</h1>
|
||||
{isCaptured(chapter.slug, found.slug) ? (
|
||||
<Capture page={chapter.slug} story={found.slug} title={found.title} />
|
||||
) : (
|
||||
<>
|
||||
<Live key={`${chapter.slug}/${found.slug}`} page={chapter.slug} story={found.slug} title={found.title} />
|
||||
<Windows page={chapter.slug} story={found.slug} title={found.title} />
|
||||
</>
|
||||
)}
|
||||
{next && (
|
||||
<Link className="next" to="/components/$page/$story/" params={{ page: next.chapter.slug, story: next.story.slug }}>
|
||||
{next.story.title}
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
</article>
|
||||
<nav className="pager" aria-label="More stories">
|
||||
{prev && (
|
||||
<Link to="/components/$page/$story/" params={{ page: prev.chapter.slug, story: prev.story.slug }}>
|
||||
← {prev.story.title}
|
||||
</Link>
|
||||
)}
|
||||
{next && (
|
||||
<Link className="next" to="/components/$page/$story/" params={{ page: next.chapter.slug, story: next.story.slug }}>
|
||||
{next.story.title} →
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user