Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
570 changes: 154 additions & 416 deletions site/assets/home.css

Large diffs are not rendered by default.

68 changes: 26 additions & 42 deletions site/assets/home.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// site/assets/home.js — homepage behaviors. The background remains a cheap
// baked demo wall; the live Pocket Stage below the CTA is code-split and boots
// only when it approaches the viewport. Tab groups (framework tabs on the code
// card, target chips on the selector) are static HTML — JS only toggles state.
// site/assets/home.js — homepage behaviors. The hero is a machine collage on
// a fixed canvas that cover-scales to the viewport, its screens cropped live
// out of the baked demo wall. Tab groups (framework tabs on the code card,
// target chips on the selector) are static HTML — JS only toggles state.

function setupTabs(tabAttr, panelAttr) {
const tabs = [...document.querySelectorAll(`[data-${tabAttr}]`)];
Expand All @@ -24,8 +24,6 @@ function setupTabs(tabAttr, panelAttr) {
}
}

// Pause the wall when it can't be seen (scrolled away) or shouldn't move
// (prefers-reduced-motion — the CSS also hides it there).
function setupCodeCardName() {
const nameEl = document.getElementById("lp-codecard-name");
if (!nameEl) return;
Expand All @@ -43,14 +41,28 @@ function setupCodeCardName() {
}
}

function setupDemoWall() {
const video = document.querySelector(".lp-hero__wall-video");
if (!video) return;
// The hero collage: a fixed 1440x820 canvas cover-scaled to fill the hero, so
// narrow screens crop the collage's sides instead of stacking the devices.
// The screen videos pause off screen and under prefers-reduced-motion.
function setupHeroCollage() {
const hero = document.querySelector(".lp-hero");
const canvas = document.querySelector("[data-collage]");
if (!hero || !canvas) return;
const fit = () => {
const s = Math.max(hero.clientWidth / 1440, hero.clientHeight / 820);
canvas.style.transform = `translate(-50%, -50%) scale(${s})`;
};
addEventListener("resize", fit);
fit();

const videos = [...canvas.querySelectorAll("video")];
const reduced = matchMedia("(prefers-reduced-motion: reduce)");
let visible = true;
const apply = () => {
if (reduced.matches || !visible) video.pause();
else video.play().catch(() => {});
for (const video of videos) {
if (reduced.matches || !visible) video.pause();
else video.play().catch(() => {});
}
};
reduced.addEventListener?.("change", apply);
const io = new IntersectionObserver(
Expand All @@ -60,39 +72,11 @@ function setupDemoWall() {
},
{ threshold: 0.05 },
);
io.observe(video);
}

function setupPocketStage() {
const root = document.querySelector("[data-pocket-stage]");
if (!root) return;
let booted = false;
const boot = async () => {
if (booted) return;
booted = true;
try {
const { mountPocketStage } = await import("/assets/pocket-stage-web.js");
await mountPocketStage(root);
} catch (error) {
root.classList.add("has-error");
const status = root.querySelector("[data-stage-status]");
if (status) status.textContent = "Pocket Stage could not be loaded.";
console.error("Pocket Stage module failed", error);
}
};
const io = new IntersectionObserver(
(entries) => {
if (!entries.some((entry) => entry.isIntersecting)) return;
io.disconnect();
void boot();
},
{ rootMargin: "240px 0px", threshold: 0.01 },
);
io.observe(root);
io.observe(hero);
apply();
}

setupTabs("code-tab", "code-panel");
setupCodeCardName();
setupTabs("tgt-tab", "tgt-panel");
setupDemoWall();
setupPocketStage();
setupHeroCollage();
Binary file added site/assets/wall/card-im.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/wall/card-openstrike.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/wall/card-voxel.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/wall/figma-8pct.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
76 changes: 76 additions & 0 deletions site/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,11 @@ async function main() {
if (existsSync(SITE + "assets/blog/")) {
for (const f of readdirSync(SITE + "assets/blog/")) copy(SITE + "assets/blog/" + f, "assets/blog/" + f);
}
// Wall crops: hero-collage stills + use-case card backgrounds, extracted
// from the baked demo wall's known tile grid.
if (existsSync(SITE + "assets/wall/")) {
for (const f of readdirSync(SITE + "assets/wall/")) copy(SITE + "assets/wall/" + f, "assets/wall/" + f);
}

// 5. playground page
write("playground/index.html", renderPage({
Expand All @@ -585,6 +590,13 @@ async function main() {
copy(SITE + "assets/home.css", "assets/home.css");
await bundle("assets/home.js", "assets/home.js");

// 6b. /for/ use-case pages — landing-styled (home.css chrome, no hero).
// site/for/shell.html carries the shared nav + footer; each fragment
// carries the page's main content.
for (const page of FOR_PAGES) {
write(`for/${page.slug}/index.html`, renderForPage(page));
}

// 7. docs + blog (setupMarkdown installs the shared marked/shiki renderer)
const highlight = await setupMarkdown();
await buildDocs(highlight);
Expand Down Expand Up @@ -677,6 +689,70 @@ ${body}
</html>`;
}

// The /for/ pages share the homepage's bespoke chrome (home.css, no shared
// Tailwind shell): site/for/shell.html holds the nav + footer, and
// site/for/<slug>.html the main content. Linked from the use-case cards.
const FOR_PAGES = [
{
slug: "interfaces",
title: "PocketJS for user interfaces",
desc: "Components, signals and Tailwind classes in Solid, Vue Vapor or Octane become one native tree that runs on every machine in the registry.",
},
{
slug: "games",
title: "PocketJS for games",
desc: "Rust engine cores under JavaScript gameplay: OpenStrike holds a locked 60 fps on a 333 MHz PSP while the HUD stays a Solid app.",
},
{
slug: "worlds",
title: "PocketJS for 3D worlds",
desc: "Pocket3D cooks a scene once and renders it through wgpu, sceGu, GXM and OpenGL ES, up to VRM humans with spring-bone physics.",
},
{
slug: "agents",
title: "PocketJS for AI-native apps",
desc: "A virtual clock makes every frame a pure function of an input tape: sessions agents can drive, replay and diff, on a runtime small enough to host the agent itself.",
},
];

function renderForPage(page: { slug: string; title: string; desc: string }): string {
const shell = injectSiteFooterDescription(readFileSync(SITE + "for/shell.html", "utf8"));
const main = readFileSync(SITE + `for/${page.slug}.html`, "utf8");
if (!shell.includes("{{FOR_MAIN}}")) throw new Error("for/shell.html must contain {{FOR_MAIN}}");
const body = shell.replace("{{FOR_MAIN}}", main);
const url = `${SITE_URL}/for/${page.slug}/`;
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${page.title} · PocketJS</title>
<meta name="description" content="${page.desc}">
<meta name="robots" content="index,follow">
<link rel="canonical" href="${url}">
<meta property="og:title" content="${page.title} · PocketJS">
<meta property="og:description" content="${page.desc}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="PocketJS">
<meta property="og:url" content="${url}">
<meta property="og:image" content="${OG_IMAGE_URL}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="${page.title} · PocketJS">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${page.title} · PocketJS">
<meta name="twitter:description" content="${page.desc}">
<meta name="twitter:image" content="${OG_IMAGE_URL}">
<meta name="theme-color" content="#05070d">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/assets/home.css">
</head>
<body>
${body}
</body>
</html>`;
}

async function compileCss() {
const proc = Bun.spawnSync(
["bunx", "@tailwindcss/cli", "-i", SITE + "assets/tailwind.css", "-o", OUT + "assets/site.css", "--minify"],
Expand Down
32 changes: 32 additions & 0 deletions site/for/agents.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<main class="lp-formain">
<div class="lp-container">
<div class="lp-for__head">
<span class="lp-kicker">PocketJS for AI-native apps</span>
<h1>A runtime agents can drive, replay and diff.</h1>
<p class="lp-for__lead">A virtual clock makes every frame a pure function of an input tape. Sessions are data: an agent can run one, rewind it, and compare the pixels byte for byte.</p>
</div>
<div class="lp-proof-grid">
<div class="lp-proof lp-reveal">
<h3>Sessions are data</h3>
<p>Input tapes record every session. Time-travel DevTools step to any frame and inspect the native tree, over USB to real hardware. <a href="/blog/time-travel-devtools/">Time-travel debugging &rarr;</a></p>
</div>
<div class="lp-proof lp-reveal">
<h3>Tests that cannot flake</h3>
<p>Goldens compare framebuffers byte for byte, and the same tape produces the same pixels on every run, on every machine. <a href="/blog/ui-runtime-that-cant-flake/">The UI runtime that can&rsquo;t flake &rarr;</a></p>
</div>
<div class="lp-proof lp-reveal">
<h3>Small enough to host the agent</h3>
<p>Pocket Pi runs the unmodified pi coding agent in QuickJS with no Node underneath: the full harness on a desktop, a 304&nbsp;KB core profile on an ESP32&#8209;P4 board with a PocketJS touch UI. <a href="/blog/pocket-pi-on-esp32-p4/">Just enough Node &rarr;</a></p>
</div>
</div>
<figure class="lp-for__shot lp-reveal">
<div class="lp-for__term"><span class="lp-t-p">pi&nbsp;&gt;</span> fix the failing build<br /><span class="lp-t-d">plan</span>&nbsp; read Makefile, patch CFLAGS<br /><span class="lp-t-d">tool</span>&nbsp; fs.write src/config.h ok<br /><span class="lp-t-d">tool</span>&nbsp; proc.run make ... exit 0<br /><span class="lp-t-p">pi&nbsp;&gt;</span> done. 2 files changed_</div>
</figure>
<p class="lp-for__shotcap">Pocket Pi at work: the pi agent planning, editing and rebuilding, with no Node underneath.</p>
<div class="lp-for__links">
<a class="lp-btn lp-btn--primary" href="/docs/getting-started/">Get started</a>
<a class="lp-btn lp-btn--ghost" href="/docs/devtools/">DevTools guide</a>
<a class="lp-btn lp-btn--ghost" href="https://github.com/pocket-stack/pocket-pi" target="_blank" rel="noreferrer">Pocket Pi source</a>
</div>
</div>
</main>
32 changes: 32 additions & 0 deletions site/for/games.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<main class="lp-formain">
<div class="lp-container">
<div class="lp-for__head">
<span class="lp-kicker">PocketJS for games</span>
<h1>Native where it must be fast. JavaScript where you iterate.</h1>
<p class="lp-for__lead">OpenStrike runs the classic maps with bots, lightmaps and recoil at a locked 60&nbsp;fps on a 333&nbsp;MHz PSP. The engine parts are Rust; the gameplay, HUD and menus stay JavaScript you can edit and rerun.</p>
</div>
<div class="lp-proof-grid">
<div class="lp-proof lp-reveal">
<h3>The split that ships</h3>
<p>Pocket3D carries BSP worlds, visibility and a painter-sorted pipeline in the core. The HUD is a Solid app on the same native tree as every other PocketJS interface. <a href="/blog/shipping-openstrike/">OpenStrike, shipped &rarr;</a></p>
</div>
<div class="lp-proof lp-reveal">
<h3>A whole game in the guest</h3>
<p>Pocket Voxel keeps world state, battles, scripts and saves in TypeScript inside QuickJS, while per-machine Rust renderers draw the diorama at a locked 30&nbsp;fps. <a href="/blog/pocket-voxel/">A creature-RPG from first principles &rarr;</a></p>
</div>
<div class="lp-proof lp-reveal">
<h3>Matches replay themselves</h3>
<p>Input tapes make every run reproducible: the same tape gives the same frames, so a whole match replays frame for frame while you debug it.</p>
</div>
</div>
<figure class="lp-for__shot lp-reveal">
<img src="/assets/wall/card-openstrike.jpg" alt="OpenStrike gameplay: sunlit BSP geometry with the HUD showing 100 HP and 30/90 ammo." loading="lazy" />
</figure>
<p class="lp-for__shotcap">OpenStrike on de_dust2: a locked 60 fps with bots on real PSP hardware.</p>
<div class="lp-for__links">
<a class="lp-btn lp-btn--primary" href="/docs/getting-started/">Get started</a>
<a class="lp-btn lp-btn--ghost" href="https://github.com/pocket-stack/open-strike" target="_blank" rel="noreferrer">OpenStrike source</a>
<a class="lp-btn lp-btn--ghost" href="https://github.com/pocket-stack/pocket-voxel" target="_blank" rel="noreferrer">Pocket Voxel source</a>
</div>
</div>
</main>
36 changes: 36 additions & 0 deletions site/for/interfaces.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<main class="lp-formain">
<div class="lp-container">
<div class="lp-for__head">
<span class="lp-kicker">PocketJS for user interfaces</span>
<h1>Write the interface once. Render it native everywhere.</h1>
<p class="lp-for__lead">Components, signals and Tailwind classes in Solid, Vue Vapor or Octane become one native tree that runs on every machine in the registry, from a 2004 PSP to a transparent macOS widget.</p>
</div>
<div class="lp-proof-grid">
<div class="lp-proof lp-reveal">
<h3>Three frameworks, one tree</h3>
<p>Solid signals, Vue refs and Octane hooks drive the same host components. Pick per project, or mix apps on the same deck; the runtime renders one tree either way. <a href="/docs/frameworks/">How the three coexist &rarr;</a></p>
</div>
<div class="lp-proof lp-reveal">
<h3>Layout and style, compiled</h3>
<p>taffy runs flexbox in the Rust core, so a familiar <span class="lp-mono">flex-col gap-4</span> lands on identical pixels on every target. Class literals compile to compact numeric records; no CSS parser ships to the device.</p>
</div>
<div class="lp-proof lp-reveal">
<h3>Input that feels right</h3>
<p>D-pad focus, real touch with gesture physics, kinetic scroll and tap-to-press, a virtual cursor for pointerless machines, and a system keyboard any app summons with one call.</p>
</div>
<div class="lp-proof lp-reveal">
<h3>Motion, baked</h3>
<p><span class="lp-mono">animate-*</span> classes compile to keyframe timelines the core plays back on the device&rsquo;s own budget, 3D transforms and arcs included.</p>
</div>
</div>
<figure class="lp-for__shot lp-reveal">
<img src="/assets/wall/card-im.jpg" alt="Pocket Talk, the IM demo, showing a message thread on a PSP-shaped screen." loading="lazy" />
</figure>
<p class="lp-for__shotcap">Pocket Talk, the IM demo: a VirtualList thread, the system keyboard and kinetic scroll on a PSP.</p>
<div class="lp-for__links">
<a class="lp-btn lp-btn--primary" href="/docs/getting-started/">Get started</a>
<a class="lp-btn lp-btn--ghost" href="/playground/">Open the playground</a>
<a class="lp-btn lp-btn--ghost" href="/docs/components/">Component reference</a>
</div>
</div>
</main>
Loading