Skip to content

Kody is live

Watch the launch video — what Kody is, and why it exists.

← 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 · TypeScript
import 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('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;')
}

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("'", '&#39;')}'`,
	]
		.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' }),
	})
}