← Public packages
@kentcdodds/package-app-kit
Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.
src/client-assets.ts
43 lines · 31.3 KB · TypeScript/**
* Interim Worker serve helper for `/client/boot.js` + `/app.js`.
* Generated from `src/client/index.ts` via `node scripts/sync-client-assets.mjs`.
* Primary path after Cole #2284: `packageContext.clientModuleUrl` (do not import
* `src/client/index.ts` from the Worker graph).
*/
const CLIENT_MODULES: Record<string, string> = {
"boot.js": "// src/client/toast.ts\nfunction bootToast() {\n const host = document.querySelector(\"[data-toast-host]\");\n if (!host) return;\n const timers = {};\n function dismiss(id) {\n const node = host.querySelector(`[data-toast-id=\"${id}\"]`);\n if (node) node.remove();\n if (timers[id]) {\n clearTimeout(timers[id]);\n delete timers[id];\n }\n }\n function show(input) {\n if (!input?.title) return null;\n const id = String(input.id || `t${Date.now()}${Math.random().toString(16).slice(2)}`);\n dismiss(id);\n const tone = input.tone || \"default\";\n const node = document.createElement(\"div\");\n node.className = \"pak-toast\";\n node.setAttribute(\"data-toast-id\", id);\n node.setAttribute(\"data-tone\", tone);\n const body = document.createElement(\"div\");\n const title = document.createElement(\"strong\");\n title.textContent = input.title;\n body.appendChild(title);\n if (input.description) {\n const p = document.createElement(\"p\");\n p.textContent = input.description;\n body.appendChild(p);\n }\n node.appendChild(body);\n const actions = document.createElement(\"div\");\n actions.className = \"pak-toast-actions\";\n if (input.action?.label && typeof input.action.onClick === \"function\") {\n const act = document.createElement(\"button\");\n act.type = \"button\";\n act.textContent = input.action.label;\n act.addEventListener(\"click\", () => input.action.onClick());\n actions.appendChild(act);\n }\n const close = document.createElement(\"button\");\n close.type = \"button\";\n close.setAttribute(\"data-toast-dismiss\", \"\");\n close.setAttribute(\"aria-label\", \"Dismiss\");\n close.textContent = \"\\xD7\";\n close.addEventListener(\"click\", () => dismiss(id));\n actions.appendChild(close);\n node.appendChild(actions);\n host.appendChild(node);\n let duration = input.durationMs;\n if (duration == null) duration = input.action ? 0 : 3400;\n if (duration > 0) timers[id] = setTimeout(() => dismiss(id), duration);\n return id;\n }\n window.pakToast = { show, dismiss };\n}\n\n// src/client/config.ts\nfunction readClientConfig() {\n const root = document.documentElement;\n let json = {};\n const node = document.querySelector(\"[data-pak-config]\");\n if (node) {\n const raw = node.textContent || node.getAttribute(\"data-pak-config\") || \"\";\n if (raw.trim()) {\n try {\n json = JSON.parse(raw);\n } catch {\n json = {};\n }\n }\n }\n const appBase = String(json.appBase ?? root.getAttribute(\"data-app-base\") ?? \"\").replace(\n /\\/$/,\n \"\"\n );\n const assetBase = String(\n json.assetBase ?? root.getAttribute(\"data-asset-base\") ?? (appBase ? `${appBase}/_assets` : \"\")\n ).replace(/\\/$/, \"\");\n const clientModuleUrl = String(\n json.clientModuleUrl ?? root.getAttribute(\"data-client-module\") ?? \"\"\n );\n const swFromAttr = root.getAttribute(\"data-sw-url\") || \"\";\n const swUrl = String(\n json.swUrl ?? swFromAttr ?? (assetBase && root.getAttribute(\"data-asset-base\") ? `${assetBase}/sw.js` : `${appBase}/sw.js`)\n );\n return {\n appBase,\n assetBase,\n clientModuleUrl,\n hostedUrl: String(json.hostedUrl ?? root.getAttribute(\"data-hosted-url\") ?? \"\"),\n runningSha: String(json.runningSha ?? root.getAttribute(\"data-running-sha\") ?? \"\"),\n appName: String(json.appName ?? root.getAttribute(\"data-app-name\") ?? \"this app\"),\n appLabel: String(json.appLabel ?? root.getAttribute(\"data-app-label\") ?? \"app\"),\n hintDismissKey: String(\n json.hintDismissKey ?? root.getAttribute(\"data-install-hint-key\") ?? \"pak-install-hint-dismissed\"\n ),\n splashDismissKey: String(\n json.splashDismissKey ?? root.getAttribute(\"data-splash-key\") ?? \"pak-first-run-splash-dismissed\"\n ),\n versionPath: String(json.versionPath ?? root.getAttribute(\"data-version-path\") ?? \"/api/version\"),\n routes: Array.isArray(json.routes) ? json.routes.map(String) : [\"/\", \"/about\"],\n swUrl\n };\n}\nfunction apiUrl(config, path) {\n return `${config.appBase}${path.startsWith(\"/\") ? path : `/${path}`}`;\n}\n\n// src/client/install.ts\nfunction bootInstall() {\n const config = readClientConfig();\n let deferredInstall = null;\n let iosHintOpen = false;\n function isStandaloneDisplay() {\n const nav = navigator;\n if (nav.standalone === true) return true;\n try {\n return window.matchMedia(\n \"(display-mode: standalone), (display-mode: fullscreen), (display-mode: minimal-ui)\"\n ).matches;\n } catch {\n try {\n return window.matchMedia(\"(display-mode: standalone)\").matches;\n } catch {\n return false;\n }\n }\n }\n function isIosBrowser() {\n if (/iphone|ipad|ipod/i.test(navigator.userAgent)) return true;\n return navigator.platform === \"MacIntel\" && (navigator.maxTouchPoints || 0) > 1;\n }\n function iosHintHtml() {\n const safe = config.appName.replace(/</g, \"<\");\n return `Add <strong>${safe}</strong> to your Home Screen: tap <strong>Share</strong>, then <strong>Add to Home Screen</strong>.`;\n }\n function isHintDismissed() {\n try {\n return localStorage.getItem(config.hintDismissKey) !== null;\n } catch {\n return false;\n }\n }\n function syncInstallUi() {\n const wrap = document.querySelector(\"[data-install-wrap]\");\n const installBtn = document.querySelector(\"[data-install]\");\n const hint = document.querySelector(\"[data-install-hint]\");\n const hintCopy = document.querySelector(\"[data-install-hint-copy]\");\n const splash = document.querySelector(\"[data-first-run-splash]\");\n const ios = isIosBrowser();\n const standalone = isStandaloneDisplay();\n const canNative = Boolean(deferredInstall?.prompt);\n const hintDismissed = isHintDismissed();\n const showIos = !standalone && ios && !hintDismissed && !canNative;\n const available = !standalone && (canNative || showIos);\n if (wrap) {\n wrap.setAttribute(\"data-available\", available ? \"true\" : \"false\");\n wrap.hidden = standalone;\n }\n if (installBtn) {\n installBtn.setAttribute(\"aria-hidden\", available ? \"false\" : \"true\");\n if (available) installBtn.removeAttribute(\"tabindex\");\n else installBtn.setAttribute(\"tabindex\", \"-1\");\n }\n if (hint) {\n const open = showIos && iosHintOpen;\n if (hintCopy && open) hintCopy.innerHTML = iosHintHtml();\n hint.hidden = !open;\n if (!open) hint.setAttribute(\"hidden\", \"\");\n else hint.removeAttribute(\"hidden\");\n }\n if (splash) {\n let splashDismissed = false;\n try {\n splashDismissed = localStorage.getItem(config.splashDismissKey) !== null;\n } catch {\n }\n splash.hidden = standalone || splashDismissed;\n if (splash.hidden) splash.setAttribute(\"hidden\", \"\");\n else splash.removeAttribute(\"hidden\");\n }\n }\n async function promptInstall() {\n const promptEvent = deferredInstall;\n if (typeof promptEvent?.prompt !== \"function\") return false;\n deferredInstall = null;\n try {\n await promptEvent.prompt();\n if (promptEvent.userChoice) await promptEvent.userChoice;\n syncInstallUi();\n return true;\n } catch {\n syncInstallUi();\n return false;\n }\n }\n document.addEventListener(\"click\", (e) => {\n let t = e.target;\n if (t && t.nodeType === 3) t = t.parentElement;\n const el = t;\n if (!el?.closest) return;\n if (el.closest(\"[data-install-hint-dismiss]\")) {\n e.preventDefault();\n e.stopPropagation();\n try {\n localStorage.setItem(config.hintDismissKey, String(Date.now()));\n } catch {\n }\n iosHintOpen = false;\n syncInstallUi();\n return;\n }\n if (el.closest(\"[data-first-run-dismiss]\")) {\n try {\n localStorage.setItem(config.splashDismissKey, String(Date.now()));\n } catch {\n }\n syncInstallUi();\n return;\n }\n if (el.closest(\"[data-install]\")) {\n e.preventDefault();\n if (deferredInstall?.prompt) {\n void promptInstall();\n return;\n }\n if (isIosBrowser() && !isHintDismissed()) {\n iosHintOpen = !iosHintOpen;\n syncInstallUi();\n }\n }\n });\n function canUseCustomInstallCta() {\n if (isStandaloneDisplay()) return false;\n return Boolean(document.querySelector(\"[data-install]\"));\n }\n window.addEventListener(\"beforeinstallprompt\", (event) => {\n const promptFn = event.prompt;\n if (typeof promptFn !== \"function\" || !canUseCustomInstallCta()) {\n return;\n }\n event.preventDefault();\n deferredInstall = event;\n syncInstallUi();\n });\n window.addEventListener(\"appinstalled\", () => {\n deferredInstall = null;\n iosHintOpen = false;\n syncInstallUi();\n });\n window.pakPromptInstall = promptInstall;\n syncInstallUi();\n}\n\n// src/client/double-check.ts\nfunction bootDoubleCheck() {\n function labelFor(btn, armed) {\n const base = btn.getAttribute(\"data-label\") || btn.getAttribute(\"data-default-label\") || btn.textContent || \"Confirm\";\n const confirm = btn.getAttribute(\"data-confirm-label\") || \"Are you sure?\";\n if (!btn.getAttribute(\"data-default-label\")) btn.setAttribute(\"data-default-label\", base);\n return armed ? confirm : btn.getAttribute(\"data-default-label\") || base;\n }\n function setArmed(btn, armed) {\n btn.setAttribute(\"data-armed\", armed ? \"true\" : \"false\");\n btn.setAttribute(\"aria-pressed\", armed ? \"true\" : \"false\");\n if (!btn.disabled) btn.textContent = labelFor(btn, armed);\n }\n document.addEventListener(\n \"click\",\n (e) => {\n let t = e.target;\n if (t && t.nodeType === 3) t = t.parentElement;\n const el = t;\n if (!el?.closest) return;\n const btn = el.closest(\"[data-double-check]\");\n if (!btn || btn.disabled) return;\n const armed = btn.getAttribute(\"data-armed\") === \"true\";\n if (!armed) {\n e.preventDefault();\n e.stopPropagation();\n setArmed(btn, true);\n btn.focus();\n return;\n }\n setArmed(btn, false);\n btn.dispatchEvent(new CustomEvent(\"pak:confirmed\", { bubbles: true }));\n },\n true\n );\n document.addEventListener(\n \"blur\",\n (e) => {\n const t = e.target;\n if (!t?.closest) return;\n const btn = t.closest(\"[data-double-check]\");\n if (btn) setArmed(btn, false);\n },\n true\n );\n document.addEventListener(\"keyup\", (e) => {\n if (e.key !== \"Escape\") return;\n const t = e.target;\n if (!t?.closest) return;\n const btn = t.closest(\"[data-double-check]\");\n if (btn) setArmed(btn, false);\n });\n}\n\n// src/client/sw-register.ts\nfunction bootServiceWorkerRegistration() {\n const config = readClientConfig();\n async function unregisterStaleWorkers() {\n if (!(\"serviceWorker\" in navigator)) return;\n const regs = await navigator.serviceWorker.getRegistrations();\n const scopePrefix = `${config.appBase}/`;\n await Promise.all(\n regs.map((reg) => {\n let scopePath = \"\";\n try {\n scopePath = new URL(reg.scope).pathname;\n } catch {\n return null;\n }\n const underApp = scopePath === config.appBase || scopePath === scopePrefix || scopePath.startsWith(scopePrefix);\n if (underApp) return null;\n if (scopePath.startsWith(\"/packages/\") && !scopePath.startsWith(scopePrefix)) {\n return reg.unregister();\n }\n return null;\n })\n );\n }\n async function register() {\n if (!(\"serviceWorker\" in navigator)) return null;\n await unregisterStaleWorkers();\n const scriptUrl = config.swUrl.startsWith(\"http\") ? config.swUrl : config.swUrl.startsWith(\"/\") ? config.swUrl : `${config.appBase}/${config.swUrl}`;\n return navigator.serviceWorker.register(scriptUrl, {\n scope: `${config.appBase}/`,\n updateViaCache: \"none\"\n });\n }\n window.pakRegisterServiceWorker = register;\n}\n\n// src/client/update-check.ts\nvar UPDATE_CHECK_MIN_INTERVAL_MS = 3e3;\nfunction shouldRunUpdateCheck(input) {\n if (!input.visible || input.inFlight) return false;\n if (input.lastStartedAt == null) return true;\n const gap = input.minIntervalMs ?? UPDATE_CHECK_MIN_INTERVAL_MS;\n return input.now - input.lastStartedAt >= gap;\n}\nfunction describeAvailableUpdate(input) {\n if (input.hasWaitingWorker) return \"worker\";\n if (input.runningSha && input.latestSha && input.runningSha !== input.latestSha) {\n return \"published\";\n }\n return null;\n}\nfunction bootUpdateCheck() {\n const config = readClientConfig();\n const root = document.documentElement;\n const pwaState = { registration: null, waitingWorker: null };\n const updateCheck = { inFlight: false, lastStartedAt: null };\n function showUpdateToast(title, text, action) {\n window.pakToast?.show({\n title,\n description: text,\n durationMs: 0,\n action: { label: \"Refresh\", onClick: action }\n });\n }\n function applyUpdate() {\n const worker = pwaState.waitingWorker;\n if (worker) worker.postMessage(\"SKIP_WAITING\");\n else location.reload();\n }\n async function loadVersion() {\n try {\n const response = await fetch(apiUrl(config, config.versionPath), { cache: \"no-store\" });\n if (!response.ok) throw new Error(`version ${response.status}`);\n return await response.json();\n } catch {\n const sha = root.getAttribute(\"data-running-sha\") || \"\";\n return { sha, shortSha: sha.slice(0, 7), message: \"\", committedAt: \"\", publishedAt: \"\" };\n }\n }\n async function ensureServiceWorker() {\n if (!(\"serviceWorker\" in navigator)) return null;\n if (pwaState.registration) return pwaState.registration;\n try {\n if (window.pakRegisterServiceWorker) {\n pwaState.registration = await window.pakRegisterServiceWorker();\n } else {\n const scriptUrl = config.swUrl.startsWith(\"http\") || config.swUrl.startsWith(\"/\") ? config.swUrl : apiUrl(config, config.swUrl);\n pwaState.registration = await navigator.serviceWorker.register(scriptUrl, {\n scope: `${config.appBase}/`,\n updateViaCache: \"none\"\n });\n }\n const track = (worker) => {\n if (!worker) return;\n worker.addEventListener(\"statechange\", () => {\n if (worker.state === \"installed\" && navigator.serviceWorker.controller) {\n pwaState.waitingWorker = worker;\n showUpdateToast(\n \"Update available\",\n `A newer ${config.appLabel} is ready.`,\n applyUpdate\n );\n }\n });\n };\n track(pwaState.registration?.installing);\n pwaState.registration?.addEventListener(\"updatefound\", () => {\n track(pwaState.registration?.installing);\n });\n navigator.serviceWorker.addEventListener(\"controllerchange\", () => {\n if (pwaState.waitingWorker) location.reload();\n });\n return pwaState.registration;\n } catch {\n return null;\n }\n }\n async function checkForUpdates() {\n if (!shouldRunUpdateCheck({\n visible: document.visibilityState === \"visible\",\n inFlight: updateCheck.inFlight,\n lastStartedAt: updateCheck.lastStartedAt,\n now: Date.now(),\n minIntervalMs: UPDATE_CHECK_MIN_INTERVAL_MS\n })) {\n return;\n }\n updateCheck.inFlight = true;\n updateCheck.lastStartedAt = Date.now();\n try {\n const registration = await ensureServiceWorker();\n const version = await loadVersion();\n if (registration?.update) await registration.update();\n const waiting = pwaState.waitingWorker || registration?.waiting || null;\n if (waiting) pwaState.waitingWorker = waiting;\n const available = describeAvailableUpdate({\n runningSha: root.getAttribute(\"data-running-sha\") || \"\",\n latestSha: version.sha || \"\",\n hasWaitingWorker: Boolean(waiting)\n });\n if (available === \"worker\") {\n showUpdateToast(\n \"Update available\",\n `A newer ${config.appLabel} is ready. Refresh to apply it.`,\n applyUpdate\n );\n return;\n }\n if (available === \"published\") {\n showUpdateToast(\n \"Update available\",\n `Latest published commit is ${version.shortSha || String(version.sha || \"\").slice(0, 7)}. Refresh to download it.`,\n () => location.reload()\n );\n } else if (window.pakToast && document.querySelector(\n \"[data-about-check-update]:focus, [data-about-check-update][data-manual]\"\n )) {\n window.pakToast.show({\n title: \"Up to date\",\n description: \"No newer publish found.\",\n durationMs: 2400\n });\n }\n } finally {\n updateCheck.inFlight = false;\n }\n }\n window.pakCheckForUpdates = checkForUpdates;\n document.addEventListener(\"click\", (e) => {\n let t = e.target;\n if (t && t.nodeType === 3) t = t.parentElement;\n const el = t;\n const btn = el?.closest?.(\"[data-about-check-update]\");\n if (!btn) return;\n btn.setAttribute(\"data-manual\", \"1\");\n void checkForUpdates().finally(() => btn.removeAttribute(\"data-manual\"));\n });\n document.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"visible\") void checkForUpdates();\n });\n window.addEventListener(\"pageshow\", () => void checkForUpdates());\n window.addEventListener(\"focus\", () => void checkForUpdates());\n void ensureServiceWorker().then(() => checkForUpdates());\n}\n\n// src/client/router.ts\nfunction bootRouter() {\n const config = readClientConfig();\n const cache = /* @__PURE__ */ new Map();\n function relativeRoute(appBase, pathname) {\n var path = pathname || \"/\";\n if (path.length > 1 && path.charAt(path.length - 1) === \"/\") path = path.slice(0, -1);\n if (path === appBase || path === appBase + \"/\") return \"/\";\n if (appBase && path.indexOf(appBase + \"/\") === 0) {\n var rel = path.slice(appBase.length);\n return rel || \"/\";\n }\n if (path.charAt(0) === \"/\") return path;\n return null;\n }\n function isAppRoute(pathname) {\n var rel = relativeRoute(config.appBase, pathname);\n return rel != null && config.routes.indexOf(rel) !== -1;\n }\n function applyDocument(html, route) {\n var doc = new DOMParser().parseFromString(html, \"text/html\");\n var nextMain = doc.querySelector(\"[data-app-main]\");\n var curMain = document.querySelector(\"[data-app-main]\");\n if (!nextMain || !curMain) return false;\n curMain.replaceWith(nextMain);\n document.title = doc.title;\n var attrs = [\"data-page\", \"data-running-sha\"];\n for (var i = 0; i < attrs.length; i++) {\n var v = doc.documentElement.getAttribute(attrs[i]);\n if (v == null) document.documentElement.removeAttribute(attrs[i]);\n else document.documentElement.setAttribute(attrs[i], v);\n }\n var curNav = document.querySelectorAll(\".pak-nav a\");\n for (var j = 0; j < curNav.length; j++) {\n var href = curNav[j].getAttribute(\"href\") || \"\";\n var destRoute = relativeRoute(config.appBase, new URL(href, location.href).pathname);\n if (destRoute === route) curNav[j].setAttribute(\"aria-current\", \"page\");\n else curNav[j].removeAttribute(\"aria-current\");\n }\n document.dispatchEvent(new CustomEvent(\"pak:pagechange\", { detail: { route } }));\n return true;\n }\n async function swapDocument(html, route, fallbackUrl) {\n var ok = false;\n function go() {\n ok = applyDocument(html, route);\n }\n if (typeof document.startViewTransition === \"function\") {\n try {\n await document.startViewTransition(go).finished.catch(function() {\n });\n } catch (e) {\n if (!ok) go();\n }\n } else {\n go();\n }\n if (!ok) location.assign(fallbackUrl);\n }\n async function loadHtml(url, useCache) {\n if (useCache && cache.has(url)) return cache.get(url);\n try {\n var res = await fetch(url, {\n headers: { Accept: \"text/html\" },\n credentials: \"same-origin\",\n cache: \"no-cache\"\n });\n if (!res.ok) return null;\n var html = await res.text();\n cache.set(url, html);\n return html;\n } catch (e) {\n return null;\n }\n }\n function prefetchRoutes() {\n for (var i = 0; i < config.routes.length; i++) {\n var route = config.routes[i];\n var path = route === \"/\" ? \"/\" : route;\n void loadHtml(apiUrl(config, path), false);\n }\n }\n async function navigateTo(dest) {\n var route = relativeRoute(config.appBase, dest.pathname);\n if (!route) {\n location.assign(dest.href);\n return;\n }\n var html = await loadHtml(dest.href, true);\n if (!html) {\n location.assign(dest.href);\n return;\n }\n await swapDocument(html, route, dest.href);\n void loadHtml(dest.href, false);\n }\n const nav = window.navigation;\n if (nav && typeof nav.addEventListener === \"function\") {\n nav.addEventListener(\"navigate\", function(event) {\n if (!event.canIntercept || event.hashChange || event.downloadRequest) return;\n if (event.formData) return;\n var dest;\n try {\n dest = new URL(event.destination.url);\n } catch (e) {\n return;\n }\n if (dest.origin !== location.origin) return;\n if (!isAppRoute(dest.pathname)) return;\n event.intercept({\n handler: async function() {\n await navigateTo(dest);\n }\n });\n });\n } else {\n document.addEventListener(\"click\", function(e) {\n var t = e.target;\n var a = t && t.closest ? t.closest(\"a\") : null;\n if (!a || a.target === \"_blank\" || a.hasAttribute(\"download\")) return;\n if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n var href = a.getAttribute(\"href\");\n if (!href || href.charAt(0) === \"#\") return;\n var dest;\n try {\n dest = new URL(href, location.href);\n } catch (err) {\n return;\n }\n if (dest.origin !== location.origin) return;\n if (!isAppRoute(dest.pathname)) return;\n e.preventDefault();\n navigateTo(dest).then(function() {\n history.pushState({}, \"\", dest.href);\n });\n });\n window.addEventListener(\"popstate\", function() {\n void navigateTo(new URL(location.href));\n });\n }\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", prefetchRoutes, { once: true });\n } else {\n prefetchRoutes();\n }\n}\n\n// src/client/notes-island.ts\nimport { createElement as h, createRoot, on } from \"remix/ui\";\nfunction toast(title, description, tone) {\n window.pakToast?.show({\n title,\n description,\n tone: tone || \"default\",\n durationMs: 2800\n });\n}\nfunction mountNotes(host) {\n if (!host || host.getAttribute(\"data-notes-mounted\") === \"1\") return;\n host.setAttribute(\"data-notes-mounted\", \"1\");\n const config = readClientConfig();\n const root = createRoot(host);\n const state = {\n notes: [],\n title: \"\",\n loading: true,\n adding: false,\n inflight: /* @__PURE__ */ Object.create(null),\n armedId: null,\n error: null\n };\n function render() {\n const listChildren = [];\n if (state.loading) {\n listChildren.push(\n h(\n \"li\",\n { class: \"pak-list-item\", \"aria-hidden\": \"true\" },\n h(\"span\", { class: \"pak-skeleton\", style: \"height:1em;width:70%\" }),\n h(\"span\", { class: \"pak-skeleton\", style: \"height:2.5em;width:5.5em\" })\n )\n );\n } else if (state.error) {\n listChildren.push(\n h(\n \"li\",\n { class: \"pak-list-item\" },\n h(\"span\", { class: \"pak-muted\" }, state.error)\n )\n );\n } else if (!state.notes.length) {\n listChildren.push(\n h(\n \"li\",\n { class: \"pak-list-item\" },\n h(\"span\", { class: \"pak-muted\" }, \"No notes yet \\u2014 add one above.\")\n )\n );\n } else {\n for (const note of state.notes) {\n const pending = Boolean(state.inflight[note.id]);\n const armed = state.armedId === note.id;\n listChildren.push(\n h(\n \"li\",\n {\n class: \"pak-list-item\",\n \"data-note-id\": note.id,\n ...pending ? { \"data-pending\": \"true\" } : {}\n },\n h(\n \"div\",\n null,\n h(\"strong\", null, note.title),\n pending ? h(\"span\", { class: \"pak-pending-label\" }, \" Deleting\\u2026\") : null,\n h(\"div\", { class: \"pak-muted\", style: \"font-size:0.8rem\" }, note.createdAt || \"\")\n ),\n h(\"button\", {\n type: \"button\",\n class: \"pak-btn pak-btn-danger\",\n ...pending ? { disabled: true } : {},\n \"data-armed\": armed ? \"true\" : \"false\",\n \"aria-pressed\": armed ? \"true\" : \"false\",\n ...on(\"click\", () => onDeleteClick(note.id)),\n ...on(\"blur\", () => {\n if (state.armedId === note.id) {\n state.armedId = null;\n render();\n }\n })\n }, armed ? \"Are you sure?\" : \"Delete\")\n )\n );\n }\n }\n root.render(\n h(\n \"div\",\n { class: \"pak-stack\", \"data-remix-notes\": \"true\" },\n h(\n \"form\",\n {\n class: \"pak-row\",\n ...on(\"submit\", onSubmit)\n },\n h(\n \"label\",\n { class: \"pak-muted\", style: \"flex:1 1 180px\" },\n h(\"span\", { class: \"pak-eyebrow\" }, \"New note\"),\n h(\"input\", {\n name: \"title\",\n required: true,\n maxlength: \"120\",\n placeholder: \"Write a note\\u2026\",\n value: state.title,\n style: \"display:block;width:100%;min-height:var(--tap);margin-top:var(--space-1);padding:0 var(--space-3);border:1px solid var(--line);border-radius:var(--radius-sm);font:inherit;background:var(--surface);color:var(--ink)\",\n ...on(\"input\", (e) => {\n state.title = e.target.value;\n })\n })\n ),\n h(\"button\", {\n class: \"pak-btn pak-btn-accent\",\n type: \"submit\",\n ...state.adding ? { disabled: true } : {},\n style: \"flex:0 0 auto;align-self:end\"\n }, state.adding ? \"Adding\\u2026\" : \"Add\")\n ),\n h(\"ul\", { class: \"pak-list\", \"aria-live\": \"polite\" }, ...listChildren)\n )\n );\n }\n async function loadNotes() {\n state.loading = true;\n state.error = null;\n render();\n try {\n const res = await fetch(apiUrl(config, \"/api/notes\"), { cache: \"no-store\" });\n if (!res.ok) throw new Error(`notes ${res.status}`);\n const data = await res.json();\n state.notes = Array.isArray(data.notes) ? data.notes : [];\n state.loading = false;\n render();\n } catch (e) {\n state.loading = false;\n state.error = \"Could not load notes.\";\n render();\n toast(\"Could not load notes\", String(e?.message || e), \"error\");\n }\n }\n async function onSubmit(e) {\n e.preventDefault();\n const title = String(state.title || \"\").trim();\n if (!title || state.adding) return;\n state.adding = true;\n render();\n try {\n const res = await fetch(apiUrl(config, \"/api/notes\"), {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ title })\n });\n if (!res.ok) throw new Error(`create ${res.status}`);\n state.title = \"\";\n state.adding = false;\n toast(\"Note added\", title, \"success\");\n await loadNotes();\n } catch (err) {\n state.adding = false;\n render();\n toast(\"Could not add note\", String(err?.message || err), \"error\");\n }\n }\n async function onDeleteClick(id) {\n if (state.inflight[id]) return;\n if (state.armedId !== id) {\n state.armedId = id;\n render();\n return;\n }\n state.armedId = null;\n state.inflight[id] = true;\n render();\n try {\n const res = await fetch(apiUrl(config, `/api/notes/${encodeURIComponent(id)}`), {\n method: \"DELETE\"\n });\n if (!res.ok) throw new Error(`delete ${res.status}`);\n delete state.inflight[id];\n state.notes = state.notes.filter((n) => n.id !== id);\n render();\n toast(\"Note deleted\", \"Removed successfully.\", \"success\");\n } catch (err) {\n delete state.inflight[id];\n render();\n toast(\"Could not delete\", String(err?.message || err), \"error\");\n }\n }\n void loadNotes();\n}\nfunction bootNotesIsland() {\n function tryMount() {\n const host = document.querySelector(\"[data-notes-root]\");\n if (host) mountNotes(host);\n }\n document.addEventListener(\"pak:pagechange\", () => tryMount());\n tryMount();\n}\nfunction bootToastDemo() {\n document.addEventListener(\"click\", (e) => {\n let t = e.target;\n if (t && t.nodeType === 3) t = t.parentElement;\n const el = t;\n if (!el?.closest?.(\"[data-toast-demo]\")) return;\n window.pakToast?.show({\n title: \"Toaster ready\",\n description: \"Sonner-inspired toast from package-app-kit.\",\n tone: \"success\",\n durationMs: 2800\n });\n });\n}\n\n// src/client/index.ts\nbootToast();\nbootServiceWorkerRegistration();\nbootInstall();\nbootDoubleCheck();\nbootUpdateCheck();\nbootRouter();\nbootToastDemo();\nbootNotesIsland();\n",
}
export const CLIENT_BOOT_PATH = '/client/boot.js'
export function listClientModules(): string[] {
return Object.keys(CLIENT_MODULES)
}
/** Return browser ESM source for a client filename, or null. */
export function readClientModule(filename: string): string | null {
const key = filename.replace(/^\.\//, '').replace(/^client\//, '')
if (!Object.prototype.hasOwnProperty.call(CLIENT_MODULES, key)) return null
return CLIENT_MODULES[key]
}
/** Response for a committed client ESM module. */
export function clientModuleResponse(filename: string): Response | null {
const body = readClientModule(filename)
if (body == null) return null
return new Response(body, {
headers: {
'content-type': 'text/javascript; charset=utf-8',
'cache-control': 'no-store',
},
})
}
/** `/app.js` and `/client/boot.js` → bundled boot entry (interim). */
export function clientBootResponse(): Response {
return clientModuleResponse('boot.js')!
}
/** Primary callable: serve boot module Response. */
export default clientBootResponse