尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證A garden of calm, ad-free puzzle games for children. Static-first monorepo; Sudoku is the first game.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗A small garden of calm puzzle games for children — starting with Sudoku.
Every game here is static files. No accounts, no ads, no chat, no server, no telemetry. Puzzles are generated on the device the child is holding and never leave it. The product goal is narrow and deliberate: be a place where the answer to "can I play something?" is simply yes.
Live: puzzlebloom.taoliapp.com
Most games built for children are built to be returned to — a streak to protect, a chest to open, a friend's score to beat. Those mechanics work, which is the problem: they teach a child to keep playing rather than to finish.
A puzzle has the opposite shape. It ends, it is satisfying at the moment it is solved, and then there is nothing left to collect. Everything in this repo follows from that.
The other half is art direction. Cartoon creatures are not decoration here — they are the accessibility feature. Nine numerals are nine abstract squiggles to a five-year-old; nine faces with their own ears and horns are nine characters. The same board can be played with either.
| Numerals | Creatures | Hints that explain |
|---|---|---|
puzzlebloom/
├─ apps/web/ the site: router, pages, theme provider, styles
├─ games/sudoku/ the first game (React)
├─ packages/
│ ├─ sudoku-engine/ generator, solver, logical grading, teaching hints
│ ├─ theme-core/ theme model, built-in moods, contrast guard, share links
│ └─ ui/ locked icon set, buttons, segmented control, creature glyphs
├─ e2e/ Playwright: the seams the unit tests cannot reach
├─ docs/ spec, ADRs, screenshots
├─ wrangler.toml the Cloudflare Pages project (the live host)
└─ vercel.json the fallback host's config; Cloudflare reads
public/_headers natively
pnpm workspaces. No bundler plugins beyond React, no CSS framework, no state
library, no router dependency — the router is 90 lines in
apps/web/src/app/router.tsx.
pnpm install
pnpm dev # http://localhost:5173
pnpm lint # biome: lint and format, one tool, no plugin zoo
pnpm typecheck # tsc --noEmit across the app, the packages and the e2e specs
pnpm test # 189 engine tests + 42 palette guards
pnpm test:e2e # Playwright on Chromium and on WebKit as an iPhone
pnpm verify # all of the above, in the order CI runs them
The end-to-end suite needs both engines before its first run, and the WebKit one is not optional: the declared target is the current iPhone or iPad, and every browser on iOS is WebKit underneath.
pnpm exec playwright install chromium webkit
Node 26.12+ (.nvmrc; Node 26 is the current line and takes the LTS badge
from 24 in late 2026). The pnpm version is pinned in package.json, so a
laptop and CI use the same binary.
Playwright does not test the dev server. It builds, serves dist through
vite preview, and drives that — because the CSP, the worker chunk and the font
preloads only exist after a build, and those are half of what it is checking.
The engine is the only thing worth trusting. packages/sudoku-engine is a
port of HeyPuter/everything's Sudoku
(MIT — see THIRD_PARTY_NOTICES.md), kept verbatim
except for types. It guarantees two things the UI leans on:
techniques.js — which is why the hint button always has something true to
say, and never has to read the answer key.Generation runs in a Web Worker; Expert grids take a few hundred milliseconds and would otherwise freeze the tab.
Themes are ten colours and nothing else. Every tint, border and cell state
is a color-mix() of those ten, computed in CSS. That is why a theme a child
makes in the studio still looks designed rather than assembled — and why adding
a mood costs twelve lines.
Three of the ten exist because one colour cannot do three jobs:
| role | job | constraint |
|---|---|---|
brand | links, focus rings, the selected cell | 4.5:1 as text, so its chroma is capped |
accent | the loud colour: primary button, sun, bands | allowed to own ~26% of the screen |
sky | the wide decorative wash behind everything | no text on it, so its chroma is uncapped |
accentInk is the fourth and the one that unlocks the rest: a bright fill can
only carry a label if the label is allowed to be dark, and that pairing is
measured (9.7:1 on Meadow) rather than guessed. deriveAccentInk() computes it,
so the studio never asks a child to pick it.
The reasoning, the measurements and what is still unbuilt are in DESIGN-REVIEW-v0.2.md.
Themes are stored in localStorage and can be shared as a link: the whole
palette is packed into ?theme= as base64url, validated on arrival. No backend.
Nothing is fetched from anyone else. Baloo 2 and Nunito are self-hosted as
four woff2 variable subsets — 135 kB in total, 71 kB on first paint — with
font-display: swap and a preload for the two Latin faces. With no CDN, no font
host and no analytics, the CSP is 'self' for scripts, styles and fonts: there
is no third party left to allow. e2e/garden.spec.ts fails if a single request
leaves the origin.
Dark moods get their own cell tints. Mixing 7% white into a dark surface is
invisible, so :root[data-mood='dark'] raises the fill. See
tokens.css.
1–9 place, Backspace
clears, N notes, H hint, ⌘/Ctrl+Z undo.role="grid" of buttons.aria-live, and everything above collapses under
prefers-reduced-motion.prefers-reduced-motion.games/<id>/ with a package that exports a manifest and a component.apps/web/src/games/registry.ts.apps/web/src/App.tsx (lazy import).The landing page, the "growing" shelf and styling all follow automatically.
Keep the game's manifest out of the package index if the game is heavy — that
mistake pulls the whole game into the first paint (there is a comment about it
in registry.ts).
The build is ~400 kB of static files: any host that serves files works. It runs
on Cloudflare Pages, because the repo already speaks its dialect —
apps/web/public/_headers (the CSP, immutable caching) is read as-is — and
because a static bundle has no traffic to meter. The reasoning, with both
vendors' 2026 quotas side by side, is in
ADR-005.
Direct upload — what the live site does. The build happens locally or in CI and Pages only stores the result, so there is no build environment to configure:
pnpm deploy # build:seo + wrangler pages deploy + IndexNow ping
Git integration — connect the repo in the dashboard instead, and Pages builds on every push:
| Setting | Value |
|---|---|
| Build command | pnpm run build:seo |
| Build output directory | apps/web/dist |
| Root directory | leave at / (the pnpm filter builds the workspace) |
| Environment variable | PNPM_VERSION=12.15.0 |
Node comes from .nvmrc. The PNPM_VERSION line is not optional in this
path: the v3 build image ships pnpm 10.11.1 and does not read packageManager
from package.json, so without it the install runs on a pnpm older than the
lockfile expects.
The custom domain is one CNAME, because the zone lives at Aliyun DNS rather than at Cloudflare:
puzzlebloom CNAME puzzlebloom.pages.dev (DNS only, no proxy)
…plus the same hostname added to the Pages project, which is what issues the certificate. Any other host works the same way.
Vercel — import the repo; vercel.json already sets the build command,
output directory, cache headers and CSP. It stays supported, but
it is the fallback: free Hobby is capped at 100 GB of transfer and 1M requests a
month, and when it hits the cap the project pauses until the window resets.
Hobby is also licensed for non-commercial use only.
GitHub Pages does not fit: it serves no custom headers, so the CSP would
have to move into a <meta> tag and lose frame-ancestors.
The live site is registered with both engines that matter, and the parts that can live in the repository do:
| Google Search Console | property https://puzzlebloom.taoliapp.com/, verified by DNS, sitemap.xml submitted |
| Bing Webmaster Tools | verification meta tag in apps/web/index.html, sitemap submitted |
| IndexNow | key at apps/web/public/<key>.txt; pnpm indexnow pings Bing and friends after every deploy |
Everything else — title, description, canonical, Open Graph, JSON-LD, the sitemap, the 404 page — is generated, not typed twice:
apps/web/site.json one table: url, name, and title/description/priority per route
├── src/app/head.ts applies it at run time, on every client-side navigation
└── scripts/seo.mjs renders each route in a browser after the build and
writes dist/<route>/index.html, plus sitemap.xml,
robots.txt and 404.html
The prerender is why a crawler that does not run JavaScript still sees four
different documents rather than four copies of a shell — and why /no-such-page
can answer a real 404 instead of 200 with the app inside it. Edit
site.json, not the HTML.
Two related scripts run on demand:
node scripts/og.mjs # redraws apps/web/public/og.png (1200×630) in the site's own type
pnpm indexnow # re-ping Bing after a content change
The reasoning — prerender instead of a meta-framework, one table instead of three, a real 404 — is in ADR-006.
| Sudoku | shipped — 4 levels, daily puzzle, hints that explain, creatures mode |
| Nonogram | in the greenhouse |
| Memory Garden | in the greenhouse |
| Tangram | in the greenhouse |
MIT. The Sudoku engine is MIT, copyright the HeyPuter/everything contributors; that attribution lives in THIRD_PARTY_NOTICES.md and must stay if you fork this.
Built by a parent, for one small person and everyone like her.