@kentcdodds/package-app-kit
Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.
README.md
202 lines · 11.9 KB · Markdown@kentcdodds/package-app-kit
Runtime design/PWA kit for Kody package apps.
Intent
Give Kent (and future @kody consumers) one lean private kit so every package app can share:
- CSS custom-property design tokens (including spacing scale
--pak-space-1…8+pak-stack/pak-cluster/pak-inset) and useful vanilla UI (including a sonner-inspired toaster) - Press feedback / optimistic UI: immediate pending on tap; busy labels use spin-delay timing (no flash)
- ErrorBanner: shared alert for validation / About / SSR failures (
app/ui/error-banner.tsx+./error-display) - Zero CLS preference: update-available is a fixed dismissible toast (not an in-flow banner); ephemeral UI stays out of document flow
- Header install icon (Lucide
download, reserved slot, no CLS) that triggers the native install prompt (beforeinstallprompt→event.prompt()), with iOS Add-to-Home-Screen only when the UA is actually iOS (fixed overlay). Tooltips for non-obvious / icon-only controls. /aboutpage with commit sha/message, commit/publish dates + relative ages, package code link, and manual update check — via the record-version pattern (not dumped on home)- Remix recipe live demo under
app/({ fetch }remount wrapper +remix/uiislands) showing tokens, Notes, install, About, and update check - Update check on visit → refresh affordance
- Double-check helpers for destructive ops (two-step + type-to-confirm; Notes uses
NotesDemo+DoubleCheckButtonwith PE + optimistic UI) packageStorageschema migrations (./storage-migrations) — stable kit subpath re-exporting@kentcdodds/package-storage-migrations(version key, orderedupsteps, isolate-memoized runner; caller always passes storage)- Optional realtime notes sync (
./realtime+./realtime-client) — WorkerbroadcastNotesChanged/handleRealtimeEvent; browserconnectPackageRealtime(see AGENTS L3) - Lucide SVG icons (ISC) — checked-in under
app/icons/viascripts/add-lucide-icon.mjs(no full Lucide runtime dep) - Client cache helpers (TanStack Query core + IndexedDB) and server cache (@epic-web/cachified +
packageStorage) - Service worker helpers with zombie protections (versioned caches, cleanup, careful
skipWaiting/clients.claim, unregister stale SWs) - Desktop + mobile friendly layout (fluid width, large tap targets, no mobile-only copy)
Starter lives in this repo at starter-remix/ (scaffolder embeds that directory into a new Artifacts package and renames placeholders). There is no separate @kentcdodds/package-app-starter package. This package’s own live demo (kody.app → app/router.ts) is Remix export-shape for the kit showcase and is not what the scaffolder copies. Archived fetch template: starter-fetch/. Archived pre-Remix kit demo: archive/fetch-demo/.
Private @kentcdodds for now; promote to @kody later.
Kit demo brand
The live kit demo uses race-driver Kody for in-app logos (bust header, full-body hero) and for the list/favicon/PWA mark (the racer bust — not the Remix R + rainbow stripes glyph). Design tokens default to Remix-dark (#0a0a0a / #121212) with cyan primary and stripe secondaries. Scaffolded apps still ship a placeholder .kody/icon — generate a distinctive mark per the After scaffolding checklist.
Private hosted apps: open via Open Package App from kody.codes. Re-open from the package page when you need a fresh session or service worker, then take Refresh on the update toast when one appears.
In-app logos: serve header/hero/favicon marks from app routes that return image/png (same pattern as iconPngResponse / /icons/…), and point markup at routes.…href(). Use public/ + _assets for CSS, sw.js, and the client bundle.
Stack note
Live kit demo is a Remix recipe under the framework-agnostic host contract (every entry is a fetch handler on the mount-stripped path):
"kody": {
"app": {
"entry": "./app/router.ts",
"client": "./app/assets/entry.ts",
"assets": "./public"
}
}Recipe boilerplate the demo/starter own: root tsconfig.json with "jsx": "react-jsx" + "jsxImportSource": "remix/ui", both per-file pragmas (/** @jsxRuntime automatic */ + /** @jsxImportSource remix/ui */) at the top of every SSR .tsx that uses JSX, remount + export default { fetch } in app/router.ts, and clientEntry('kody:app#Name', …) islands with a browser registry. No kody.app.runtime. Demo controllers import kit helpers with relative ../src/... paths (this package must not depend on itself via kody:@kentcdodds/package-app-kit).
Scaffolded apps copy starter-remix/ (same recipe) and import the kit with kody:@kentcdodds/package-app-kit/....
Library consumers still get Worker HTML helpers + optional dual-path client (packageContext.clientModuleUrl, interim ./client boot). Prefer platform remix/ui for Remix apps — do not add @remix-run/ui import maps or the fat remix meta-package as a runtime dependency.
Install / import
import themeCss from 'kody:@kentcdodds/package-app-kit/styles'
import { toastCss, toastHostHtml } from 'kody:@kentcdodds/package-app-kit/ui'
import {
createDoubleCheck,
createTypeToConfirm,
} from 'kody:@kentcdodds/package-app-kit/double-check'
import { installChromeHtml } from 'kody:@kentcdodds/package-app-kit/install'
import { formatAboutPageData, aboutPageHtml } from 'kody:@kentcdodds/package-app-kit/about'
import { kitServedManifestIcons, iconPngResponse } from 'kody:@kentcdodds/package-app-kit/icons'
import { appShellHtml } from 'kody:@kentcdodds/package-app-kit/app-shell'
import { clientBootResponse, clientModuleResponse } from 'kody:@kentcdodds/package-app-kit/client'
import recordVersion from 'kody:@kentcdodds/package-app-kit/record-version'
import checkForUpdate from 'kody:@kentcdodds/package-app-kit/update-check'
import { createPersistedQueryClient } from 'kody:@kentcdodds/package-app-kit/client-cache'
import { createServerCachified } from 'kody:@kentcdodds/package-app-kit/server-cache'
import { renderServiceWorker, buildWebManifest } from 'kody:@kentcdodds/package-app-kit/sw'
import {
runPackageStorageMigrations,
createMigrationRunner,
} from 'kody:@kentcdodds/package-app-kit/storage-migrations'
import {
createNotesRealtimeHandler,
broadcastNotesChanged,
} from 'kody:@kentcdodds/package-app-kit/realtime'
import { connectPackageRealtime } from 'kody:@kentcdodds/package-app-kit/realtime-client'Always build in-app URLs from packageContext.appBasePath + packageContext.hostedUrl.
See AGENTS.md for ADOPTION LAYERS (CSS-only → fetch-handler PWA → Remix → optional realtime L3), token/class contracts, install client requirements, and the scaffolder playbook. Fetch-handler apps can use ./app-shell (appShellHtml) instead of hand-rolling the document.
After publish
For this kit (demo About / update checks), call the kit export:
import recordVersion from 'kody:@kentcdodds/package-app-kit/record-version'
await recordVersion({
sha: '<published_commit>',
message: '<commit subject>',
committedAt: '<optional ISO>',
})Scaffolded consumer apps ship their own ./record-version that writes that app's packageStorage (see starter-remix/src/record-version.ts). After publishing a scaffolded app, call kody:@scope/app/record-version — not the kit's export.
Icons + installability
List/identity mark (packages, repos, community cards): put .kody/icon.png (also .svg / .webp / .jpg / .jpeg). New packages use .kody/icon — do not scaffold root community-icon.*. Root icon.* / community-icon.* remain permanent platform aliases. Keep PWA icons (icons/icon-192.png, public/) out of .kody/. This kit ships .kody/icon.png; the scaffolder emits .kody/icon.svg (text) because packageSave cannot write binaries.
PWA install (Chromium / Brave URL-bar): requires a manifest with name/short_name, start_url, display, and PNG icons at 192 and 512 that actually 200. This kit embeds those PNGs and serves them through ./icons (iconPngResponse / kitServedManifestIcons) so apps do not need to commit binary PWA icon files. Designed/generated icons are still welcome for polish; empty icons: [] will not be promoted.
buildWebManifest defaults to display: "standalone" and accepts fullscreen, orientation, categories, and extra manifest fields — useful when iOS Safari gestures would otherwise fight the app.
start_url / scope / id stay under /packages/<kody.id>/. Every HTML page must <link rel="manifest" crossorigin="use-credentials">. Manifest responses use cache-control: no-store. The SW precaches home, /about, /notes, icons, and the manifest, and fetches the manifest network-first without caching 403s.
Private hosted apps need Open Package App handoff from kody.codes. An installed PWA can still show a cached HTML shell while brand/icon network fetches return 403 after the session cookie expires — reopen from the package page, then Refresh on the update toast.
Starter + scaffolder
See starter-remix/STARTER.md. After create-package-app, agents follow the After scaffolding checklist in AGENTS.md (and the copy that lands in the new package's AGENTS.md): distinctive .kody/icon list mark, PWA icons/icon-192.png + icons/icon-512.png, titles/description, local record-version, Remix recipe reminders, and manifest colors matching the mark.
Mint a new private Remix package app in one execute call:
import createPackageApp from 'kody:@kentcdodds/package-app-kit/create-package-app'
export default async function main() {
return await createPackageApp({
packageName: '@kentcdodds/my-notes', // or leaf `my-notes`
title: 'My Notes',
})
}Lucide icons (checked-in SVG)
Default icon set: Lucide (repo) — ISC license, consistent 24×24 stroke icons, plain SVG. Browse names at https://lucide.dev/icons.
Add icons into the app (and starter twin) without bundling the full Lucide package:
node scripts/add-lucide-icon.mjs download trash-2 plus
# writes app/icons/{name}.svg + {name}.tsx and mirrors into starter-remix/app/icons/Use the generated Remix Handle component (<IconDownload size={20} />) next to a visible label, or alone with data-tip + aria-label when icon-only:
import { IconDownload } from '../icons/download.tsx'
import { IconTrash2 } from '../icons/trash-2.tsx'
<button class="pak-btn pak-btn-icon" type="button" data-tip="Install App" aria-label="Install App">
<IconDownload size={20} />
</button>
<button class="pak-btn pak-btn-danger" type="submit">
<span class="pak-cluster pak-gap-2"><IconTrash2 size={18} /> Delete</span>
</button>Attribution note ships as app/icons/ATTRIBUTION.md.
packageStorage migrations
Evolve durable packageStorage schemas with a version key + ordered migrations.
The kit subpath re-exports @kentcdodds/package-storage-migrations; keep importing
kody:@kentcdodds/package-app-kit/storage-migrations and always pass the caller's
storage. Run once per Worker isolate (memoized) on boot or the first request that
needs the data:
import { packageStorage } from 'kody:runtime'
import {
createMigrationRunner,
} from 'kody:@kentcdodds/package-app-kit/storage-migrations'
const ensureSchema = createMigrationRunner({
storage: packageStorage(),
versionKey: 'my-app:schema-version',
migrations: [
{
version: 1,
name: 'notes-to-document',
async up(storage) {
const legacy = await storage.get('notes-v1')
if (Array.isArray(legacy)) {
await storage.set('notes', { items: legacy })
await storage.delete?.('notes-v1')
}
},
},
],
})
// in a loader / action:
await ensureSchema()The live demo Notes storage (app/data/notes.ts) shows this pattern end-to-end.