LeCodesdocs

App lifecycle, clipboard & openURL

The app's own lifecycle (foreground/background), the URL it was opened with (deep links), the system clipboard, and opening external URLs. app and clipboard are plain global objects — nothing to construct.

At a glance

TypeScript
app.addEventListener('pause', () => saveGame())      // user left — persist now
app.addEventListener('resume', () => refreshFeed())

if (app.launchUrl) openFromLink(app.launchUrl)       // cold-start deep link
app.addEventListener('url', url => openFromLink(url)) // …and links arriving while running

clipboard.write('LECODES-42')
openURL('https://le.codes/docs')

Lifecycle: app.state, pause / resume

TypeScript
app.state                                   // "active" | "background"  (read-only)
app.addEventListener('pause', () => { })    // app left the foreground
app.addEventListener('resume', () => { })   // app came back
app.removeEventListener('pause', callback)  // same function reference

"pause" fires when the app stops being the foreground app — home button, another app on top, the browser tab hidden. It is delivered before the host halts the frame loop, so it's the last reliable moment to persist state, pause music, or stop work that shouldn't run blind (a Geolocation watch, polling timers). "resume" fires when frames are running again.

The events are semantic and identical on every platform — you never see Activity/Scene vocabulary. app.state is the same information as a pull, readable at any time.

Note

don't count on code running while backgrounded — on mobile the frame loop stops and timers freeze until resume. pause is for saving, not for scheduling background work.

TypeScript
app.launchUrl                               // string | null  (read-only)
app.addEventListener('url', (url: string) => { })

launchUrl is the URL the app was (most recently) opened with — null for a plain launch. When a link arrives while the app is already running (a warm link), launchUrl updates to the new value and the "url" event fires with it. Handle both, in that order:

TypeScript
const openFromLink = (url: string) => {
  const screen = new URL(url).searchParams.get('screen')
  if (screen === 'stats') router.push(StatsScreen())
}
if (app.launchUrl) openFromLink(app.launchUrl)
app.addEventListener('url', openFromLink)
Note

host-gated — launchUrl reads null and "url" never fires on hosts without a link concept (headless).

World control: app.restart / app.quit

TypeScript
app.restart(url?)     // re-run this app's bundle in a fresh world — location.reload()
app.quit()            // leave to the LeCodes launcher (home) — no-op elsewhere

restart() reboots the app from scratch: fresh world, same code. Pass a url to reboot with different launch arguments (the new run reads them via app.launchUrl), pass null to reboot with a cleared one, or omit it to replay the current one — the argument mirrors launchUrl's role as the app's "command line". Unsaved state is gone, exactly like a page reload.

quit() returns the user to the LeCodes launcher. In a standalone-built app there is no launcher, so it does nothing (platforms forbid programmatic exit) — safe to wire to a "back to LeCodes" button unconditionally.

TypeScript
UIButton(UIText('Start over')).onClick(() => app.restart(null))
UIButton(UIText('Exit')).onClick(() => app.quit())

Keyboard: app.keyboardHeight + the keyboard event {#keyboard}

TypeScript
app.keyboardHeight                          // number  (read-only, logical px; 0 when hidden)
app.addEventListener('keyboard', (height: number, duration: number) => { })

keyboardHeight is the on-screen keyboard's raw overlap with the app viewport — independent of the focused input's keyboardShrink policy. With the default shrink policy the layout already avoids the keyboard, so you rarely need it; its main consumer is overlay mode (keyboardShrink: false), where the app manages its own inset. The event fires on every keyboard frame change; duration is the platform's keyboard animation duration in ms (0 where none) — pass it to animateTo to move UI in sync:

TypeScript
// chat composer: keyboard overlays the UI, the composer slides itself up
input.style({ keyboardShrink: false })
app.addEventListener('keyboard', (height, duration) => {
  composer.animateTo({ transform: `translateY(${-height}px)`, duration })
})
Note

host-gated — reads 0 and never fires where no on-screen keyboard exists (macOS, desktop, hardware keyboards).

clipboard

TypeScript
clipboard.write(text: string): void         // fire-and-forget
clipboard.read(): Promise<string>           // rejects: no access / denied / nothing readable

write puts text on the system clipboard (a silent no-op where there's no clipboard). read is async because the web host gates it behind a permission prompt; expect and handle rejection:

TypeScript
clipboard.write(inviteCode)
toast('Copied!')

try { input.value = await clipboard.read() }
catch { toast('Nothing to paste') }

openURL

TypeScript
openURL(url: string): void

Open url in the system browser / external handler — fire-and-forget, like share. Use it for "Terms of service", "Rate us", mailto: links. A silent no-op on hosts without one (headless).

TypeScript
UIButton('Privacy policy').onClick(() => openURL('https://le.codes/privacy'))

See also

  • Devicedevice.online + online/offline events (connectivity)
  • Geolocation — stop watches in pause
  • Storage — what to persist when pause fires