/** * Embedded copy of `starter-tanstack/` (excluding STARTER.md and binary icons) for the Worker scaffolder. * Source of truth is the `starter-tanstack/` directory — regenerate via `node scripts/embed-starter.mjs` when it changes. * Icons: scaffolded apps use kit `iconPngResponse` (no binary copy required). */ export const TANSTACK_STARTER_FILES: Record = { ".gitignore": "node_modules/\n.kody-dev/\n.wrangler/\n*.log\n.DS_Store\n.env\n.env.*\n", ".kody/icon.svg": "\n \n\n", "AGENTS.template.md": "# __PACKAGE_NAME__ — agent notes\n\nScaffolded from `@kentcdodds/package-app-kit/starter-tanstack`. Human Intent lives in README.\n\n- **Handoff:** open private hosted apps with **Open Package App** from kody.codes; re-open from the package page when you need a fresh session or service worker.\n\n## After scaffolding\n\nAfter `create-package-app-tanstack` lands this tree (or you copy `starter-tanstack/`), work through this checklist before treating the app as product-ready:\n\n1. **List mark (TanStack default included)** — This scaffold ships the square TanStack koala mark at `.kody/icon.png` / `.kody/icon.svg` (SVG wraps the PNG for text-only `packageSave`). Keep it unless the product needs its own brand — then replace with a distinctive square mark (do not reuse the kit demo race-driver). Prefer PNG on the Artifacts git lane. This is package / repo / community list identity.\n2. **PWA + in-app chrome marks (TanStack default included)** — Favicon / header / PWA icons already default to the same TanStack koala via `src/icon-png-bytes.ts` and `/icons/icon-192.png` + `/icons/icon-512.png` routes (git lane also has matching `icons/*.png`). Replace those bytes/files only when product-branding. Serve from **app routes** and point favicon / header `` at `routes.…href()`. For a hero or alternate bust, add the same kind of route (for example `/brand/logo-full.png`). Keep list identity in `.kody/`; keep `public/` for CSS, `sw.js`, and other asset-host files.\n3. **Fill product identity** — Set `__APP_TITLE__` / title, description, and `packageName` (and related placeholders) to the real product names.\n4. **Keep local version writers** — Use this package's `src/version.ts` + `src/record-version.ts` against **this** app's `packageStorage`. Call **this** package's `./record-version` (`kody:__PACKAGE_NAME__/record-version`), not the kit's export.\n5. **Record each publish** — After every successful publish, call this package's `./record-version` with the published SHA (and message / `committedAt` when available).\n6. **TanStack recipe ([#2312](https://github.com/kody-hq/kody/pull/2312))** — Confirm root `tsconfig.json` has `\"jsx\": \"react-jsx\"` + `\"jsxImportSource\": \"react\"`; `app/router.ts` remounts the Request and `export default { fetch }`; `app/routeTree.tsx` builds `@tanstack/react-router` routes; SSR uses `react-dom/server.edge` `renderToReadableStream` in `app/ssr.tsx` (**no** TanStack Start / Vite); client `app/assets/entry.ts` calls `hydrateRoot` + `RouterProvider`.\n7. **Match manifest colors to the mark** — Set `themeColor` / `backgroundColor` in `buildWebManifest` (see `app/controllers/manifest.ts`) and the document `theme-color` meta so they harmonize with the mark.\n8. **Optional UI icons** — Add Lucide glyphs via `node scripts/add-lucide-icon.mjs ` into `app/icons/` when the UI needs icon-only or labeled controls.\n\nThen smoke with `packageAppFetch` (paths in **Smoke** below).\n\n## Kody CLI workflow\n\nThis package ships npm scripts around the Kody CLI (`@kodycodes/cli`, pinned in `devDependencies`, so run `npm install` once). Each script is a thin wrapper; the raw command is listed so agents without npm (or with only MCP) can follow along.\n\n| Step | Script | Raw command |\n| --- | --- | --- |\n| Sign in + git access (humans) | `npm run setup` | `kody login && kody auth setup-git` |\n| Pull | — | `git clone https://kody.codes/__PACKAGE_NAME__.git` then `git pull` |\n| Run locally (mock) | `npm run dev` | `kody dev --target mock` |\n| Run against real data | `npm run dev:prod` | `kody dev --target prod` |\n| Fetch one path | `npm run fetch -- /health` | `kody dev fetch /health` |\n| Run a job once | `npm run job -- ` | `kody dev job ` |\n| Send a webhook | `npm run webhook -- --body fixtures/webhook.example.json` | `kody dev webhook --body ` |\n| Emit an event | `npm run emit -- --body fixtures/event.example.json` | `kody dev emit --body ` |\n| Bundle + publish checks | `npm run check` | `kody dev check` |\n| Types | `npm run typecheck` | `tsc --noEmit` |\n| Tests | `npm test` | `node --test` (smoke tests call `kody dev fetch` on mock) |\n| Push | — | `git push` |\n| Publish | `npm run publish:kody` (add `-- ` if your CLI rejects the name) | tests, `node scripts/prepublish-check.mjs`, then `kody package publish ` |\n\nLoop: pull, edit, `npm run dev` (or `npm run fetch -- `), `npm test`, commit, `git push`, `npm run publish:kody`. Add `--dry-run` to `job` / `webhook` / `emit` to see what would run without side effects. Mock is the default target: scratch storage and fake credentials, so unpublished code never touches real data. Use `dev:prod` only when you mean to.\n\n### Signing in (explicit token lifetimes)\n\n- **Humans:** `npm run setup` (browser `kody login`, then `kody auth setup-git`, which defaults to `--lifetime long`).\n- **Agents on a task:** get a one-shot `kody_bc_…` code from MCP `cliCredentialBootstrap`, then `npx kody auth bootstrap --code --lifetime short` (1h idle / 24h max). Only use `--lifetime long` (14d idle / 3mo max) for a long-running agent the owner set up on purpose. Then `npx kody auth setup-git --lifetime short` for git access.\n- Always pass `--lifetime` explicitly. Never print, log, commit, or paste tokens. Do not call `kody api cliCredentialBootstrapRedeem` (it prints the token). `kody status` / `kody whoami` confirm sign-in without showing it.\n- Package code reads Kody **credentials** through the runtime, never from files in this repo. `scripts/prepublish-check.mjs` refuses to publish if a tracked file looks like it contains one.\n\n### Without a shell (MCP-only agents)\n\nScripts are optional. The same loop works through Kody MCP tools: edit files with `packageSave` (or repo sessions), publish with `packagePublishExternalPush`, and smoke with `packageAppFetch` (see **Smoke** below). Before publishing, re-read **After scaffolding** and confirm README/AGENTS still describe the app.\n\n\n## TanStack recipe (framework-agnostic host)\n\n```json\n\"kody\": {\n \"app\": {\n \"entry\": \"./app/router.ts\",\n \"client\": \"./app/assets/entry.ts\",\n \"assets\": \"./public\"\n }\n}\n```\n\n- **tsconfig** — root `tsconfig.json` with `\"jsx\": \"react-jsx\"` and `\"jsxImportSource\": \"react\"`.\n- **Router** `app/router.ts` — remount the Request for `appBasePath`, then `export default { fetch(request) { … } }` dispatching API routes + `renderApp` SSR.\n- **Route tree** `app/routeTree.tsx` — `@tanstack/react-router` `createRootRouteWithContext` + page routes; `basepath` matches the mount.\n- **SSR** `app/ssr.tsx` — `createMemoryHistory` + `renderToReadableStream()` (Workers-safe; **not** TanStack Start).\n- **Routes helpers** `app/routes.ts` — `createRoutes(appBasePath)` for hrefs (no `kody:runtime` in this module).\n- **Client** `app/assets/entry.ts` — `hydrateRoot(document, )` + SW register.\n- **Assets** `public/` — `styles.css`, `sw.js` (reads `__version.json`; no content hash in source).\n\n## Imports — React + TanStack Router (no Vite / Start)\n\n```ts\nimport {\n createRouter,\n createRootRouteWithContext,\n createRoute,\n createMemoryHistory,\n createBrowserHistory,\n RouterProvider,\n Link,\n} from '@tanstack/react-router'\nimport { renderToReadableStream } from 'react-dom/server.edge'\nimport { hydrateRoot } from 'react-dom/client'\nimport { packageContext, packageStorage } from 'kody:runtime'\n```\n\n**Forbid:**\n\n- TanStack Start / Vite / Pitlane tooling inside the Worker publish\n- `remix/component` JSX pragmas (this starter is standard React)\n- Importing `kody:` / `kody:@…` from the browser client graph\n\n## Kit (Worker / SSR only)\n\n```ts\nimport { formatAboutPageData } from 'kody:@kentcdodds/package-app-kit/about'\nimport { buildWebManifest } from 'kody:@kentcdodds/package-app-kit/sw'\nimport { starterManifestIcons, iconPngResponse } from '../src/icon-png-bytes.ts'\n```\n\n**Version metadata:** use this package's local `./record-version` (`src/record-version.ts` → this app's `packageStorage`). Do not re-export or call `kody:@kentcdodds/package-app-kit/record-version` from execute — that writes the kit's storage and fails provenance. About / `/api/version` import `readRecordedVersion` relatively from `../../src/record-version.ts`.\n\nBrowser islands must **not** import `kody:` / `kody:@…`.\n\n## List / identity mark\n\nPut the package list mark at `.kody/icon.png` (also `.svg` / `.webp` / `.jpg` / `.jpeg`). This scaffold includes the **TanStack koala** mark (`.kody/icon.png` + `.kody/icon.svg` base64 wrapper) — keep it unless replacing with the product's official logo (prefer PNG on the git lane). Do **not** add root `community-icon.*`. Do **not** move PWA icons (`icons/icon-192.png`, routes served by `src/icon-png-bytes.ts`) into `.kody/`.\n\n## PWA / SW\n\nRegister `${assetBasePath}/sw.js` with `{ scope: `${appBasePath}/` }`. Precache via `GET …/_assets/__version.json` → `clientModuleUrl`. Icons via starter `src/icon-png-bytes.ts` routes (192 + 512; TanStack koala default). Manifest `crossorigin=\"use-credentials\"`.\n\n\n## Optional L3 — Realtime\n\nPackage apps can add live sync with kit `./realtime` + `./realtime-client` (see kit AGENTS **L3 — Realtime**). Typical recipe: export `handleRealtimeEvent` from `app/router.ts`, broadcast after durable writes via `KodyRuntime.realtime`, connect from a browser island with `connectPackageRealtime`. Do not wire WebSocket into every scaffold by default — adopt when the product needs multi-session notify (Notes-style).\n\n## Smoke\n\n```ts\nimport { kody } from 'kody:runtime'\nexport default async function main() {\n const paths = ['/', '/notes', '/about', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/icon-512.png', '/health']\n const out = []\n for (const path of paths) {\n const res = await kody.packageAppFetch({ kody_id: '__PACKAGE_ID__', path })\n out.push({ path, status: res.status, type: res.headers?.['content-type'] || res.headers?.['Content-Type'] })\n }\n return out\n}\n```\n\n## Prefer zero content layout shift (CLS)\n\nKeep document flow stable. Update-available, toasts, and other post-hydration chrome use **fixed overlays** (see `UpdateBanner` / `.pak-update-toast`). Do not inject in-flow banners that push content down. Reserve height for async UI; hide install chrome in `display-mode: standalone`.\n\n\n## Tooltips (`data-tip`)\n\nTip when the tip adds information: icon-only install in the header, truncated SHA, ISO behind local time. Labeled nav and action buttons rely on their visible words. Color swatches use visible text labels.\n\n## Install header icon\n\nPlace `InstallCta` in the header brand row **to the right of the title**. Keep a reserved same-size slot (`pak-install-slot`) so BIP reveal does not shift layout. No large in-flow Install button.\n\n## Optimistic UI + press feedback\n\nImmediate `data-pending` / `aria-busy` on press. Use `app/ui/busy.ts` (`createBusyGate`) for spin-delay timing: delay busy labels ~200ms, keep them ≥400ms once shown, so fast ops do not flash \"Loading…\".\n\n\n## Lucide icons\n\n```bash\n# From the kit repo while editing the starter twin, or copy script into the app:\nnode scripts/add-lucide-icon.mjs download trash-2 plus\n```\n\nImport generated React SVG components from `app/icons/{name}.tsx`. Icon-only controls get `data-tip` + `aria-label`; labeled buttons keep visible text.\n\n## packageStorage migrations\n\n```ts\nimport { packageStorage } from 'kody:runtime'\nimport {\n createMigrationRunner,\n} from 'kody:@kentcdodds/package-app-kit/storage-migrations'\n\nconst ensureSchema = createMigrationRunner({\n storage: packageStorage(),\n versionKey: '__PACKAGE_ID__-schema-version',\n migrations: [/* { version, name, up } */],\n})\nawait ensureSchema()\n```\n\nSee `app/data/notes.ts` for a copy-paste example.\n", "README.template.md": "# __APP_TITLE__\n\n## Intent\n\nDesktop + mobile friendly Kody package app scaffolded from `@kentcdodds/package-app-kit` `starter-tanstack/`.\nReplace this Intent with the product goal before the first real publish.\n\n## Stack\n\n- **TanStack Router + React** under the framework-agnostic host — `app/router.ts` remounts + `export default { fetch }` (no `kody.app.runtime`)\n- Root `tsconfig.json` with `\"jsx\": \"react-jsx\"` + `\"jsxImportSource\": \"react\"` (standard React — not remix/component)\n- `@tanstack/react-router` route tree (`app/routeTree.tsx`) with Workers SSR via `react-dom/server.edge` `renderToReadableStream` (**no** TanStack Start / Vite)\n- Mount-prefixed routes (`basepath` + remount) and `createRoutes(appBasePath)` href helpers\n- Controllers + notes POST with `packageStorage()`; JSON `Accept` for client loaders\n- Browser `hydrateRoot` entry (`app/assets/entry.ts`) + `RouterProvider`\n- Static assets (`public/styles.css`, `public/sw.js` via `__version.json`)\n- Kit: design tokens CSS, record-version / About, install CTA + update banner, Notes form + double-check\n\n## List mark\n\nIdentity/list icon: `.kody/icon.png` (scaffold ships `.kody/icon.svg`). Replace with your logo. PWA icons stay under `/icons/` via the kit — not in `.kody/`.\n\n## After scaffolding\n\nSee **After scaffolding** in [`AGENTS.md`](./AGENTS.md): brand the list mark + PWA icons, fill titles/description, keep local `record-version`, confirm the TanStack recipe, and match manifest theme colors to the mark.\n\n## Develop locally\n\nYou need Node 24+ and git.\n\n```bash\ngit clone https://kody.codes/__PACKAGE_NAME__.git\ncd __PACKAGE_ID__\nnpm install\nnpm run setup # first time only: kody login + git access\nnpm run dev # local app with mock data and fake credentials\n```\n\nCommon scripts:\n\n- `npm run dev`: run the app locally on mock data (`npm run dev:prod` uses your real data)\n- `npm run fetch -- /health`: request one path without opening a browser\n- `npm run job -- `, `npm run webhook -- --body fixtures/webhook.example.json`, `npm run emit -- --body fixtures/event.example.json`: trigger jobs, webhooks, and events once\n- `npm run check`, `npm run typecheck`, `npm test`: catch problems before you ship\n- `npm run publish:kody`: run tests and the pre-publish check, then publish what you pushed\n\nShip changes with `git push`, then `npm run publish:kody`. No terminal? Ask your Kody agent to make and publish changes instead; [`AGENTS.md`](./AGENTS.md) has the full workflow.\n\n\n## After publish\n\nCall **this package's** `./record-version` (local `packageStorage`), not the kit's:\n\n```ts\nimport recordVersion from 'kody:__PACKAGE_NAME__/record-version'\nawait recordVersion({ sha: '', message: '' })\n```\n", "app/assets/entry.ts": "import { hydrateRoot } from 'react-dom/client'\nimport { createElement } from 'react'\nimport { createBrowserHistory, RouterProvider } from '@tanstack/react-router'\nimport { createAppRouter, type RouterContext } from '../routeTree.tsx'\n\nfunction readContext(): RouterContext {\n\tconst root = document.documentElement\n\treturn {\n\t\tappBasePath: root.getAttribute('data-app-base') || '',\n\t\tassetBasePath: root.getAttribute('data-asset-base') || '',\n\t\tclientModuleUrl: root.getAttribute('data-client-module'),\n\t\trunningSha: root.getAttribute('data-running-sha') || '',\n\t}\n}\n\nfunction registerServiceWorker() {\n\tif (!('serviceWorker' in navigator)) return\n\tconst root = document.documentElement\n\tconst appBase = (root.getAttribute('data-app-base') || '').replace(/\\/$/, '')\n\tconst assetBase = (root.getAttribute('data-asset-base') || '').replace(/\\/$/, '')\n\tconst swUrl =\n\t\troot.getAttribute('data-sw-url') ||\n\t\t(assetBase ? `${assetBase}/sw.js` : `${appBase}/sw.js`)\n\tif (!appBase || !swUrl) return\n\tvoid navigator.serviceWorker\n\t\t.register(swUrl, { scope: `${appBase}/`, updateViaCache: 'none' })\n\t\t.catch((error) => console.warn('SW register failed', error))\n}\n\nconst context = readContext()\nconst basepath = context.appBasePath.replace(/\\/+$/, '') || undefined\nconst history = createBrowserHistory()\nconst router = createAppRouter({ history, context, basepath })\n\nvoid router.load().then(() => {\n\thydrateRoot(document, createElement(RouterProvider, { router }))\n\tdocument.documentElement.dataset.hydrated = 'true'\n\tregisterServiceWorker()\n})\n", "app/controllers/icons.ts": "import { iconPngResponse } from '../../src/icon-png-bytes.ts'\n\nexport async function icon192() {\n\treturn iconPngResponse(192)\n}\n\nexport async function icon512() {\n\treturn iconPngResponse(512)\n}\n", "app/controllers/manifest.ts": "import { packageContext } from 'kody:runtime'\nimport { buildWebManifest } from 'kody:@kentcdodds/package-app-kit/sw'\nimport { starterManifestIcons } from '../../src/icon-png-bytes.ts'\n\nexport async function manifest() {\n\tconst appBasePath = packageContext?.appBasePath ?? ''\n\tconst body = buildWebManifest({\n\t\tappBasePath,\n\t\tname: '__APP_TITLE__',\n\t\tshortName: '__APP_TITLE__',\n\t\tdescription: 'Scaffolded from @kentcdodds/package-app-kit starter-tanstack/.',\n\t\tthemeColor: '#0a0a0a',\n\t\tbackgroundColor: '#0a0a0a',\n\t\ticons: starterManifestIcons(appBasePath),\n\t})\n\treturn new Response(JSON.stringify(body), {\n\t\theaders: {\n\t\t\t'content-type': 'application/manifest+json; charset=utf-8',\n\t\t\t'cache-control': 'no-store',\n\t\t},\n\t})\n}\n", "app/controllers/notes-action.ts": "import { addNote, deleteNote } from '../data/notes.ts'\nimport { createRoutes } from '../routes.ts'\n\nfunction wantsJson(request: Request) {\n\tconst accept = (request.headers.get('accept') || '').toLowerCase()\n\treturn accept.startsWith('application/json')\n}\n\n/**\n * Notes form POST (add / delete). Mirrors Remix starter progressive-enhancement contract.\n */\nfunction redirectTo(request: Request, location: string, status = 303) {\n\tconst absolute = new URL(location, request.url).toString()\n\treturn Response.redirect(absolute, status)\n}\n\nexport async function notesAction(request: Request, appBasePath: string) {\n\tconst form = await request.formData()\n\tconst intent = String(form.get('intent') ?? '')\n\tconst json = wantsJson(request)\n\tconst routes = createRoutes(appBasePath)\n\n\tif (intent === 'delete') {\n\t\tconst id = String(form.get('id') || '').trim()\n\t\tconst result = id ? await deleteNote(id) : { ok: false }\n\t\tif (json) return Response.json({ ok: Boolean(result?.ok), id })\n\t\treturn redirectTo(request, routes.notes.index.href())\n\t}\n\n\tconst text = String(form.get('text') || '').trim()\n\tif (!text) {\n\t\tif (json) {\n\t\t\treturn Response.json({ ok: false, error: 'A note needs some text.' }, { status: 400 })\n\t\t}\n\t\treturn redirectTo(request, routes.notes.index.href())\n\t}\n\n\tconst note = await addNote(text)\n\tif (json) return Response.json({ ok: true, note })\n\treturn redirectTo(request, routes.notes.index.href())\n}\n", "app/controllers/version.ts": "import { readRecordedVersion } from '../../src/record-version.ts'\n\nexport async function version() {\n\tconst recorded = await readRecordedVersion()\n\treturn Response.json(recorded, {\n\t\theaders: { 'cache-control': 'no-store' },\n\t})\n}\n", "app/data/notes.ts": "import { packageStorage } from 'kody:runtime'\nimport { createMigrationRunner } from 'kody:@kentcdodds/package-app-kit/storage-migrations'\n\nexport type Note = {\n\tid: string\n\ttext: string\n\tcreatedAt: string\n}\n\n/** Document shape after schema v1. */\ntype NotesDocument = {\n\titems: Array\n}\n\nconst notesKey = '__PACKAGE_ID__-notes'\nconst legacyNotesKey = '__PACKAGE_ID__-notes-v1'\nconst schemaVersionKey = '__PACKAGE_ID__-schema-version'\n\nfunction getStorage() {\n\treturn packageStorage()\n}\n\n/**\n * Demo schema bump: legacy flat array key → `{ items: Note[] }` document.\n * Runner is isolate-memoized; safe to await on every notes read/write.\n */\nconst ensureNotesSchema = (() => {\n\tlet ensure: ReturnType | null = null\n\treturn () => {\n\t\tif (!ensure) {\n\t\t\tconst storage = getStorage()\n\t\t\tensure = createMigrationRunner({\n\t\t\t\tstorage,\n\t\t\t\tversionKey: schemaVersionKey,\n\t\t\t\tmigrations: [\n\t\t\t\t\t{\n\t\t\t\t\t\tversion: 1,\n\t\t\t\t\t\tname: 'notes-array-to-document',\n\t\t\t\t\t\tasync up(s: any) {\n\t\t\t\t\t\t\tconst legacy = await s.get(legacyNotesKey)\n\t\t\t\t\t\t\tconst existing = await s.get(notesKey)\n\t\t\t\t\t\t\tif (Array.isArray(legacy)) {\n\t\t\t\t\t\t\t\tawait s.set(notesKey, { items: legacy } satisfies NotesDocument)\n\t\t\t\t\t\t\t\tawait s.delete?.(legacyNotesKey)\n\t\t\t\t\t\t\t} else if (Array.isArray(existing)) {\n\t\t\t\t\t\t\t\tawait s.set(notesKey, { items: existing } satisfies NotesDocument)\n\t\t\t\t\t\t\t} else if (existing == null) {\n\t\t\t\t\t\t\t\tawait s.set(notesKey, { items: [] } satisfies NotesDocument)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t})\n\t\t}\n\t\treturn ensure()\n\t}\n})()\n\nasync function readDocument(): Promise {\n\tawait ensureNotesSchema()\n\tconst stored = await getStorage().get(notesKey)\n\tif (stored && typeof stored === 'object' && Array.isArray((stored as NotesDocument).items)) {\n\t\treturn stored as NotesDocument\n\t}\n\treturn { items: [] }\n}\n\nexport async function listNotes(): Promise> {\n\tconst doc = await readDocument()\n\treturn doc.items\n}\n\nexport async function addNote(text: string) {\n\tconst storage = getStorage()\n\tconst doc = await readDocument()\n\tconst note: Note = {\n\t\tid: crypto.randomUUID(),\n\t\ttext,\n\t\tcreatedAt: new Date().toISOString(),\n\t}\n\tconst next: NotesDocument = { items: [note, ...doc.items] }\n\tawait storage.set(notesKey, next)\n\treturn note\n}\n\nexport async function deleteNote(id: string) {\n\tconst storage = getStorage()\n\tconst doc = await readDocument()\n\tconst items = doc.items.filter((note) => note.id !== id)\n\tawait storage.set(notesKey, { items } satisfies NotesDocument)\n\treturn { ok: items.length !== doc.items.length }\n}\n", "app/icons/ATTRIBUTION.md": "# Icon attribution\n\nIcons in this folder are from [Lucide](https://lucide.dev) ([GitHub](https://github.com/lucide-icons/lucide)).\n\n- License: **ISC** (and MIT for icons derived from Feather — see Lucide LICENSE)\n- Added via `node scripts/add-lucide-icon.mjs ` from `lucide-static@0.544.0`\n- Prefer checked-in SVG + generated TSX over bundling the full Lucide package\n\nKeep this notice when copying icons into scaffolded apps.\n", "app/icons/download.svg": "\n \n \n \n\n", "app/icons/download.tsx": "import type { SVGProps } from 'react'\n\nexport type IconDownloadProps = SVGProps & {\n\tsize?: number\n}\n\n/** Lucide icon `download` (ISC) — React SVG component. */\nexport function IconDownload({ size = 20, className, ...rest }: IconDownloadProps) {\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t)\n}\n", "app/icons/plus.svg": "\n \n \n\n", "app/icons/plus.tsx": "import type { SVGProps } from 'react'\n\nexport type IconPlusProps = SVGProps & {\n\tsize?: number\n}\n\n/** Lucide icon `plus` (ISC) — React SVG component. */\nexport function IconPlus({ size = 20, className, ...rest }: IconPlusProps) {\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t)\n}\n", "app/icons/trash-2.svg": "\n \n \n \n \n \n\n", "app/icons/trash-2.tsx": "import type { SVGProps } from 'react'\n\nexport type IconTrash2Props = SVGProps & {\n\tsize?: number\n}\n\n/** Lucide icon `trash-2` (ISC) — React SVG component. */\nexport function IconTrash2({ size = 20, className, ...rest }: IconTrash2Props) {\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t)\n}\n", "app/middleware/request-id.ts": "export const REQUEST_ID_HEADER = 'x-request-id'\n\nexport function createRequestId(): string {\n\treturn crypto.randomUUID()\n}\n\nexport function withRequestId(response: Response, requestId: string): Response {\n\tconst headers = new Headers(response.headers)\n\theaders.set(REQUEST_ID_HEADER, requestId)\n\treturn new Response(response.body, {\n\t\tstatus: response.status,\n\t\tstatusText: response.statusText,\n\t\theaders,\n\t})\n}\n", "app/pages/about.tsx": "import { getRouteApi } from '@tanstack/react-router'\nimport { AppShell } from '../ui/layout.tsx'\nimport { AboutPanel } from '../ui/about-panel.tsx'\n\nconst routeApi = getRouteApi('/about')\n\nexport function AboutPage() {\n\tconst about = routeApi.useLoaderData()\n\treturn (\n\t\t\n\t\t\t\n\t\t\n\t)\n}\n", "app/pages/home.tsx": "import { Link, getRouteApi } from '@tanstack/react-router'\nimport { AppShell } from '../ui/layout.tsx'\nimport { Counter } from '../ui/counter.tsx'\n\nconst routeApi = getRouteApi('/')\n\nexport function HomePage() {\n\tconst { noteCount } = routeApi.useLoaderData()\n\treturn (\n\t\t\n\t\t\t
\n\t\t\t\t

Colors

\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\tAccent\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\tInk\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\tSurface\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\tGood\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

Notes

\n\t\t\t\t\n\t\t\t\t\tNotes\n\t\t\t\t\t\n\t\t\t\t\t\t{noteCount}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t

Pattern example

\n\t\t\t\t

\n\t\t\t\t\tHydrated React state — copy this shape for interactive bits.\n\t\t\t\t

\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t)\n}\n", "app/pages/notes.tsx": "import { getRouteApi } from '@tanstack/react-router'\nimport { AppShell } from '../ui/layout.tsx'\nimport { NotesDemo } from '../ui/notes-demo.tsx'\nimport { createRoutes } from '../routes.ts'\nimport { useAppShell } from '../router-context.ts'\n\nconst routeApi = getRouteApi('/notes')\n\nexport function NotesPage() {\n\tconst { notes } = routeApi.useLoaderData()\n\tconst { appBasePath } = useAppShell()\n\tconst routes = createRoutes(appBasePath)\n\treturn (\n\t\t\n\t\t\t\n\t\t\n\t)\n}\n", "app/routeTree.tsx": "import {\n\tOutlet,\n\tcreateRootRouteWithContext,\n\tcreateRoute,\n\tcreateRouter,\n\tuseRouter,\n\tuseRouterState,\n} from '@tanstack/react-router'\nimport type { RouterHistory } from '@tanstack/react-router'\nimport type { AppShellContext } from './router-context.ts'\nimport { AppShellReactContext } from './router-context.ts'\nimport { Document } from './ui/document.tsx'\nimport { ErrorBanner } from './ui/error-banner.tsx'\nimport { HomePage } from './pages/home.tsx'\nimport { AboutPage } from './pages/about.tsx'\nimport { NotesPage } from './pages/notes.tsx'\nimport { toErrorMessage } from './ui/to-error-message.ts'\nimport type { Note } from './data/notes.ts'\n\nexport type RouterContext = AppShellContext\n\ntype PageStaticData = { page?: string; title?: string }\n\nconst rootRoute = createRootRouteWithContext()({\n\tcomponent: function Root() {\n\t\tconst router = useRouter()\n\t\tconst ctx = router.options.context as RouterContext\n\t\tconst leaf = useRouterState({ select: (s) => s.matches.at(-1) })\n\t\tconst staticData = (leaf?.staticData || {}) as PageStaticData\n\t\treturn (\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t)\n\t},\n\terrorComponent: function RootError({ error }) {\n\t\tconst router = useRouter()\n\t\tconst ctx = router.options.context as RouterContext\n\t\treturn (\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\n\t\t\t
\n\t\t)\n\t},\n})\n\nconst homeRoute = createRoute({\n\tgetParentRoute: () => rootRoute,\n\tpath: '/',\n\tstaticData: { page: 'home', title: '__APP_TITLE__' } satisfies PageStaticData,\n\tloader: async ({ context }): Promise<{ noteCount: number }> => {\n\t\tif (context.listNotes) {\n\t\t\tconst notes = await context.listNotes()\n\t\t\treturn { noteCount: notes.length }\n\t\t}\n\t\tconst appBase = context.appBasePath.replace(/\\/$/, '')\n\t\tconst res = await fetch(`${appBase}/notes`, {\n\t\t\theaders: { Accept: 'application/json' },\n\t\t\tcredentials: 'same-origin',\n\t\t})\n\t\tif (!res.ok) return { noteCount: 0 }\n\t\tconst data = (await res.json()) as { notes?: Array }\n\t\treturn { noteCount: Array.isArray(data.notes) ? data.notes.length : 0 }\n\t},\n\tcomponent: HomePage,\n})\n\nconst aboutRoute = createRoute({\n\tgetParentRoute: () => rootRoute,\n\tpath: '/about',\n\tstaticData: { page: 'about', title: 'About · __APP_TITLE__' } satisfies PageStaticData,\n\tloader: async ({ context }) => {\n\t\tif (context.loadAbout) return await context.loadAbout()\n\t\tconst appBase = context.appBasePath.replace(/\\/$/, '')\n\t\tconst res = await fetch(`${appBase}/api/version`, {\n\t\t\tcache: 'no-store',\n\t\t\tcredentials: 'same-origin',\n\t\t})\n\t\tconst version = res.ok\n\t\t\t? ((await res.json()) as {\n\t\t\t\t\tsha?: string\n\t\t\t\t\tshortSha?: string\n\t\t\t\t\tmessage?: string\n\t\t\t\t\tpublishedAt?: string\n\t\t\t\t\tcommittedAt?: string\n\t\t\t\t})\n\t\t\t: {}\n\t\treturn {\n\t\t\tsha: version.sha || '',\n\t\t\tshortSha: version.shortSha || '',\n\t\t\tmessage: version.message || '',\n\t\t\tpublishedAt: version.publishedAt || '',\n\t\t\tcommittedAt: version.committedAt || '',\n\t\t\tcodeUrl: null as string | null,\n\t\t\trunningSha: version.sha || '',\n\t\t}\n\t},\n\tcomponent: AboutPage,\n})\n\nconst notesRoute = createRoute({\n\tgetParentRoute: () => rootRoute,\n\tpath: '/notes',\n\tstaticData: { page: 'notes', title: 'Notes · __APP_TITLE__' } satisfies PageStaticData,\n\tloader: async ({ context }): Promise<{ notes: Array }> => {\n\t\tif (context.listNotes) {\n\t\t\treturn { notes: await context.listNotes() }\n\t\t}\n\t\tconst appBase = context.appBasePath.replace(/\\/$/, '')\n\t\tconst res = await fetch(`${appBase}/notes`, {\n\t\t\theaders: { Accept: 'application/json' },\n\t\t\tcredentials: 'same-origin',\n\t\t})\n\t\tif (!res.ok) return { notes: [] }\n\t\tconst data = (await res.json()) as { notes?: Array }\n\t\treturn { notes: Array.isArray(data.notes) ? data.notes : [] }\n\t},\n\tcomponent: NotesPage,\n})\n\nexport const routeTree = rootRoute.addChildren([homeRoute, aboutRoute, notesRoute])\n\nexport function createAppRouter(input: {\n\thistory: RouterHistory\n\tcontext: RouterContext\n\tbasepath?: string\n}) {\n\treturn createRouter({\n\t\trouteTree,\n\t\thistory: input.history,\n\t\tcontext: input.context,\n\t\tbasepath: input.basepath && input.basepath !== '/' ? input.basepath : undefined,\n\t\tdefaultPreload: 'intent',\n\t\tscrollRestoration: true,\n\t})\n}\n\ndeclare module '@tanstack/react-router' {\n\tinterface Register {\n\t\trouter: ReturnType\n\t}\n}\n", "app/router-context.ts": "import { createContext, useContext } from 'react'\nimport type { Note } from './data/notes.ts'\n\nexport type AppShellContext = {\n\tappBasePath: string\n\tassetBasePath: string\n\tclientModuleUrl: string | null\n\trunningSha: string\n\t/** Server-only: list notes from packageStorage. Undefined in the browser. */\n\tlistNotes?: () => Promise>\n\t/** Server-only: format about page data. */\n\tloadAbout?: () => Promise<{\n\t\tsha: string\n\t\tshortSha: string\n\t\tmessage: string\n\t\tpublishedAt: string\n\t\tcommittedAt: string\n\t\tcodeUrl: string | null\n\t\trunningSha: string\n\t}>\n}\n\nexport const AppShellReactContext = createContext(null)\n\nexport function useAppShell(): AppShellContext {\n\tconst value = useContext(AppShellReactContext)\n\tif (!value) {\n\t\tthrow new Error('useAppShell must be used within AppShellReactContext')\n\t}\n\treturn value\n}\n", "app/router.ts": "import { packageContext } from 'kody:runtime'\nimport { createRequestId, withRequestId } from './middleware/request-id.ts'\nimport { relativePath } from './routes.ts'\nimport { icon192, icon512 } from './controllers/icons.ts'\nimport { manifest } from './controllers/manifest.ts'\nimport { version } from './controllers/version.ts'\nimport { notesAction } from './controllers/notes-action.ts'\nimport { listNotes } from './data/notes.ts'\nimport { renderApp } from './ssr.tsx'\n\n// Host strips the mount before forwarding. TanStack routes that use basepath /\n// remounted URLs need the hosted pathname, so remount here (Remix parity).\nfunction remountRequest(request: Request) {\n\tconst appBasePath = String(packageContext?.appBasePath ?? '').replace(/\\/+$/, '')\n\tif (!appBasePath) return request\n\tconst url = new URL(request.url)\n\turl.pathname = url.pathname === '/' ? appBasePath : appBasePath + url.pathname\n\treturn new Request(url, request)\n}\n\nfunction wantsJson(request: Request) {\n\tconst accept = (request.headers.get('accept') || '').toLowerCase()\n\treturn accept.startsWith('application/json')\n}\n\nasync function dispatch(request: Request): Promise {\n\tconst appBasePath = String(packageContext?.appBasePath ?? '').replace(/\\/+$/, '')\n\tconst url = new URL(request.url)\n\tconst path = relativePath(url.pathname, appBasePath)\n\tconst method = request.method.toUpperCase()\n\n\tif (path === '/health') {\n\t\treturn Response.json({ ok: true })\n\t}\n\tif (path === '/api/version') {\n\t\treturn version()\n\t}\n\tif (path === '/manifest.webmanifest') {\n\t\treturn manifest()\n\t}\n\tif (path === '/icons/icon-192.png') {\n\t\treturn icon192()\n\t}\n\tif (path === '/icons/icon-512.png') {\n\t\treturn icon512()\n\t}\n\n\tif (path === '/notes' && method === 'POST') {\n\t\treturn notesAction(request, appBasePath)\n\t}\n\n\tif (path === '/notes' && method === 'GET' && wantsJson(request)) {\n\t\tconst notes = await listNotes()\n\t\treturn Response.json({ notes })\n\t}\n\n\tif (method === 'GET' || method === 'HEAD') {\n\t\treturn renderApp(request)\n\t}\n\n\treturn new Response('Method Not Allowed', { status: 405 })\n}\n\n// Default-export { fetch } — do not export a class/router object (Worker env is not RequestInit).\nexport default {\n\tasync fetch(request: Request) {\n\t\tconst requestId = createRequestId()\n\t\ttry {\n\t\t\tconst response = await dispatch(remountRequest(request))\n\t\t\treturn withRequestId(response, requestId)\n\t\t} catch (error) {\n\t\t\tconsole.error('package app fetch failed:', error)\n\t\t\treturn withRequestId(\n\t\t\t\tResponse.json(\n\t\t\t\t\t{ ok: false, error: error instanceof Error ? error.message : String(error) },\n\t\t\t\t\t{ status: 500 },\n\t\t\t\t),\n\t\t\t\trequestId,\n\t\t\t)\n\t\t}\n\t},\n}\n", "app/routes.ts": "/**\n * Mount-aware path helpers (pure). Pass `appBasePath` from packageContext (Worker)\n * or `document.documentElement.dataset.appBase` (browser) — never import kody:runtime here.\n */\nexport type AppRoutes = ReturnType\n\nexport function createRoutes(appBasePath = '') {\n\tconst base = String(appBasePath ?? '').replace(/\\/+$/, '')\n\tconst href = (path: string) => {\n\t\tif (!path || path === '/') return base || '/'\n\t\tconst normalized = path.startsWith('/') ? path : `/${path}`\n\t\treturn `${base}${normalized}`\n\t}\n\treturn {\n\t\thome: { path: '/', href: () => href('/') },\n\t\tabout: { path: '/about', href: () => href('/about') },\n\t\tnotes: {\n\t\t\tpath: '/notes',\n\t\t\tindex: { href: () => href('/notes') },\n\t\t\taction: { href: () => href('/notes') },\n\t\t},\n\t\tmanifest: { path: '/manifest.webmanifest', href: () => href('/manifest.webmanifest') },\n\t\ticon192: { path: '/icons/icon-192.png', href: () => href('/icons/icon-192.png') },\n\t\ticon512: { path: '/icons/icon-512.png', href: () => href('/icons/icon-512.png') },\n\t\tversionApi: { path: '/api/version', href: () => href('/api/version') },\n\t\thealth: { path: '/health', href: () => href('/health') },\n\t}\n}\n\n/** Relative path under the mount (leading slash, no trailing slash except root). */\nexport function relativePath(pathname: string, appBasePath: string): string {\n\tconst base = String(appBasePath ?? '').replace(/\\/+$/, '')\n\tlet path = pathname || '/'\n\tif (base && (path === base || path.startsWith(base + '/'))) {\n\t\tpath = path.slice(base.length) || '/'\n\t}\n\tif (path.length > 1 && path.endsWith('/')) path = path.slice(0, -1)\n\treturn path || '/'\n}\n", "app/ssr.tsx": "import { createMemoryHistory, RouterProvider } from '@tanstack/react-router'\nimport { renderToReadableStream } from 'react-dom/server.edge'\nimport { packageContext } from 'kody:runtime'\nimport { formatAboutPageData } from 'kody:@kentcdodds/package-app-kit/about'\nimport { readRecordedVersion } from '../src/record-version.ts'\nimport { listNotes } from './data/notes.ts'\nimport { createAppRouter, type RouterContext } from './routeTree.tsx'\nimport { ErrorBanner } from './ui/error-banner.tsx'\nimport { Document } from './ui/document.tsx'\nimport { toErrorMessage } from './ui/to-error-message.ts'\n\nasync function buildContext(runningSha?: string): Promise {\n\tconst version = await readRecordedVersion()\n\treturn {\n\t\tappBasePath: packageContext?.appBasePath ?? '',\n\t\tassetBasePath: packageContext?.assetBasePath ?? '',\n\t\tclientModuleUrl: packageContext?.clientModuleUrl ?? null,\n\t\trunningSha: runningSha ?? version.sha,\n\t\tlistNotes,\n\t\tasync loadAbout() {\n\t\t\tconst recorded = await readRecordedVersion()\n\t\t\tconst about = formatAboutPageData({\n\t\t\t\tversion: recorded,\n\t\t\t\trunningSha: recorded.sha,\n\t\t\t\tcodeUrl: null,\n\t\t\t})\n\t\t\treturn {\n\t\t\t\tsha: about.sha,\n\t\t\t\tshortSha: about.shortSha,\n\t\t\t\tmessage: about.message,\n\t\t\t\tpublishedAt: about.publishedAt,\n\t\t\t\tcommittedAt: about.committedAt,\n\t\t\t\tcodeUrl: about.codeUrl,\n\t\t\t\trunningSha: about.runningSha || about.sha,\n\t\t\t}\n\t\t},\n\t}\n}\n\n/**\n * SSR the matched TanStack route into a full HTML document Response.\n * Uses react-dom/server.edge renderToReadableStream (Workers-safe) — not TanStack Start / Vite.\n */\nexport async function renderApp(request: Request): Promise {\n\tconst url = new URL(request.url)\n\tconst context = await buildContext()\n\tconst basepath = context.appBasePath.replace(/\\/+$/, '') || undefined\n\tconst history = createMemoryHistory({\n\t\tinitialEntries: [url.pathname + url.search],\n\t})\n\tconst router = createAppRouter({ history, context, basepath })\n\n\ttry {\n\t\tawait router.load()\n\t\tconst stream = await renderToReadableStream(\n\t\t\t,\n\t\t\t{\n\t\t\t\tonError(error) {\n\t\t\t\t\tconsole.error('SSR render error:', error)\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\t\treturn new Response(stream, {\n\t\t\theaders: {\n\t\t\t\t'content-type': 'text/html; charset=utf-8',\n\t\t\t\t'cache-control': 'no-store',\n\t\t\t},\n\t\t})\n\t} catch (error) {\n\t\tconst message = toErrorMessage(error)\n\t\tconsole.error('SSR render failed:', error)\n\t\tconst stream = await renderToReadableStream(\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t,\n\t\t)\n\t\treturn new Response(stream, {\n\t\t\tstatus: 500,\n\t\t\theaders: {\n\t\t\t\t'content-type': 'text/html; charset=utf-8',\n\t\t\t\t'cache-control': 'no-store',\n\t\t\t},\n\t\t})\n\t}\n}\n", "app/ui/about-panel.tsx": "import { useEffect, useState } from 'react'\nimport {\n\tformatLocalDateTime,\n\treadLastUpdateCheckAt,\n\twriteLastUpdateCheckAt,\n} from './local-time.ts'\nimport { createBusyGate } from './busy.ts'\nimport { ErrorBanner } from './error-banner.tsx'\n\nexport type AboutPanelProps = {\n\tsha: string\n\tshortSha: string\n\tmessage: string\n\tpublishedAt: string\n\tcommittedAt: string\n\tcodeUrl: string | null\n\trunningSha: string\n\tappId?: string\n}\n\n/**\n * About metadata + manual update check.\n * Times render in the browser's local timezone; last-checked is stored locally.\n */\nexport function AboutPanel(props: AboutPanelProps) {\n\tconst appId = props.appId || '__PACKAGE_ID__'\n\tconst [lastCheckedAt, setLastCheckedAt] = useState(null)\n\tconst [checking, setChecking] = useState(false)\n\tconst [showBusy, setShowBusy] = useState(false)\n\tconst [status, setStatus] = useState<'idle' | 'ok' | 'update' | 'error'>('idle')\n\tconst [statusDetail, setStatusDetail] = useState('')\n\n\tuseEffect(() => {\n\t\tsetLastCheckedAt(readLastUpdateCheckAt(appId))\n\t}, [appId])\n\n\tasync function runCheck() {\n\t\tif (checking || typeof window === 'undefined') return\n\t\tsetChecking(true)\n\t\tsetStatus('idle')\n\t\tsetStatusDetail('')\n\t\tconst gate = createBusyGate({ delayMs: 200, minDurationMs: 400 })\n\t\tgate.start((phase) => setShowBusy(phase === 'busy'))\n\t\ttry {\n\t\t\tconst root = document.documentElement\n\t\t\tconst appBase = root.getAttribute('data-app-base') || ''\n\t\t\tconst runningSha =\n\t\t\t\tprops.runningSha || root.getAttribute('data-running-sha') || ''\n\t\t\tconst res = await fetch(`${appBase}/api/version`, {\n\t\t\t\tcache: 'no-store',\n\t\t\t\tcredentials: 'same-origin',\n\t\t\t})\n\t\t\tconst version = res.ok\n\t\t\t\t? ((await res.json()) as { sha?: string; shortSha?: string })\n\t\t\t\t: {}\n\t\t\tlet waiting = false\n\t\t\tif ('serviceWorker' in navigator) {\n\t\t\t\tconst regs = await navigator.serviceWorker.getRegistrations()\n\t\t\t\tfor (const reg of regs) {\n\t\t\t\t\tif (reg.waiting) waiting = true\n\t\t\t\t\tif (reg.update) await reg.update().catch(() => null)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst checked = writeLastUpdateCheckAt(new Date().toISOString(), appId)\n\t\t\tsetLastCheckedAt(checked)\n\t\t\tconst latestSha = String(version.sha || '')\n\t\t\tif (waiting || (runningSha && latestSha && runningSha !== latestSha)) {\n\t\t\t\tsetStatus('update')\n\t\t\t\tsetStatusDetail(\n\t\t\t\t\twaiting ? 'Service worker waiting' : `Latest ${version.shortSha || latestSha.slice(0, 7)}`,\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tsetStatus('ok')\n\t\t\t\tsetStatusDetail('Up to date')\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tsetStatus('error')\n\t\t\tsetStatusDetail(error instanceof Error ? error.message : String(error))\n\t\t\tsetLastCheckedAt(writeLastUpdateCheckAt(new Date().toISOString(), appId))\n\t\t} finally {\n\t\t\tsetChecking(false)\n\t\t\tawait gate.stop((phase) => setShowBusy(phase === 'busy'))\n\t\t}\n\t}\n\n\tconst {\n\t\tsha,\n\t\tshortSha,\n\t\tmessage,\n\t\tpublishedAt,\n\t\tcommittedAt,\n\t\tcodeUrl,\n\t} = props\n\tconst publishedLocal = formatLocalDateTime(publishedAt)\n\tconst committedLocal = formatLocalDateTime(committedAt)\n\tconst checkedLocal = formatLocalDateTime(lastCheckedAt)\n\n\treturn (\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
SHA
\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{shortSha || sha || '—'}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
Message
\n\t\t\t\t\t
{message || '—'}
\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
Published
\n\t\t\t\t\t
{publishedLocal}
\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
Committed
\n\t\t\t\t\t
{committedLocal}
\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
Last checked
\n\t\t\t\t\t
\n\t\t\t\t\t\t{lastCheckedAt ? checkedLocal : 'Never'}\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t{codeUrl ? (\n\t\t\t\t\t
\n\t\t\t\t\t\t
Code
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tPackage\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t) : null}\n\t\t\t
\n\t\t\t
\n\t\t\t\t void runCheck()}\n\t\t\t\t>\n\t\t\t\t\t{showBusy ? 'Checking…' : 'Check for updates'}\n\t\t\t\t\n\t\t\t\t{status === 'ok' ? Up to date : null}\n\t\t\t\t{status === 'update' ? (\n\t\t\t\t\t{statusDetail || 'Update available'}\n\t\t\t\t) : null}\n\t\t\t
\n\t\t\t{status === 'error' ? (\n\t\t\t\t\n\t\t\t) : null}\n\t\t
\n\t)\n}\n", "app/ui/busy.ts": "/**\n * Busy / pending helpers inspired by `spin-delay`:\n * - Give immediate affordance on press (so taps never feel ignored)\n * - Delay showing a \"loading\" label/spinner so fast ops do not flash\n * - Once shown, keep it visible for a minimum duration\n */\n\nexport type BusyPhase = 'idle' | 'pending' | 'busy'\n\nexport type BusyGateOptions = {\n\t/** Wait this long before flipping pending → busy (default 200ms). */\n\tdelayMs?: number\n\t/** Once busy, stay busy at least this long (default 400ms). */\n\tminDurationMs?: number\n}\n\nexport function createBusyGate(options: BusyGateOptions = {}) {\n\tconst delayMs = options.delayMs ?? 200\n\tconst minDurationMs = options.minDurationMs ?? 400\n\tlet phase: BusyPhase = 'idle'\n\tlet delayTimer: ReturnType | null = null\n\tlet busySince = 0\n\tlet generation = 0\n\n\tfunction clearDelay() {\n\t\tif (delayTimer) {\n\t\t\tclearTimeout(delayTimer)\n\t\t\tdelayTimer = null\n\t\t}\n\t}\n\n\treturn {\n\t\tget phase() {\n\t\t\treturn phase\n\t\t},\n\t\t/** Call synchronously on pointer/click — marks pending immediately. */\n\t\tstart(onChange?: (phase: BusyPhase) => void) {\n\t\t\tgeneration += 1\n\t\t\tconst gen = generation\n\t\t\tclearDelay()\n\t\t\tphase = 'pending'\n\t\t\tonChange?.(phase)\n\t\t\tdelayTimer = setTimeout(() => {\n\t\t\t\tif (gen !== generation) return\n\t\t\t\tphase = 'busy'\n\t\t\t\tbusySince = Date.now()\n\t\t\t\tonChange?.(phase)\n\t\t\t}, delayMs)\n\t\t},\n\t\t/** Resolve when the async work finishes; respects min busy duration. */\n\t\tasync stop(onChange?: (phase: BusyPhase) => void) {\n\t\t\tconst gen = generation\n\t\t\tclearDelay()\n\t\t\tif (phase === 'busy') {\n\t\t\t\tconst elapsed = Date.now() - busySince\n\t\t\t\tconst wait = Math.max(0, minDurationMs - elapsed)\n\t\t\t\tif (wait > 0) await new Promise((r) => setTimeout(r, wait))\n\t\t\t}\n\t\t\tif (gen !== generation) return\n\t\t\tphase = 'idle'\n\t\t\tonChange?.(phase)\n\t\t},\n\t\t/** True while the user should see a loading treatment (after delay). */\n\t\tget showBusy() {\n\t\t\treturn phase === 'busy'\n\t\t},\n\t\t/** True from the moment of press until stop — use for disable / aria-busy. */\n\t\tget isActive() {\n\t\t\treturn phase !== 'idle'\n\t\t},\n\t}\n}\n", "app/ui/counter.tsx": "import { useState } from 'react'\n\n/** Interactive counter — demonstrates client React state after hydration. */\nexport function Counter({ initialCount, label }: { initialCount: number; label: string }) {\n\tconst [count, setCount] = useState(initialCount)\n\treturn (\n\t\t setCount((value) => value + 1)}\n\t\t>\n\t\t\t{label}: {count}\n\t\t\n\t)\n}\n", "app/ui/document.tsx": "import type { ReactNode } from 'react'\nimport { createRoutes } from '../routes.ts'\n\nexport type DocumentProps = {\n\ttitle: string\n\tappBasePath: string\n\tassetBasePath: string\n\tclientModuleUrl: string | null\n\trunningSha?: string\n\tpage?: string\n\tchildren?: ReactNode\n}\n\nexport function Document({\n\ttitle,\n\tappBasePath,\n\tassetBasePath,\n\tclientModuleUrl,\n\trunningSha,\n\tpage,\n\tchildren,\n}: DocumentProps) {\n\tconst appBase = appBasePath.replace(/\\/$/, '')\n\tconst assetBase = assetBasePath.replace(/\\/$/, '')\n\tconst routes = createRoutes(appBase)\n\tconst icon192 = `${routes.icon192.href()}?v=4`\n\tconst manifest = routes.manifest.href()\n\tconst swUrl = assetBase ? `${assetBase}/sw.js` : `${appBase}/sw.js`\n\tconst pakConfig = JSON.stringify({\n\t\tappName: '__APP_TITLE__',\n\t\tappLabel: '__APP_TITLE__',\n\t\thintDismissKey: '__PACKAGE_ID__-install-hint-dismissed',\n\t\tversionPath: '/api/version',\n\t\tappBase,\n\t\tassetBase: assetBase || undefined,\n\t\tclientModuleUrl: clientModuleUrl || undefined,\n\t\tswUrl,\n\t})\n\n\treturn (\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t{title}\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t{children}\n\t\t\t\t
\n\t\t\t\t{clientModuleUrl ?