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

function escapeAttr(value: string): string {
	return escapeHtml(value).replaceAll('"', '&quot;')
}