@kentcdodds/christ-paintings
Official Latter-day Saint paintings of Jesus Christ, one at a time, to show children during church, plus coloring pages of the Savior. Swipe to move. Works offline.
AGENTS.md
216 lines · 11.5 KB · Markdownchrist-paintings (agent notes)
Package app with no credentials. One Worker fetch handler renders the viewer,
coloring, and About pages with inline CSS and JS, and serves the Church's image
files from a same-origin route. Only PNG icons are static kody.app.assets
under public/. Only packageStorage use is the publish version record.
Images are NOT stored in the repo. Kody's publish check counts binary files at
about 1.8x their size (they are measured as lossily decoded UTF-8), against a
15 MiB source limit, so about 8 MB of JPEGs is the real ceiling. Instead
src/images.ts serves {appBase}/img/p/{id}.jpg and {appBase}/img/c/{id}.jpg
by fetching the curated Church URL (only ids in the lists; not an open proxy),
with caches.default when the runtime has it. Same origin keeps the service
worker cache normal (no opaque responses) and keeps the coloring canvas
untainted.
Layout
src/app.ts:kody.app.entry. Routes (mount-stripped):/(viewer),/coloring,/about,/img/p/{id}.jpg,/img/c/{id}.jpg,/api/version,/manifest.webmanifest,/icon.svg,/og.jpg,/sw.js. Canonical,og:url, andog:imageare absolute URLs built from the request's forwarded host (publicOrigin()), so a fork or a new domain needs no code change.src/og-image.ts: GENERATED base64 of/og.jpg(1200x630) bynode scripts/render-og.mjs [appUrl](headless Chrome screenshot of the real app showing "Jesus the Christ" by Del Parson, plus live text). Run it againstkody dev --app-base-path /(for examplehttp://127.0.0.1:4598/) withOG_SITE_LABEL=your.domainto change the address at the bottom. Needsplaywright-coreresolvable (node_modules is gitignored) and the Cormorant Garamond and Inter fonts. No em dashes in its text. Anything else 404, non GET/HEAD 405. Icons at{assetBasePath}/icons/*.png. All URLs come frompackageContext.src/images.ts: the image route (id to Church URL map, fetch, long cache headers),allImagePaths(),totalImageBytes().src/coloring-page.ts: coloring picker and editor (HTML, CSS, JS inString.raw, same no backticks rule) and the 16 color palette.src/client-script.ts: viewer JS as aString.rawtemplate (no backticks or template placeholders inside).src/styles.ts: viewer CSS, same rule.src/paintings.tsandsrc/coloring.ts: GENERATED bynode scripts/gen-paintings.mjsfromscripts/paintings.jsonandscripts/coloring.json. Do not edit by hand.scripts/paintings.json,scripts/coloring.json: the curated lists with each image's Media Library slug, title (and artist for paintings) exactly as the Church gives them, source page,imageUrl(the Church file the app serves),bytes,ipCode, and full Church description.scripts/make-icons.py: regeneratespublic/icons/*and.kody/icon.png(Pillow).src/version.ts,src/record-version.ts,src/update-script.ts: publish metadata, update check, and update toast, copied from the package-app-kitstarter-remixpattern (same as@kentcdodds/pocket-piano).src/about-page.ts: About page (how to use, offline progress, credits with a link to every image's Church page, terms note, version card with Check for updates).CODE_URLis the one constant for the "View this app on Kody" button (Source and fork on Kody section); forks change it.src/index.ts: default exportlistPaintings({}).test/paintings.test.ts:npm test(node --test test/). Checks ids, source and image URLs, titles, artists, sizes, that the image route only serves curated ids, the offline total, and no em dashes in src, tests, or docs.
Image rules (keep these when adding or swapping paintings)
- Only images from the Church's Gospel Media Library
(
https://www.churchofjesuschrist.org/media/image/<slug>), and only ones that depict Jesus Christ. - The page's
creditLine.ipCodemust beCODE_0orCODE_7(the page shows only the standard Gospel Media terms). SkipCODE_4(shown as Church Use Only),CODE_1(Do Not Copy or not downloadable), and anything whose description says DO NOT COPY, Church use only, or Church purposes only.CODE_4MR(Bloch museum loans),CODE_0MR,CODE_0E, andCODE_9were also skipped to stay conservative. - Title and artist are copied exactly from the Church page. When the Media
Library description has no artist, the artist may come from the Church's
Gospel Art Book page (
artistSource), and the About page says so. If neither names the artist, leave the image out. - Use the LARGE download (1280 tall or 1920 wide). When it is wider than 1600
px (or not a JPEG), use the Church's own IIIF rendition of the same image at
1600 wide as JPEG (
/imgs/<assetId>/full/1600,/0/default.jpg).largeDownloadUrlkeeps the original LARGE link. Put the URL (ending indefault.jpg) inimageUrland its size inbytes. - Coloring pages follow the same rules (
CODE_0only, exact title, must show the Savior), from the Media Library Coloring Pages collections. No artist is shown for them because the Church pages do not name one. - Add to the JSON, run
node scripts/gen-paintings.mjsandnpm test. The whole offline download (all images) is about 14.7 MB; the test keeps it under 18 MB.
Coloring
/coloringopens a picker: a CSS scroll-snap track (native swipe), one card per page, with Previous, Next, and Color this page buttons. The card shows saved progress under the line art. Swiping only happens in the picker.- The editor stacks a paint
canvasunder the line artimg(mix-blend-mode: multiply), so the lines always stay on top. The canvas is the line art size capped at 1280 px on the long side. - Fill: walls are line art pixels darker than luminance 175. A tap floods the enclosed non-wall area (ignoring earlier paint, so tapping again recolors), then grows 2 px into the lines so no white halo shows. A tap on a line snaps to the nearest open pixel within 8 px.
- Brush sizes small, medium, large (8, 18, 36 CSS px), eraser 30 px
(
destination-out). One pointer only; a second finger cancels and undoes the stroke. Undo keeps 10ImageDatasnapshots. Clear asks first. - Progress: IndexedDB
christ-paintings-coloring, storepages, key = page id, value{ blob: PNG of the paint layer, updated }, saved 700 ms after each change and on pagehide. A blank page deletes its record. - Share: composites white, paint, and line art (multiply) to a PNG; uses
navigator.share({ files })whencanShareallows, otherwise downloads it. - Opening the editor pushes a history entry so the phone's back button returns
to the picker. localStorage
christ-paintings:coloring:indexremembers the picker page. - Test hooks:
window.christColoring(state,pages,open(i),close(),fillAt(x, y),flush(),pixel(x, y),wallAt(x, y),size()).
Viewer behavior
- Three
.slidefigures hold the previous, current, and next painting; eachimgisobject-fit: containon a dark background. Slides move withtranslate3d; after a move the off-screen slide is recycled to the far side, so neighbors are always loaded. Two more in each direction are warmed withnew Image(). - Pointer events on
#viewer(touch-action: none). A horizontal drag follows the finger and commits past 18% of the width or a flick faster than 0.35 px/ms (and 24 px). Otherwise it snaps back. A tap (under 10 px, 600 ms) in the left or right 22% goes back or forward; a tap in the middle toggles the caption. Arrow keys, PageUp/PageDown, Space, Home, End work on desktop. The list wraps around. - localStorage:
christ-paintings:current(painting id),christ-paintings:caption(hiddenorshown), plus the update check keyschrist-paintings-last-update-checkandchrist-paintings-update-dismissed-<sha>. - Screen Wake Lock is requested on load, on every pointerdown, and when the page becomes visible again.
- The caption holds the only links: the palette (Coloring) and the small circle (About). The main screen has no other UI by design, except the update toast when a new version exists.
- Test hooks:
window.christPaintings.state(index,id,count,captionHidden,animating,dragging,moves,wakeLock,loaded), plusnext(),prev(),show(i).
Service worker and offline
/sw.js(scope{appBase}/): shell pages cache first with background refresh, manifest and icons network first,/api/*never cached, Kody gate responses (403, redirected,x-kody-handoff) never cached, navigations with a query string go straight to the network (session handoff).- Shell pages:
/,/coloring,/about. - All images (
{appBase}/img/) are cache first in thechrist-paintings-imagescache. The worker's script embeds the full list (ALL_IMAGES). On activate, and whenever a page postsWARM_ALL, it downloads every missing image one at a time, drops ones no longer listed, stops if the gate answers, and postsWARM_PROGRESS { done, total, running, failed }to open pages. The About page shows a progress bar and a Save everything for offline button. - Bump
VERSIONinsrc/app.tsto rotate the shell cache. Change an image id (or path) if the Church file it points to changes, since the image cache is keyed by path. - The manifest link keeps
crossorigin="use-credentials"because the app is behind Kody's session gate.
After every publish
import recordVersion from 'kody:@kentcdodds/christ-paintings/record-version'
await recordVersion({ sha: '<published_commit>', message: '<commit subject>', committedAt: '<ISO>' })Call this package's export, never the kit's.
Imports
import listPaintings from 'kody:@kentcdodds/christ-paintings'
const { count, paintings, coloringPages } = await listPaintings({})
// paintings: [{ id, title, artist, sourceUrl }], coloringPages: [{ id, title, sourceUrl }]Smoke tests
- Local:
kody dev --app-base-path /christ-paintings, then the Playwright checks (Pixel 7, CDPInput.dispatchTouchEventswipes, taps, and strokes, fills checked by reading canvas pixels, offline reload of a never-opened painting and coloring page,Page.getInstallabilityErrorsin a persistent context). - Git lane:
npm test,kody dev check, commit, push, thenpackagePublishExternalPush, thenrecordVersion. - After publish,
packageAppFetchon/,/about,/manifest.webmanifest,/sw.js,/coloring, one/img/p/...jpg, one/img/c/...jpg, and/api/versionshould be 200 with the right content type.
Image route security
The app is internet-visible, so /img/* is anonymous. imageSourceFor() only
accepts /img/(p|c)/<slug>.jpg where <slug> is an id in the generated
lists, and the upstream URL always comes from that list (never from the
request), so there is no open proxy or SSRF. Upstream redirects are not
followed. Responses carry cache-control: public, max-age=31536000, immutable
and are stored in caches.default when the runtime has it (x-image-cache: hit|miss shows which), so churchofjesuschrist.org is asked rarely. Failures are
502 with no-store.
Forks
- License: MIT for code only (
LICENSE). The images stay under the Church's terms; keep the About credits, links, and terms note. - No
kody:@...imports and no credentials, so a fork publishes as is. Onlykody:runtimeis imported. - Change
CODE_URLinsrc/about-page.ts, and re-render the og image if you want your own address on it.
Sharing
Public source (forkable, community listing) and internet-visible, served at https://christ-paintings.kentcdodds.com/ and the kody.run mount.
No em dashes in UI text or docs.