A monorepo of tiny static web apps. Each lives in its own package, builds to plain HTML/CSS/JS with Vite, and shows up automatically in a gallery that's deployed to GitHub Pages.
🔗 Live gallery: https://kumavis.github.io/vibe-dump/
vibe-dump/
├─ packages/
│ ├─ origami-crane/ # each package is an independent static app (Vite + three)
│ └─ mirror-field/ # self-contained Three.js scene (no build step needed)
├─ dist/ # the assembled gallery — BUILT BY HAND AND COMMITTED
├─ scripts/
│ ├─ build-gallery.mjs # collects the apps + emits the grid index (no browser)
│ ├─ thumbnails.mjs # shoots packages/<name>/thumbnail.jpg (run by hand)
│ ├─ verify.mjs # checks dist/ matches packages/ — what CI runs
│ ├─ apps.mjs # package discovery + metadata shared by the above
│ ├─ static-server.mjs # dependency-free static server (preview + screenshots)
│ ├─ serve.mjs
│ └─ clean.mjs
├─ vite.config.shared.js
└─ .github/workflows/ # ci.yml (verify) · deploy.yml (publish dist/)
npm run build:appsbuilds every package (npm workspaces) intopackages/<name>/dist.npm run build:gallerycopies each app intodist/<name>/along with its committedthumbnail.jpg, and writes a gallerydist/index.html— a tight grid of thumbnail + title + description, each card linking to the app. Cards are ordered newest vibe first and carry tags (game·simulation·tool·art·kids·tutorial), awip/donestatus and the model that built them. The grid filters on tags and status — chips are OR within a group and AND across them, and the choice lives in the URL, so/vibe-dump/#art,wipis a shareable filtered view.- You commit
dist/. The workflow publishes it verbatim.
Two things are generated by hand and committed rather than rebuilt on every deploy, because both got more expensive with every vibe added:
dist/— the whole assembled gallery. CI publishes the committed copy, so a deploy is a file copy with no Node, no npm and no build.packages/<name>/thumbnail.jpg— captured with Playwright bynpm run thumbnails. No build ever launches a browser, and a ca 9D62 rd only changes when someone deliberately re-shoots it.
Vite output is content-hashed and deterministic, so rebuilding an app you didn't
touch produces byte-identical files — a PR's dist/ diff only covers what
actually changed.
Apps use base: './' (see vite.config.shared.js) so they work under any
sub-path — locally, in a file:// preview, or at /vibe-dump/<app>/ on Pages.
npm install # install Vite + Playwright (run once)
npm run dev -w @vibe-dump/<slug> # hot-reload a single app while building it
npm run build # build all apps + gallery into dist/ (no browser needed)
npm run preview # serve dist/ locally at http://127.0.0.1:4173
npm run verify # check dist/ is in step with packages/ (what CI runs)
npm run clean # delete dist/ and packages/*/dist — rebuild to restore
# thumbnails — only when adding a vibe or deliberately re-shooting one:
npx playwright install chromium # one-time: fetch the screenshot browser
npm run thumbnails # shoot any app with no thumbnail.jpg yet
npm run thumbnails -- <slug>... # re-shoot specific apps
npm run thumbnails -- --all # re-shoot every app
npm run thumbnails -- --check # list apps missing one (exit 1)Pick a slug (kebab-case, e.g. particle-pond). It becomes both the directory
name and the URL path (/vibe-dump/<slug>/).
-
packages/<slug>/package.json— thegalleryfield is the card, so write a real title and a one-line pitch:{ "name": "@vibe-dump/<slug>", "version": "0.0.0", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build" }, "gallery": { "title": "Particle Pond", "description": "One-line pitch shown on the gallery card.", "tags": ["art"], "status": "wip", "models": ["Opus 5"], "thinking": "high" } }field values tagsone or more of game,simulation,tool,art,kids,tutorialstatusdone, orwipfor a badge on the cardmodelswhich model(s) built it, e.g. ["Opus 5"]thinkinglow/medium/high/extra/max/ultracode, orunknownThe vocabularies live in
scripts/apps.mjs, andnpm run verifyfails on a value outside them — a tag with no chip would drop the app out of every filtered view. -
packages/<slug>/vite.config.js— always re-export the shared config (it setsbase: './'so the app works under the Pages sub-path):export { default } from '../../vite.config.shared.js'
-
packages/<slug>/index.htmlplus whatevermain.js/style.cssit needs. Reference assets with relative paths (./main.js) — never absolute (/main.js). -
npm installonce, so npm links the new workspace.
Then go round the iterate loop below until it's what you want.
npm run dev -w @vibe-dump/<slug> # hot reload while you workThe dev server is the fast loop. When the app is close, check it the way the gallery will see it — built, not hot-reloaded:
npm run build && npm run preview # then open http://127.0.0.1:4173The card image is the app's whole pitch on the grid, and it isn't generated for
you — a new vibe has no card until you shoot one, and npm run verify fails
without it.
npm run build # thumbnails are shot against dist/, not dev
npm run thumbnails # only apps that don't have one yetLook at the result (packages/<slug>/thumbnail.jpg) before moving on. If it
came out blank, mistimed, or buried under the app's own chrome, tune it from the
gallery.thumbnail field rather than accepting it:
"thumbnail": { "waitFor": "!#loading", "click": "#close-panel", "settle": 2000 }waitFor— a selector to wait for before shooting; prefix!to wait for it to leave (howspace-goblinwaits out its 20-second boot).click— a control to press once the app is ready, for an app whose opening chrome hides its own subject (howrule-explorercollapses its detail rail andabyssal-*dismiss their intro modals).settle— milliseconds to wait before the shot. Defaults to 1200.
Re-shoot with npm run thumbnails -- <slug> until it looks right. See
CLAUDE.md for the full list of thumbnail gotchas.
Because dist/ is committed, the last step before pushing is always the same:
npm run build # rebuild apps + gallery
npm run verify # same check CI runs
git add packages/<slug> distCommit packages/<slug>/thumbnail.jpg along with the source — it's an input to
the build, not output of it. Keep package-lock.json changes in their own
commit, separate from anything touching app code.
CI on the PR runs npm run verify and nothing else: it builds nothing, installs
nothing, and launches no browser. It only catches the one mistake this model
makes possible — changing an app and forgetting to rebuild dist/ before
committing. If it fails, the message names the app and the fix is always
npm run build.
.github/workflows/deploy.yml publishes the committed dist/ to GitHub Pages
on every push to main, or manually via Actions → Deploy gallery to GitHub
Pages → Run workflow. It runs no build — whatever is in dist/ on main is
what goes live.
First-time setup: in Settings → Pages, set Source to GitHub Actions.