← Public packages
@kentcdodds/package-app-kit
Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.
archive/fetch-demo/demo-page.ts
212 lines · 9.3 KB · TypeScriptimport themeCss from './styles.ts'
import { toastCss, toastHostHtml } from './ui.ts'
import { doubleCheckCss } from './double-check.ts'
import { installChromeHtml } from './install.ts'
import { aboutPageHtml, formatAboutPageData } from './about.ts'
import type { AppVersion } from './version.ts'
function escapeAttr(value: string) {
return value.replaceAll('&', '&').replaceAll('"', '"')
}
function escapeHtml(value: string) {
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"')
}
export type DemoHealth = {
ok: boolean
generatedAt: string
cache: string
fresh?: boolean
}
export type DemoPageInput = {
appBasePath: string
hostedUrl: string
/** Platform fingerprinted client module URL, or null on older hosts. */
clientModuleUrl?: string | null
/** Platform `/_assets` base, or null on older hosts. */
assetBasePath?: string | null
runningSha?: string
version?: AppVersion | null
codeUrl?: string | null
health?: DemoHealth | null
}
function notesSectionHtml() {
return `
<section class="pak-card pak-stack" data-notes-demo aria-labelledby="notes-heading">
<div>
<p class="pak-eyebrow">Notes</p>
<h2 id="notes-heading" class="pak-mt-1" style="margin-bottom:0;font-size:1.15rem">Remix UI island + double-check</h2>
<p class="pak-muted">Server shell + <code>remix/ui</code> <code>createRoot</code> island. Deletes arm with double-check and stay pending until settled.</p>
</div>
<div data-notes-root class="pak-stack" style="min-height:9.5rem">
<ul class="pak-list" aria-live="polite">
<li class="pak-list-item" data-skeleton aria-hidden="true"><span class="pak-skeleton" style="height:1em;width:70%"></span><span class="pak-skeleton" style="height:2.5em;width:5.5em"></span></li>
<li class="pak-list-item" data-skeleton aria-hidden="true"><span class="pak-skeleton" style="height:1em;width:55%"></span><span class="pak-skeleton" style="height:2.5em;width:5.5em"></span></li>
<li class="pak-list-item" data-skeleton aria-hidden="true"><span class="pak-skeleton" style="height:1em;width:62%"></span><span class="pak-skeleton" style="height:2.5em;width:5.5em"></span></li>
</ul>
</div>
</section>
`.trim()
}
function homeMainHtml(input: DemoPageInput) {
const appBasePath = input.appBasePath.replace(/\/$/, '')
const health = input.health
const healthLine = health
? `${health.ok ? 'Healthy' : 'Unhealthy'} · ${escapeHtml(health.generatedAt || '—')} · ${escapeHtml(health.cache || 'cachified')}`
: 'Health unavailable'
return `
<div class="pak-card pak-stack" data-first-run-splash>
<strong>Welcome</strong>
<p class="pak-muted">First-run tip — hidden automatically in standalone / installed display modes.</p>
<p class="pak-mt-2"><button class="pak-btn" type="button" data-first-run-dismiss>Got it</button></p>
</div>
${installChromeHtml({ appName: 'Package App Kit' })}
<section class="pak-card pak-stack">
<div>
<p class="pak-eyebrow">Tokens</p>
<h2 class="pak-mt-1" style="margin-bottom:0;font-size:1.15rem">Design tokens</h2>
<p class="pak-muted">CSS custom properties from <code>./styles</code> — light/dark, ≥48px taps.</p>
</div>
<div class="pak-swatches">
<span class="pak-swatch-label"><span class="pak-swatch" style="background:var(--accent)"></span>accent</span>
<span class="pak-swatch-label"><span class="pak-swatch" style="background:var(--ink)"></span>ink</span>
<span class="pak-swatch-label"><span class="pak-swatch" style="background:var(--surface)"></span>surface</span>
<span class="pak-swatch-label"><span class="pak-swatch" style="background:var(--good)"></span>good</span>
<span class="pak-swatch-label"><span class="pak-swatch" style="background:var(--warn)"></span>warn</span>
<span class="pak-swatch-label"><span class="pak-swatch" style="background:var(--bad)"></span>bad</span>
</div>
</section>
<section class="pak-card pak-stack">
<div>
<p class="pak-eyebrow">Toaster</p>
<h2 class="pak-mt-1" style="margin-bottom:0;font-size:1.15rem">Sonner-inspired toast</h2>
<p class="pak-muted">Vanilla <code>window.pakToast</code> — used for update refresh and mutations.</p>
</div>
<div class="pak-cluster"><button class="pak-btn pak-btn-accent" type="button" data-toast-demo>Show toast</button></div>
</section>
<section class="pak-card pak-stack" data-cache-health>
<div>
<p class="pak-eyebrow">Server cache</p>
<h2 class="pak-mt-1" style="margin-bottom:0;font-size:1.15rem">cachified + packageStorage</h2>
<p class="pak-muted">${healthLine}</p>
</div>
</section>
${notesSectionHtml()}
<p class="pak-muted">Version, dates, and update check live on <a href="${escapeAttr(appBasePath + '/about')}">About</a>.</p>
`.trim()
}
function renderShell(input: DemoPageInput & { page: 'home' | 'about'; title: string; main: string }) {
const appBasePath = input.appBasePath.replace(/\/$/, '')
const runningSha = input.runningSha || ''
const clientModuleUrl = input.clientModuleUrl || null
const assetBasePath = (input.assetBasePath || '').replace(/\/$/, '') || null
// Dual-path: prefer platform fingerprinted module; fall back to Worker-served interim boot.
const moduleSrc = clientModuleUrl || `${appBasePath}/client/boot.js`
const swUrl = assetBasePath ? `${assetBasePath}/sw.js` : `${appBasePath}/sw.js`
const css = `${themeCss()}\n${toastCss()}\n${doubleCheckCss()}`
const homeHref = escapeAttr(appBasePath + '/')
const aboutHref = escapeAttr(appBasePath + '/about')
const homeCurrent = input.page === 'home' ? ' aria-current="page"' : ''
const aboutCurrent = input.page === 'about' ? ' aria-current="page"' : ''
const icon192 = escapeAttr(appBasePath + '/icons/icon-192.png')
const pakConfig = JSON.stringify({
appName: 'Package App Kit',
appLabel: 'Package App Kit',
hintDismissKey: 'pak-demo-install-hint-dismissed',
versionPath: '/api/version',
routes: ['/', '/about'],
appBase: appBasePath,
assetBase: assetBasePath || undefined,
clientModuleUrl: clientModuleUrl || undefined,
swUrl,
})
const htmlAttrs = [
`lang="en"`,
`data-app-base="${escapeAttr(appBasePath)}"`,
clientModuleUrl ? `data-client-module="${escapeAttr(clientModuleUrl)}"` : '',
assetBasePath ? `data-asset-base="${escapeAttr(assetBasePath)}"` : '',
`data-sw-url="${escapeAttr(swUrl)}"`,
`data-hosted-url="${escapeAttr(input.hostedUrl)}"`,
`data-running-sha="${escapeAttr(runningSha)}"`,
`data-page="${input.page}"`,
`data-app-name="Package App Kit"`,
`data-app-label="Package App Kit"`,
`data-install-hint-key="pak-demo-install-hint-dismissed"`,
`data-version-path="/api/version"`,
`data-pak-config='${pakConfig.replaceAll("'", ''')}'`,
]
.filter(Boolean)
.join(' ')
const clientNote = clientModuleUrl
? 'Client via <code>packageContext.clientModuleUrl</code> (platform bundle).'
: 'Client via interim Worker <code>/client/boot.js</code> (older platform without <code>kody.app.client</code>).'
return `<!doctype html>
<html ${htmlAttrs}>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="light dark">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#0d9488">
<title>${escapeHtml(input.title)}</title>
<link rel="manifest" href="${escapeAttr(appBasePath + '/manifest.webmanifest')}" crossorigin="use-credentials">
<link rel="icon" type="image/png" sizes="192x192" href="${icon192}">
<link rel="apple-touch-icon" href="${icon192}">
<style>${css}</style>
</head>
<body>
<div class="pak-wrap">
<header class="pak-stack-sm">
<p class="pak-eyebrow">@kentcdodds/package-app-kit</p>
<h1 class="pak-mt-1" style="margin-bottom:0;font-size:clamp(1.35rem,2.5vw,1.7rem);letter-spacing:-0.03em">${escapeHtml(input.page === 'about' ? 'About' : 'Kit demo')}</h1>
<p class="pak-muted">${input.page === 'about' ? 'Publish metadata, package code, and a manual update check.' : `Tokens, toaster, Remix UI Notes island, server-cache health, and native install. ${clientNote} Scaffold from <code>starter/</code>, not this demo.`}</p>
<nav class="pak-nav" aria-label="App">
<a class="pak-btn" href="${homeHref}"${homeCurrent}>Home</a>
<a class="pak-btn" href="${aboutHref}"${aboutCurrent}>About</a>
</nav>
</header>
<div data-app-main>
${input.main}
</div>
</div>
${toastHostHtml()}
<script type="module" src="${escapeAttr(moduleSrc)}"></script>
</body>
</html>`
}
/**
* Kit demo home — primary UI + utilities, not an About dump.
*/
export function renderDemoPage(input: DemoPageInput) {
return renderShell({
...input,
page: 'home',
title: 'Package App Kit',
main: homeMainHtml(input),
})
}
/**
* Kit demo `/about` — sha, message, dates + relative, package code, update check.
*/
export function renderAboutPage(input: DemoPageInput) {
const runningSha = input.runningSha || ''
const about = formatAboutPageData({
version: input.version,
runningSha,
codeUrl: input.codeUrl ?? null,
})
return renderShell({
...input,
page: 'about',
title: 'About · Package App Kit',
main: aboutPageHtml(about, { title: 'This publish' }),
})
}