Site · Showcase: apps built with Ely, from the public Ely-GPUI-Showcases repo at a pinned commit

The build fetches showcase.lock's commit and runs the catalog's own validator. /showcase/ lists featured apps first, then the rest by newest or stars, with categories and search; /showcase/<id>/ shows previews, links and the README. The header drops its sound switch below 414px so the fourth link fits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-04 21:39:13 -04:00
1 parent 7e39a9383d
commit f281d01f35
17 files changed
+567 -13

No files matched your search

+2
View File
@@ -4,3 +4,5 @@ public/gallery
.wrangler
test-results
playwright-report
public/showcase
src/showcase.json
+83
View File
@@ -0,0 +1,83 @@
import { expect, test, type Page } from "@playwright/test";
import { SUBMIT, type Catalog } from "../src/apps";
import data from "../src/showcase.json" with { type: "json" };
// The catalog the site was built with: the pinned commit, or ELY_SHOWCASE's checkout.
const catalog = data as Catalog;
const empty = catalog.apps.length === 0;
function watch(page: Page): string[] {
const errors: string[] = [];
page.on("console", (m) => m.type() === "error" && errors.push(m.text()));
page.on("pageerror", (e) => errors.push(e.message));
return errors;
}
test("the header's Showcase link opens the page, with a way to submit", async ({ page }) => {
const errors = watch(page);
await page.goto("/components/");
await page.getByRole("navigation", { name: "Site" }).getByRole("link", { name: "Showcase" }).click();
await expect(page).toHaveURL("/showcase/");
await expect(page.getByRole("heading", { level: 1 })).toHaveText("Showcase.");
await expect(page).toHaveTitle("Showcase · Ely");
await expect(page.getByRole("link", { name: "Submit your app" })).toHaveAttribute("href", SUBMIT);
expect(errors).toEqual([]);
});
test("an empty catalog says so", async ({ page }) => {
test.skip(!empty, "the catalog has apps");
await page.goto("/showcase/");
await expect(page.locator(".none")).toHaveText("No apps yet. Yours could be the first.");
await expect(page.getByRole("searchbox")).toHaveCount(0);
});
test("featured apps come first, then the rest, and categories count them", async ({ page }) => {
test.skip(empty, "the catalog is empty");
await page.goto("/showcase/");
const all = page.getByRole("group", { name: "Category" }).getByRole("button");
await expect(all.first()).toHaveText(`All ${catalog.apps.length}`);
await expect(all).toHaveCount(Object.keys(catalog.categories).length + 1);
if (catalog.featured.length) {
const first = catalog.apps.find((a) => a.id === catalog.featured[0])!;
await expect(page.getByRole("list", { name: "Featured apps" }).locator(".app-name").first()).toContainText(first.name);
}
});
test("search narrows the apps, and a miss says so", async ({ page }) => {
test.skip(empty, "the catalog is empty");
const [app] = catalog.apps;
await page.goto("/showcase/");
const find = page.getByRole("searchbox", { name: "Search apps" });
await find.fill(app.name);
await expect(page.getByRole("list", { name: "Matching apps" }).locator(".app-name").first()).toContainText(app.name);
await find.fill("zzzz-no-such-app");
await expect(page.locator(".none")).toHaveText("Nothing matches.");
await find.press("Escape");
await expect(find).toHaveValue("");
});
test("a card opens its app, with its previews and links", async ({ page }) => {
test.skip(empty, "the catalog is empty");
const errors = watch(page);
const [app] = catalog.apps;
await page.goto("/showcase/");
await page.locator(`.app a[href="/showcase/${app.id}/"]`).first().click();
await expect(page).toHaveURL(`/showcase/${app.id}/`);
await expect(page.getByRole("heading", { level: 1 })).toContainText(app.name);
const shots = page.locator(".app-previews img");
await expect(shots).toHaveCount(app.previews.length);
await expect.poll(() => shots.first().evaluate((img: HTMLImageElement) => img.naturalWidth)).toBeGreaterThan(0);
const links = page.locator(".app-links");
if (app.source) await expect(links.getByRole("link", { name: "Source" })).toHaveAttribute("href", app.source);
if (app.website) await expect(links.getByRole("link", { name: "Website" })).toHaveAttribute("href", app.website);
await expect(page.locator(".app-readme")).toHaveCount(app.html ? 1 : 0);
expect(errors).toEqual([]);
});
test("an unknown app says so", async ({ page }) => {
const errors = watch(page);
// Ids are kebab-case, so no catalog holds this one.
await page.goto("/showcase/no_such_app/");
await expect(page.getByRole("heading", { level: 1 })).toHaveText("Not found");
expect(errors.join()).toContain("No app named no_such_app");
});
+4 -3
View File
@@ -4,13 +4,14 @@
"type": "module",
"scripts": {
"gallery": "../scripts/web.sh public/gallery",
"dev": "vite",
"build": "tsc --noEmit && vite build",
"showcase": "node scripts/showcase.mjs",
"dev": "pnpm showcase && vite",
"build": "pnpm showcase && tsc --noEmit && vite build",
"preview": "vite preview",
"local": "ELY_GALLERY_ASSETS=http://localhost:4173/gallery/assets/ pnpm gallery && pnpm build && vite preview --port 4173 --strictPort",
"deploy": "wrangler deploy",
"e2e": "playwright test",
"test": "node --test src/iso/engine.test.ts"
"test": "node --test src/iso/engine.test.ts src/apps.test.ts"
},
"devDependencies": {
"@playwright/test": "1.63.0",
+71
View File
@@ -0,0 +1,71 @@
// Writes the showcase catalog for the site: the pinned commit, or the checkout ELY_SHOWCASE names.
import { execFileSync } from "node:child_process";
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { Marked } from "marked";
const REPO = "ZacharyZhang-NY/Ely-GPUI-Showcases";
const site = fileURLToPath(new URL("..", import.meta.url));
const pub = join(site, "public", "showcase");
const out = join(site, "src", "showcase.json");
const stamp = join(pub, ".commit");
/** The catalog at `commit`, unpacked with its validator's packages. */
async function fetchCatalog(commit) {
const work = mkdtempSync(join(tmpdir(), "ely-showcase-"));
const url = `https://codeload.github.com/${REPO}/tar.gz/${commit}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`showcase: ${url} answered ${res.status}`);
writeFileSync(join(work, "catalog.tar.gz"), Buffer.from(await res.arrayBuffer()));
execFileSync("tar", ["-xzf", "catalog.tar.gz"], { cwd: work });
const root = join(work, `${REPO.split("/")[1]}-${commit}`);
const pnpm = process.env.npm_execpath;
if (!pnpm) throw new Error("showcase: run through pnpm, which installs the catalog's validator");
execFileSync(process.execPath, [pnpm, "install", "--frozen-lockfile", "--ignore-scripts", "--silent"], { cwd: root, stdio: "inherit" });
return { root, done: () => rmSync(work, { recursive: true, force: true }) };
}
/** Checks the catalog at `root` with its own validator and writes the site's copy. */
async function write(root, source) {
const { read } = await import(pathToFileURL(join(root, "scripts", "catalog.mjs")).href);
const catalog = read(root);
rmSync(pub, { recursive: true, force: true });
mkdirSync(pub, { recursive: true });
const apps = catalog.apps.map(({ readme, ...app }) => {
const to = join(pub, app.id);
mkdirSync(to, { recursive: true });
const pictures = new Set(app.previews);
const marked = new Marked({
gfm: true,
walkTokens(token) {
if (token.type === "heading") token.depth = Math.min(6, token.depth + 1);
if (token.type !== "image") return;
pictures.add(token.href);
token.href = `/showcase/${app.id}/${token.href}`;
},
});
const html = readme === null ? null : marked.parse(readme, { async: false });
for (const name of pictures) copyFileSync(join(root, "apps", app.id, name), join(to, name));
return { ...app, html };
});
writeFileSync(out, `${JSON.stringify({ source, categories: catalog.categories, featured: catalog.featured, apps }, null, 2)}\n`);
writeFileSync(stamp, source);
console.log(`showcase: ${apps.length} apps, ${catalog.featured.length} featured, from ${source}`);
}
const local = process.env.ELY_SHOWCASE;
if (local) {
await write(resolve(local), "local");
} else {
const commit = readFileSync(join(site, "showcase.lock"), "utf8").trim();
if (!/^[0-9a-f]{40}$/.test(commit)) throw new Error(`showcase: showcase.lock holds no commit: ${commit}`);
if (existsSync(out) && existsSync(stamp) && readFileSync(stamp, "utf8") === commit) {
console.log(`showcase: ${commit.slice(0, 7)} already written`);
} else {
const { root, done } = await fetchCatalog(commit);
await write(root, commit);
done();
}
}
+1
View File
@@ -0,0 +1 @@
0f8f5b9cc392d4741c239b1c1c0f6071dfc6faa5
+3
View File
@@ -23,6 +23,9 @@ function Header() {
<Link to="/docs/" className="link" activeProps={{ "aria-current": "page" }}>
Docs
</Link>
<Link to="/showcase/" className="link" activeProps={{ "aria-current": "page" }}>
Showcase
</Link>
</nav>
<div className="end">
<button className="tool sound" aria-label={sound ? "Sound off" : "Sound on"} aria-pressed={sound} onClick={() => setSound(!sound)}>
+58
View File
@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { counts, filtered, sorted, split, type App, type Catalog } from "./apps.ts";
const app = (id: string, publishedAt: string, more: Partial<App> = {}): App => ({
id,
name: id,
author: "Ada",
description: `${id} does things.`,
category: "dev",
platforms: ["Linux"],
website: null,
source: `https://github.com/ada/${id}`,
publishedAt,
previews: ["preview0.png"],
html: null,
...more,
});
const old = app("old", "2026-01-01T00:00:00Z", { stars: 900 });
const mid = app("mid", "2026-05-01T00:00:00+08:00", { category: "work", author: "Grace" });
const fresh = app("fresh", "2026-09-01T00:00:00Z", { stars: 5 });
const twin = app("twin", "2026-09-01T00:00:00Z", { stars: 50 });
const catalog: Catalog = { source: "test", categories: { dev: "Developer Tools", work: "Productivity & Media", system: "System & Desktop" }, featured: ["mid", "old"], apps: [old, mid, fresh, twin] };
test("newest comes first, and stars break a tie", () => {
assert.deepEqual(sorted(catalog.apps, "newest").map((a) => a.id), ["twin", "fresh", "mid", "old"]);
});
test("most stars comes first, and an app without stars comes last", () => {
assert.deepEqual(sorted(catalog.apps, "stars").map((a) => a.id), ["old", "twin", "fresh", "mid"]);
});
test("featured apps keep the catalog's order, and the rest leave them out", () => {
const { featured, rest } = split(catalog);
assert.deepEqual(featured.map((a) => a.id), ["mid", "old"]);
assert.deepEqual(rest.map((a) => a.id), ["fresh", "twin"]);
});
test("a featured id with no app fails", () => {
assert.throws(() => split({ ...catalog, featured: ["ghost"] }), /featured ghost is no app/);
});
test("search reads the name, the author and the description, in any case", () => {
assert.deepEqual(filtered(catalog.apps, "FRESH", null).map((a) => a.id), ["fresh"]);
assert.deepEqual(filtered(catalog.apps, "grace", null).map((a) => a.id), ["mid"]);
assert.deepEqual(filtered(catalog.apps, "twin does", null).map((a) => a.id), ["twin"]);
assert.deepEqual(filtered(catalog.apps, " ", null).length, 4);
});
test("a category narrows the search", () => {
assert.deepEqual(filtered(catalog.apps, "", "work").map((a) => a.id), ["mid"]);
assert.deepEqual(filtered(catalog.apps, "fresh", "work"), []);
});
test("every category counts its apps, empty ones too", () => {
assert.deepEqual(counts(catalog), [["dev", "Developer Tools", 3], ["work", "Productivity & Media", 1], ["system", "System & Desktop", 0]]);
});
+61
View File
@@ -0,0 +1,61 @@
/** An app from the showcase catalog, as `scripts/showcase.mjs` writes it. */
export interface App {
id: string;
name: string;
author: string;
description: string;
category: string;
platforms: string[];
website: string | null;
source: string | null;
publishedAt: string;
previews: string[];
building?: boolean;
stars?: number;
starsUpdatedAt?: string;
/** The README, rendered; null without one. */
html: string | null;
}
export interface Catalog {
source: string;
categories: Record<string, string>;
featured: string[];
apps: App[];
}
export type Sort = "newest" | "stars";
export const SUBMIT = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Showcases#submit-an-app";
/** Newest first, then most stars; or most stars, then newest. */
export function sorted(apps: App[], sort: Sort): App[] {
const time = (a: App) => Date.parse(a.publishedAt);
const stars = (a: App) => a.stars ?? -1;
const [first, second] = sort === "newest" ? [time, stars] : [stars, time];
return [...apps].sort((a, b) => first(b) - first(a) || second(b) - second(a) || a.name.localeCompare(b.name));
}
/** The featured apps in their order, and the rest. */
export function split(catalog: Catalog): { featured: App[]; rest: App[] } {
const byId = new Map(catalog.apps.map((a) => [a.id, a]));
const featured = catalog.featured.map((id) => {
const app = byId.get(id);
if (!app) throw new Error(`showcase: featured ${id} is no app`);
return app;
});
return { featured, rest: catalog.apps.filter((a) => !catalog.featured.includes(a.id)) };
}
/** Apps in a category, or all with none, whose name, author or description holds the query. */
export function filtered(apps: App[], query: string, category: string | null): App[] {
const q = query.trim().toLowerCase();
return apps.filter((a) => (!category || a.category === category) && (!q || [a.name, a.author, a.description].some((s) => s.toLowerCase().includes(q))));
}
/** How many apps each category holds. */
export function counts(catalog: Catalog): [string, string, number][] {
return Object.entries(catalog.categories).map(([key, label]) => [key, label, catalog.apps.filter((a) => a.category === key).length]);
}
export const cover = (app: App) => `/showcase/${app.id}/${app.previews[0]}`;
+5 -1
View File
@@ -8,6 +8,8 @@ import { Chapters } from "./pages/Chapters";
import { Docs } from "./pages/Docs";
import { Home } from "./pages/Home";
import { NotFound } from "./pages/NotFound";
import { Showcase } from "./pages/Showcase";
import { ShowcaseApp } from "./pages/ShowcaseApp";
import { Story } from "./pages/Story";
import "./styles.css";
@@ -27,9 +29,11 @@ const chapter = createRoute({
const story = createRoute({ getParentRoute: () => root, path: "/components/$page/$story", component: Story });
const docs = createRoute({ getParentRoute: () => root, path: "/docs", component: Docs });
const doc = createRoute({ getParentRoute: () => root, path: "/docs/$page", component: Docs });
const showcase = createRoute({ getParentRoute: () => root, path: "/showcase", component: Showcase });
const showcaseApp = createRoute({ getParentRoute: () => root, path: "/showcase/$app", component: ShowcaseApp });
const router = createRouter({
routeTree: root.addChildren([home, index, chapter, story, docs, doc]),
routeTree: root.addChildren([home, index, chapter, story, docs, doc, showcase, showcaseApp]),
trailingSlash: "always",
scrollRestoration: true,
});
+125
View File
@@ -0,0 +1,125 @@
import { Link } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { counts, cover, filtered, sorted, split, SUBMIT, type App, type Catalog, type Sort } from "../apps";
import data from "../showcase.json";
export const catalog = data as Catalog;
/** The day an app joined, as its manifest wrote it. */
export const joined = (app: App) =>
new Date(`${app.publishedAt.slice(0, 10)}T00:00:00Z`).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", timeZone: "UTC" });
export const starCount = (n: number) => `${new Intl.NumberFormat("en-US", { notation: "compact" }).format(n)} ${n === 1 ? "star" : "stars"}`;
function Card({ app }: { app: App }) {
return (
<li className="app">
<Link to="/showcase/$app/" params={{ app: app.id }}>
<span className="app-shot">
<img src={cover(app)} alt="" loading="lazy" />
</span>
<span className="app-name">
{app.name}
{app.building && <span className="app-tag">Building</span>}
</span>
<span className="app-by">{app.author}</span>
<span className="app-text">{app.description}</span>
<span className="app-meta">
<span>{app.platforms.join(" · ")}</span>
{app.stars !== undefined && <span>{starCount(app.stars)}</span>}
<span>{joined(app)}</span>
</span>
</Link>
</li>
);
}
function Grid({ apps, label }: { apps: App[]; label: string }) {
return (
<ul className="apps" aria-label={label}>
{apps.map((a) => (
<Card key={a.id} app={a} />
))}
</ul>
);
}
export function Submit() {
return (
<section className="submit">
<h2>Built something with Ely?</h2>
<p>Add it by pull request to the showcase repository.</p>
<a className="button" href={SUBMIT} rel="noopener">
Submit your app
</a>
</section>
);
}
export function Showcase() {
const [query, setQuery] = useState("");
const [category, setCategory] = useState<string | null>(null);
const [sort, setSort] = useState<Sort>("newest");
useEffect(() => {
document.title = "Showcase · Ely";
}, []);
const { featured, rest } = split(catalog);
const searching = query.trim() !== "" || category !== null;
const found = sorted(filtered(catalog.apps, query, category), sort);
const empty = catalog.apps.length === 0;
return (
<>
<section className="showcase">
<h1>Showcase.</h1>
<p className="lede">Apps built with Ely.</p>
{empty ? (
<p className="none">No apps yet. Yours could be the first.</p>
) : (
<>
<input className="find" type="search" placeholder={`Search ${catalog.apps.length} apps`} aria-label="Search apps" value={query} onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => e.key === "Escape" && setQuery("")} />
<div className="filters">
<div className="chips" role="group" aria-label="Category">
<button className="chip" aria-pressed={category === null} onClick={() => setCategory(null)}>
All <span>{catalog.apps.length}</span>
</button>
{counts(catalog).map(([key, label, n]) => (
<button key={key} className="chip" aria-pressed={category === key} onClick={() => setCategory(key)}>
{label} <span>{n}</span>
</button>
))}
</div>
<div className="chips" role="group" aria-label="Sort">
<button className="chip" aria-pressed={sort === "newest"} onClick={() => setSort("newest")}>
Newest
</button>
<button className="chip" aria-pressed={sort === "stars"} onClick={() => setSort("stars")}>
Most stars
</button>
</div>
</div>
{searching ? (
found.length ? <Grid apps={found} label="Matching apps" /> : <p className="none">Nothing matches.</p>
) : (
<>
{featured.length > 0 && (
<>
<h2 className="apps-title">Featured</h2>
<Grid apps={featured} label="Featured apps" />
</>
)}
{rest.length > 0 && (
<>
<h2 className="apps-title">{featured.length ? "More apps" : "All apps"}</h2>
<Grid apps={sorted(rest, sort)} label={featured.length ? "More apps" : "All apps"} />
</>
)}
</>
)}
</>
)}
</section>
<Submit />
</>
);
}
+68
View File
@@ -0,0 +1,68 @@
import { Link, useParams } from "@tanstack/react-router";
import { useEffect } from "react";
import { catalog, joined, starCount, Submit } from "./Showcase";
import { NotFound } from "./NotFound";
export function ShowcaseApp() {
const { app: id } = useParams({ strict: false });
const app = catalog.apps.find((a) => a.id === id);
useEffect(() => {
if (app) document.title = `${app.name} · Showcase · Ely`;
}, [app]);
if (!app) return <NotFound what={`No app named ${id} in the showcase.`} />;
return (
<>
<article className="app-page">
<Link className="crumb" to="/showcase/">
← Showcase
</Link>
<h1>
{app.name}
{app.building && <span className="app-tag">Building</span>}
</h1>
<p className="app-by">{app.author}</p>
<p className="lede">{app.description}</p>
<div className="app-links">
{app.website && (
<a className="button" href={app.website} rel="noopener">
Website
</a>
)}
{app.source && (
<a className={app.website ? "text" : "button"} href={app.source} rel="noopener">
Source
</a>
)}
</div>
<dl className="app-facts">
<div>
<dt>Category</dt>
<dd>{catalog.categories[app.category]}</dd>
</div>
<div>
<dt>Platforms</dt>
<dd>{app.platforms.join(", ")}</dd>
</div>
{app.stars !== undefined && (
<div>
<dt>GitHub</dt>
<dd>{starCount(app.stars)}</dd>
</div>
)}
<div>
<dt>Added</dt>
<dd>{joined(app)}</dd>
</div>
</dl>
<div className="app-previews">
{app.previews.map((name, i) => (
<img key={name} src={`/showcase/${app.id}/${name}`} alt={`${app.name}, screenshot ${i + 1} of ${app.previews.length}`} loading={i ? "lazy" : "eager"} />
))}
</div>
{app.html && <div className="prose app-readme" dangerouslySetInnerHTML={{ __html: app.html }} />}
</article>
<Submit />
</>
);
}
+49 -6
View File
@@ -197,6 +197,41 @@ img.shot { display: block; max-width: 100%; height: auto; }
.code .copy { position: absolute; top: 10px; right: 12px; padding: 4px 8px; font-size: 12px; color: var(--muted); transition: color 160ms ease-out; }
.code .copy:hover { color: var(--fg); }
.showcase, .app-page { padding: 160px 64px 64px; }
.showcase h1 { font-size: clamp(48px, 7vw, 112px); line-height: 0.95; font-weight: 800; letter-spacing: -0.045em; }
.lede { margin-top: 20px; font-size: 20px; line-height: 30px; max-width: 48ch; }
.showcase .none { margin-top: 56px; }
.filters { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 16px 32px; margin: -40px 0 56px; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip { height: 32px; padding: 0 14px; border: 1px solid var(--line); border-radius: 999px; font-size: 13px; color: var(--muted); transition: color 160ms ease-out, border-color 160ms ease-out; }
.chip span { color: var(--faint); font-variant-numeric: tabular-nums; }
.chip:hover, .chip[aria-pressed="true"] { color: var(--fg); border-color: var(--fg); }
.apps-title { margin: 0 0 24px; font-size: 14px; font-weight: 500; letter-spacing: 0; color: var(--muted); }
.apps { list-style: none; margin: 0 0 72px; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr)); gap: 48px 32px; }
.app a { display: grid; gap: 6px; border-radius: 12px; }
.app-shot { display: block; aspect-ratio: 16 / 10; margin-bottom: 14px; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; background: var(--code); }
.app-shot img { display: block; width: 100%; height: 100%; object-fit: contain; transition: transform 320ms ease-out; }
.app a:hover .app-shot img { transform: scale(1.02); }
.app-name { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; font-size: 20px; line-height: 28px; font-weight: 700; letter-spacing: -0.02em; overflow-wrap: anywhere; }
.app-tag { padding: 0 8px; border: 1px solid var(--line); border-radius: 999px; font: 400 12px/20px var(--mono); letter-spacing: 0; color: var(--muted); }
.app-by { font-size: 14px; color: var(--muted); overflow-wrap: anywhere; }
.app-page .lede, .app-facts dd { overflow-wrap: anywhere; }
.app-text { font-size: 15px; line-height: 24px; color: color-mix(in srgb, var(--fg) 82%, var(--bg)); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }
.app-meta { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 4px; font: 400 12px/18px var(--mono); color: var(--faint); }
.submit { display: grid; justify-items: start; gap: 20px; margin: 0 64px; padding: 96px 0 128px; border-top: 1px solid var(--line); }
.submit h2 { font-size: clamp(32px, 4vw, 56px); line-height: 1; font-weight: 800; letter-spacing: -0.04em; }
.app-page { max-width: 1080px; }
.app-page h1 { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin: 8px 0 12px; font-size: clamp(40px, 5vw, 80px); line-height: 1; font-weight: 800; letter-spacing: -0.04em; overflow-wrap: anywhere; }
.app-page h1 .app-tag { font-size: 13px; }
.app-links { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; margin-top: 32px; }
.app-facts { display: flex; flex-wrap: wrap; gap: 24px 48px; margin: 48px 0; }
.app-facts dt { font: 400 12px/18px var(--mono); color: var(--faint); }
.app-facts dd { margin: 4px 0 0; font-size: 15px; }
.app-previews { display: grid; gap: 32px; }
.app-previews img, .app-readme img { display: block; max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: 12px; }
.app-readme { max-width: 720px; margin-top: 32px; }
.app-readme th, .app-readme td { overflow-wrap: anywhere; }
.missing { min-height: 70vh; display: grid; align-content: center; justify-items: start; gap: 24px; padding: 160px 64px; }
.missing h1 { font-size: 64px; letter-spacing: -0.04em; }
@@ -224,8 +259,12 @@ img.shot { display: block; max-width: 100%; height: auto; }
@media (max-width: 767px) {
.bar { padding: 0 16px; gap: 12px; }
.bar nav { gap: 16px; }
.bar .end { gap: 0; }
.hero, .chapters, .missing { padding-left: 20px; padding-right: 20px; }
.hero, .chapters, .missing, .showcase, .app-page { padding-left: 20px; padding-right: 20px; }
.showcase, .app-page { padding-top: 112px; }
.submit { margin: 0 20px; padding: 72px 0 96px; }
.filters { margin-top: -40px; }
.reader, .docs { padding: 0 20px; }
.chapters li { grid-template-columns: 40px minmax(0, 1fr); }
.chapters .titles { grid-column: 2; margin-top: 8px; }
@@ -233,7 +272,7 @@ img.shot { display: block; max-width: 100%; height: auto; }
.chapters .name { font-size: 22px; }
.hero-iso { width: 100%; height: 55%; }
.hero > :not(.iso) { max-width: none; }
.hero h1, .chapters h1 { font-size: 13vw; }
.hero h1, .chapters h1, .showcase h1 { font-size: 13vw; }
.feature h2, .city h2, .close h2 { font-size: 10vw; }
.feature, .city { padding: 112px 20px 48px; }
.feature-iso { min-height: 220px; }
@@ -243,13 +282,17 @@ img.shot { display: block; max-width: 100%; height: auto; }
.close { padding: 112px 20px; }
}
@media (max-width: 359px) {
.bar { padding: 0 12px; gap: 10px; }
.bar nav { gap: 14px; }
.bar .tool { width: 32px; }
@media (max-width: 413px) {
.bar .sound { display: none; }
}
@media (max-width: 359px) {
.bar { padding: 0 12px; gap: 8px; }
.bar nav { gap: 10px; }
.bar .link { font-size: 13px; }
.bar .tool { width: 30px; }
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; animation: none !important; }
}