← Public packages
@kody/mailroom
Offline-first PWA for your Kody mailbox: sent and received threads, copy-prompt for agents, no send.
src/ui/page.ts
94 lines · 4.1 KB · TypeScriptimport type { AppVersion } from '../version-shape.ts'
import { clientScript } from './client-script.ts'
import { styles } from './styles.ts'
export function renderAppPage(input: {
appBasePath: string
hostedUrl: string
version?: AppVersion
}): string {
const base = input.appBasePath.endsWith('/')
? input.appBasePath
: `${input.appBasePath}/`
const version = input.version
return `<!doctype html>
<html lang="en" data-base="${escapeAttr(base)}" data-running-sha="${escapeAttr(version?.sha || '')}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#12813f" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#16191c" media="(prefers-color-scheme: dark)">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Mailroom">
<meta name="description" content="Your Kody mailbox: received and sent threads, offline, no send.">
<link rel="canonical" href="${escapeAttr(input.hostedUrl)}/">
<title>Mailroom</title>
<link rel="manifest" href="${escapeAttr(base)}manifest.webmanifest">
<link rel="icon" href="${escapeAttr(base)}favicon.png" type="image/png" sizes="64x64">
<link rel="apple-touch-icon" href="${escapeAttr(base)}apple-touch-icon.png" sizes="180x180">
<style>${styles}</style>
</head>
<body>
<main class="app" id="app">
<header class="top">
<div class="brand">
<img class="mark" src="${escapeAttr(base)}pwa-192.png" width="40" height="40" alt="Kody as a mail clerk">
<div class="brand-copy">
<h1>Mailroom</h1>
<p class="status-text" data-status-text></p>
</div>
</div>
<div class="actions" data-actions>
<span class="status-mark" data-status-mark aria-hidden="true"></span>
<button class="ghost" type="button" data-about>About</button>
<div class="install-wrap" data-install-wrap>
<button class="install install-trigger" type="button" data-install-open popovertarget="mailroom-install-popover">Install</button>
<div class="install-popover" id="mailroom-install-popover" popover data-install-popover>
<strong>Install Mailroom</strong>
<p>Home screen, full screen, works offline.</p>
<div class="install-steps" data-install-steps></div>
<button class="install" type="button" data-install hidden>Install now</button>
</div>
</div>
</div>
</header>
<div class="install-hint" data-install-hint hidden>
<p>Install: tap <strong>Share</strong>, then <strong>Add to Home Screen</strong>.</p>
<button class="install-hint-dismiss" type="button" data-install-hint-dismiss aria-label="Dismiss install tip">×</button>
</div>
<nav class="mailbox" data-mailbox aria-label="Mailbox">
<button type="button" data-direction="inbound" aria-current="true">Received</button>
<button type="button" data-direction="outbound">Sent</button>
</nav>
<input class="search" data-search type="search" placeholder="Search subject, from, to" aria-label="Search mail">
<section class="list" data-list aria-busy="false"></section>
<aside class="sheet" data-sheet hidden></aside>
<aside class="sheet" data-about-sheet hidden></aside>
<div class="toast" data-toast hidden>
<div>
<strong data-toast-title>Update available</strong>
<p data-toast-text>A newer app is ready. Reload to download it.</p>
</div>
<div class="toast-actions">
<button class="install" type="button" data-toast-action>Reload</button>
<button class="ghost" type="button" data-toast-dismiss>Later</button>
</div>
</div>
</main>
<script>${clientScript}</script>
</body>
</html>`
}
function escapeHtml(value: string): string {
return value
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
}
function escapeAttr(value: string): string {
return escapeHtml(value).replaceAll('"', '"')
}