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.

app/controllers/home.tsx

285 lines · 9.6 KB · TypeScript
/** @jsxRuntime automatic */
/** @jsxImportSource remix/ui */
import type { BuildAction } from 'remix/router'
import { listNotes } from '../data/notes.ts'
import { render } from '../ui/render.tsx'
import { AppShell } from '../ui/layout.tsx'
import { Counter } from '../ui/counter.tsx'
import { BusyDemo } from '../ui/busy-demo.tsx'
import { ErrorBanner } from '../ui/error-banner.tsx'
import { DoubleCheckDemo } from '../ui/double-check-demo.tsx'
import { NotesDemo } from '../ui/notes-demo.tsx'
import { IconDownload } from '../icons/download.tsx'
import { routes } from '../routes.ts'

export default {
	async handler(context) {
		const notes = await listNotes(context)
		const heroSrc = routes.logoFull.href()
		return render(
			context,
			<AppShell page="home" heading="Package App Kit">
				<section class="pak-hero" aria-label="Brand">
					<img
						class="pak-hero-logo"
						src={heroSrc}
						width="220"
						height="412"
						alt="Kody the koala as a race driver"
						decoding="async"
					/>
					<p class="pak-muted">Kody: Ready to race to your next app.</p>
				</section>

				<section class="pak-card pak-stack" aria-label="Colors">
					<p class="pak-eyebrow">Colors</p>
					<p class="pak-muted">Accent, ink, surface, good — plus racing stripe tokens.</p>
					<div class="pak-swatches">
						<span class="pak-swatch-label">
							<span class="pak-swatch" style="background:var(--accent)" aria-hidden="true" />
							Accent
						</span>
						<span class="pak-swatch-label">
							<span class="pak-swatch" style="background:var(--ink)" aria-hidden="true" />
							Ink
						</span>
						<span class="pak-swatch-label">
							<span class="pak-swatch" style="background:var(--surface)" aria-hidden="true" />
							Surface
						</span>
						<span class="pak-swatch-label">
							<span class="pak-swatch" style="background:var(--good)" aria-hidden="true" />
							Good
						</span>
					</div>
					<div class="pak-swatches pak-swatches-stripes" aria-label="Racing stripe tokens">
						<span class="pak-swatch-label">
							<span class="pak-swatch" style="background:var(--pak-stripe-cyan)" aria-hidden="true" />
							Cyan
						</span>
						<span class="pak-swatch-label">
							<span class="pak-swatch" style="background:var(--pak-stripe-green)" aria-hidden="true" />
							Green
						</span>
						<span class="pak-swatch-label">
							<span class="pak-swatch" style="background:var(--pak-stripe-yellow)" aria-hidden="true" />
							Yellow
						</span>
						<span class="pak-swatch-label">
							<span class="pak-swatch" style="background:var(--pak-stripe-magenta)" aria-hidden="true" />
							Magenta
						</span>
						<span class="pak-swatch-label">
							<span class="pak-swatch" style="background:var(--pak-stripe-red)" aria-hidden="true" />
							Red
						</span>
					</div>
				</section>

				<section class="pak-card pak-stack" aria-label="Spacing">
					<p class="pak-eyebrow">Spacing</p>
					<p class="pak-muted">
						Token scale <code>--pak-space-1</code>…<code>8</code> and layout primitives.
					</p>
					<div class="pak-space-rulers" aria-label="Space token rulers">
						<div class="pak-space-ruler">
							<span class="pak-space-ruler-label">space-1</span>
							<span
								class="pak-space-ruler-bar"
								style="width:var(--pak-space-1)"
								aria-hidden="true"
							/>
						</div>
						<div class="pak-space-ruler">
							<span class="pak-space-ruler-label">space-2</span>
							<span
								class="pak-space-ruler-bar"
								style="width:var(--pak-space-2)"
								aria-hidden="true"
							/>
						</div>
						<div class="pak-space-ruler">
							<span class="pak-space-ruler-label">space-3</span>
							<span
								class="pak-space-ruler-bar"
								style="width:var(--pak-space-3)"
								aria-hidden="true"
							/>
						</div>
						<div class="pak-space-ruler">
							<span class="pak-space-ruler-label">space-4</span>
							<span
								class="pak-space-ruler-bar"
								style="width:var(--pak-space-4)"
								aria-hidden="true"
							/>
						</div>
						<div class="pak-space-ruler">
							<span class="pak-space-ruler-label">space-5</span>
							<span
								class="pak-space-ruler-bar"
								style="width:var(--pak-space-5)"
								aria-hidden="true"
							/>
						</div>
						<div class="pak-space-ruler">
							<span class="pak-space-ruler-label">space-6</span>
							<span
								class="pak-space-ruler-bar"
								style="width:var(--pak-space-6)"
								aria-hidden="true"
							/>
						</div>
						<div class="pak-space-ruler">
							<span class="pak-space-ruler-label">space-7</span>
							<span
								class="pak-space-ruler-bar"
								style="width:var(--pak-space-7)"
								aria-hidden="true"
							/>
						</div>
						<div class="pak-space-ruler">
							<span class="pak-space-ruler-label">space-8</span>
							<span
								class="pak-space-ruler-bar"
								style="width:var(--pak-space-8)"
								aria-hidden="true"
							/>
						</div>
					</div>
					<div class="pak-cluster" aria-label="Layout primitives">
						<div class="pak-demo-box pak-stack-sm">
							<span class="pak-eyebrow">pak-stack</span>
							<span class="pak-demo-chip">A</span>
							<span class="pak-demo-chip">B</span>
						</div>
						<div class="pak-demo-box">
							<span class="pak-eyebrow">pak-cluster</span>
							<div class="pak-cluster pak-mt-2">
								<span class="pak-demo-chip">1</span>
								<span class="pak-demo-chip">2</span>
								<span class="pak-demo-chip">3</span>
							</div>
						</div>
						<div class="pak-demo-box pak-inset" style="background:var(--surface-2)">
							<span class="pak-eyebrow">pak-inset</span>
							<p class="pak-muted pak-mt-1" style="margin:0">
								Padded box
							</p>
						</div>
					</div>
				</section>

				<section class="pak-card pak-stack" aria-label="Buttons and chrome">
					<p class="pak-eyebrow">Buttons & chrome</p>
					<p class="pak-muted">Default, accent, danger — plus pill and eyebrow samples.</p>
					<div class="pak-cluster">
						<button type="button" class="pak-btn">
							Default
						</button>
						<button type="button" class="pak-btn pak-btn-accent">
							Accent
						</button>
						<button type="button" class="pak-btn pak-btn-danger">
							Danger
						</button>
						<span class="pak-btn pak-btn-accent" style="pointer-events:none">
							Notes
							<span class="pak-pill" aria-hidden="true">
								{notes.length}
							</span>
						</span>
					</div>
					<p class="pak-eyebrow">Eyebrow</p>
					<p class="pak-muted">Muted supporting copy stays calm beside louder chrome.</p>
				</section>

				<section class="pak-card pak-stack" aria-label="Error banner">
					<p class="pak-eyebrow">ErrorBanner</p>
					<p class="pak-muted">
						Use the alert pattern when replacing expected content; keep ephemeral failures in
						toasts.
					</p>
					<ErrorBanner
						title="Sample alert"
						message="This is a static ErrorBanner demo so the pattern stays visible on home."
						hint="Retry with a clearer action, or open Notes to try a real form error."
					/>
				</section>

				<section class="pak-card pak-stack" aria-label="Double-check">
					<p class="pak-eyebrow">Double-check</p>
					<p class="pak-muted">
						First press arms; second press confirms. Blur resets. Tip-free because the control is
						labeled.
					</p>
					<DoubleCheckDemo />
				</section>

				<section class="pak-card pak-stack" aria-label="Busy spin-delay">
					<p class="pak-eyebrow">Busy / spin-delay</p>
					<p class="pak-muted">
						Press feedback is immediate; the Loading label waits ~200ms and stays ≥400ms once
						shown.
					</p>
					<div class="pak-cluster">
						<BusyDemo label="Run demo job" />
					</div>
				</section>

				<section class="pak-card pak-stack" aria-label="Tooltip">
					<p class="pak-eyebrow">Tooltip</p>
					<p class="pak-muted">
						<code>data-tip</code> on icon-only controls — labeled Home/Notes/About stay tip-free.
					</p>
					<div class="pak-cluster">
						<button
							type="button"
							class="pak-btn pak-btn-icon"
							data-tip="Download sample (demo)"
							aria-label="Download sample (demo)"
						>
							<IconDownload size={20} />
						</button>
					</div>
				</section>

				<section class="pak-card pak-stack" aria-label="Hydrated island">
					<p class="pak-eyebrow">Hydrated island</p>
					<p class="pak-muted">
						Hydrated <code>clientEntry</code> Counter — copy this shape for interactive bits.
					</p>
					<div class="pak-cluster">
						<Counter initialCount={0} label="Taps" />
					</div>
				</section>

				<section class="pak-stack" aria-label="Notes live">
					<div class="pak-card pak-stack-sm" style="padding-bottom:var(--space-3)">
						<p class="pak-eyebrow">Notes (live)</p>
						<p class="pak-muted">
							Optimistic add/delete with package realtime (<code>notes.changed</code>);
							packageStorage stays source of truth. Live status sits above the form.
						</p>
						<a class="pak-btn" href={routes.notes.index.href()} style="align-self:flex-start">
							Open full Notes page
						</a>
					</div>
					<NotesDemo notes={notes} actionHref={routes.notes.action.href()} />
				</section>

				<section class="pak-card pak-stack" aria-label="About and version">
					<p class="pak-eyebrow">About / version</p>
					<p class="pak-muted">
						About shows local times and last update check. The header Install icon demos the A2HS
						CTA — use it there rather than duplicating mid-page.
					</p>
					<a class="pak-btn pak-btn-accent" href={routes.about.href()} style="align-self:flex-start">
						Open About
					</a>
				</section>
			</AppShell>,
			{ page: 'home', title: 'Package App Kit' },
		)
	},
} satisfies BuildAction<'ANY', typeof routes.home>