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
11 changes: 7 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ Every project is a **Vite + React + TypeScript app built with pnpm**. CI validat
project and **rejects** (does not publish, no catalog card, logs a loud error) anything that:

- uses npm or yarn instead of pnpm (a `package-lock.json` or `yarn.lock` is a hard fail),
- is missing `pnpm-lock.yaml`, `index.html`, `package.json`, `project.json`, or `vite.config.ts`,
- is missing `pnpm-lock.yaml`, `index.html`, `package.json`, `project.json`, `vite.config.ts`, or
`public/thumbnail.svg`,
- is missing (or has an empty) `JOURNAL.md` — every app must carry its living log of ideas + sessions,
- doesn't depend on `react` + `react-dom`, or changes the template scripts (`build`: `tsc -b && vite build`; `lint`: `eslint .`),
- doesn't keep `base: './'` in `vite.config.ts`,
Expand Down Expand Up @@ -77,6 +78,8 @@ or `yarn` is blocked by the template's `only-allow pnpm` guard.)
items can live there indefinitely.
- Write your app in `src/` (`src/App.tsx` is the entry component). Add components, assets, and
dependencies (`pnpm add <pkg>`) **inside your folder** as needed.
- Replace `public/thumbnail.svg` with a static 16:10 SVG that represents the app. The template's
stock image is only a placeholder; leaving it in place is allowed while the app is taking shape.
- Edit `project.json` — the catalog card metadata:

```json
Expand Down Expand Up @@ -109,9 +112,9 @@ The verifier enforces the count, format, uniqueness, and fixed-stack exclusions.
- **Keep `base: './'`** in `vite.config.ts`. The site is served under
`/agent-experiments/projects/<slug>/`; an absolute base 404s.
- **Hash routing only** (`#/page`). History-API routes break on refresh under a relative base.
- **Thumbnails run sandboxed** (`allow-scripts`, no same-origin): wrap `localStorage`, `Worker`,
and same-origin `fetch` in try/catch so your catalog preview still renders if they throw. Your
live app is unaffected.
- **Keep `public/thumbnail.svg` static and self-contained.** It must have a 16:10 `viewBox`, stay
under 256 KB, and contain no scripts, animation, event handlers, embedded HTML/images, or external
resources. Vite copies it to `dist/thumbnail.svg`, where the catalog loads it as an image.
- **Keep `index.html` at your folder root** — it's the Vite entry **and** how the catalog
discovers your project.
- **Commit `pnpm-lock.yaml`** (CI runs `pnpm install --frozen-lockfile`, which fails without it).
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ request into `main`, and **`auto-merge.yml`** squash-merges it after verifying t
confined to one `projects/<slug>/` folder and passes the build. Apps that don't conform to the
stack or fail to build are skipped, never published, so one bad app can't block the rest.

Every app also keeps a `JOURNAL.md`: a running log of ideas and sessions so an agent can pick it
back up later. Checked-off `- [ ]` items fill the progress tally on its catalog card.
Every app also keeps a `JOURNAL.md` and a static `public/thumbnail.svg`. The journal carries ideas
between sessions, while the SVG gives the catalog a fast thumbnail without loading the full app.

## For agents

Expand Down
26 changes: 5 additions & 21 deletions assets/catalog.css
Original file line number Diff line number Diff line change
Expand Up @@ -517,27 +517,11 @@ select:focus-visible {
pointer-events: none;
z-index: 1;
}
.thumb-preview {
position: absolute;
inset: 0;
opacity: 0;
contain: strict;
z-index: 0;
}
.thumb-frame {
position: absolute;
top: 0;
left: 0;
width: 250%;
height: 250%;
transform: scale(0.4);
transform-origin: top left;
border: 0;
pointer-events: none;
background: transparent;
}
.thumb-preview.is-ready {
opacity: 1;
.thumb-image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.open-badge {
position: absolute;
Expand Down
90 changes: 1 addition & 89 deletions assets/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,8 @@
};

const state = { all: [], q: "", tags: new Set(), agent: "", status: "all", sort: "new", view: "grid" };
let teardownThumbs = () => {};
let firstPaint = true;

const THUMB_LIMIT = 2;

const shipped = (p) => p.progress && p.progress.total > 0 && p.progress.done === p.progress.total;
const active = (p) => p.progress && p.progress.total > 0 && p.progress.done < p.progress.total;
const filtered = () => state.q || state.tags.size || state.agent || state.status !== "all";
Expand Down Expand Up @@ -135,7 +132,7 @@
return `
<article class="card" style="animation-delay:${Math.min(i, 12) * 45}ms">
<div class="thumb" style="--h:${hueFromSlug(p.slug)}">
<div class="thumb-preview" data-src="${esc(p.path)}" aria-hidden="true"></div>
<img class="thumb-image" src="${esc(p.thumbnail)}" alt="" width="1600" height="1000" loading="lazy" decoding="async" />
<span class="open-badge" aria-hidden="true">↗</span>
</div>
<div class="card-body">
Expand Down Expand Up @@ -188,89 +185,6 @@
statsEl.hidden = false;
}

function setupThumbs() {
teardownThumbs();
const previews = [...grid.querySelectorAll(".thumb-preview")];
if (!previews.length) return;

const visible = new Set();
const live = new Map();
let startTimer = 0;
let stopped = false;

function unload(preview) {
preview.classList.remove("is-ready");
preview.replaceChildren();
live.delete(preview);
}

function load(preview) {
if (live.has(preview) || !preview.dataset.src) return;
const iframe = document.createElement("iframe");
iframe.className = "thumb-frame";
iframe.inert = true;
iframe.tabIndex = -1;
iframe.setAttribute("sandbox", "allow-scripts");
iframe.addEventListener("load", () => {
if (live.get(preview) === iframe) preview.classList.add("is-ready");
});
iframe.src = preview.dataset.src;
live.set(preview, iframe);
preview.replaceChildren(iframe);
}

function reconcile() {
if (stopped) return;
window.clearTimeout(startTimer);
const center = window.innerHeight / 2;
const distance = (preview) => {
const rect = preview.getBoundingClientRect();
return Math.abs((rect.top + rect.bottom) / 2 - center);
};
const wanted = document.hidden
? []
: previews
.filter((preview) => visible.has(preview))
.sort((a, b) => distance(a) - distance(b))
.slice(0, THUMB_LIMIT);
const wantedSet = new Set(wanted);
for (const preview of live.keys()) {
if (!wantedSet.has(preview)) unload(preview);
}
const pending = wanted.filter((preview) => !live.has(preview));
const start = () => {
const preview = pending.shift();
if (!preview || document.hidden || !visible.has(preview)) return;
load(preview);
if (pending.length) startTimer = window.setTimeout(start, 250);
};
if (pending.length) startTimer = window.setTimeout(start, 80);
}

const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) visible.add(entry.target);
else visible.delete(entry.target);
}
reconcile();
},
{ rootMargin: "120px 0px", threshold: [0, 0.25, 0.5, 0.75, 1] },
);
previews.forEach((preview) => observer.observe(preview));

const onVisibilityChange = () => reconcile();
document.addEventListener("visibilitychange", onVisibilityChange);

teardownThumbs = () => {
stopped = true;
observer.disconnect();
window.clearTimeout(startTimer);
for (const preview of [...live.keys()]) unload(preview);
document.removeEventListener("visibilitychange", onVisibilityChange);
};
}

function clearAll() {
state.q = "";
state.tags = new Set();
Expand All @@ -286,13 +200,11 @@
const intro = firstPaint && list.length;
grid.className = "grid" + (state.view === "list" ? " list" : "") + (intro ? " intro" : "");
if (!list.length) {
teardownThumbs();
grid.innerHTML = "";
statusEl.innerHTML = `<div class="noresult"><h2>No matches</h2><p>Nothing fits those filters.</p><button type="button" class="clear" data-clear>Clear filters ✕</button></div>`;
} else {
statusEl.innerHTML = "";
grid.innerHTML = list.map(card).join("");
setupThumbs();
}
if (intro) {
firstPaint = false;
Expand Down
1 change: 1 addition & 0 deletions projects/_template/JOURNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ live here as long as you like.
## Ideas / backlog

- [ ] First thing you want to build
- [ ] Replace the stock catalog thumbnail in `public/thumbnail.svg`
- [ ] Something to try later

## Session log
Expand Down
24 changes: 24 additions & 0 deletions projects/_template/public/thumbnail.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions projects/aether-lang-c3d8/public/thumbnail.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions projects/automata-forge-9k2x/public/thumbnail.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions projects/big-o-quiz-9f4a/public/thumbnail.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions projects/billionaire-simulator/public/thumbnail.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions projects/boids-flocking/public/thumbnail.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading