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.

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, \"&lt;\");\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