Glider Documentation Glider Core 1.3

Glider quickstart

Glider is a single HTML file that is a whole app: a small React app (its source code), its data, and the runtime that runs and edits it, all in one self-saving file — in the spirit of TiddlyWiki, but aimed at tiny single-purpose apps (a Scrabble scorer, a book log, a packing list) instead of a wiki.

Use an app

Open a Glider file (e.g. dist/glider.html, or any example in dist/glider-examples/) in a browser. That's it — the app renders under a thin menubar showing its name. By default your changes are also remembered by the browser, so reopening the same file picks up where you left off.

Edit the app

Menubar ☰ → Edit Code (or ⌘E / Ctrl+E) opens a split view: code editor on the left, live app on the right. The app is one JSX file that defines function App(). A few globals are injected — click ▸ Globals at the top of the editor for the reference. The essentials:

const { data, setData } = useGlider({ count: 0 })  // persistent data; setData merges keys
setAppTitle('My Counter')                          // rename the app
useMenu([{ label: 'Reset', onClick: () => setData({ count: 0 }) }])  // add ☰ items

Edits re-render live as you type. If the code doesn't compile, an error banner shows and the last good version keeps running.

Save

☰ → Save (or ⌘S / Ctrl+S) writes the whole thing — runtime, your code, your data, your settings — back into one HTML file:

  • In a browser with the File System Access API (Chrome/Edge), Save As… picks a file once, then Save overwrites it in place.
  • Otherwise Save downloads a fresh copy.
  • Inside Hangar (the iOS container app), saving is automatic.

A dot next to the app name means unsaved changes. Send the saved file to anyone — it's the entire app.

Settings

☰ → Settings… covers the app name, appearance (light/dark shell and editor themes), where changes live between visits (browser storage vs manual-save-only), a view of the app's raw data with a reset button, and the Glider Core version. The Jet syntax toggle (an opt-in Swift-flavored alternative to JSX — off by default, see manual/writing-apps.md) lives in the Editor view instead, next to ▸ Globals.

Make your own app

Easiest path: open dist/glider.html, edit the code (it ships with a Scrabble scorer that demonstrates the whole API), rename it in Settings, and Save As… under a new name. When an app outgrows the built-in editor, ☰ → Eject as Zip… unpacks it into index.html + app.jsx + data.json for editing in a real editor (serve the folder over HTTP; saving from the page re-packs it into a single file).

Building from source

npm install
npm run dev      # dev-serve Glider
npm run build    # dist/glider.html, dist/aircraft.html, dist/glider-examples/

Want more? The full manual is in manual/ — start with manual/overview.md.

Overview

What Glider is

Glider is a tool for making and carrying tiny single-purpose apps — a Scrabble scorer, a book log grouped by author, a receipt splitter — each one a single self-contained HTML file. Like TiddlyWiki, the file is the application: it carries its own runtime, its own editor, its own data, and it can save itself back out as a new copy of the file. Unlike TiddlyWiki, Glider is not a wiki; each file is exactly one app.

Opening a Glider file in any modern browser runs the app. Editing it needs nothing but the file itself — a code editor is built in. Saving produces a new complete HTML file (or overwrites the old one in place, in browsers that allow it). The long-term goal (see CLAUDE.md) is API compatibility with TiddlyWiki save servers such as tiddlyhost.com, so a Glider file could live on one masquerading as a TiddlyWiki.

The three names

This repo contains three related things (full history in NAMING.md):

  • Glider (glider/dist/glider.html) — the simple one-app version described above. One file = one app: menubar, live code editor, settings, save. When unqualified, "Glider" means this. This manual is mostly about it.
  • Aircraft (aircraft/dist/aircraft.html) — the original, full-featured multi-window version: many apps, libraries, notes, and datasets in one file, four window-management styles, a command palette. See Aircraft.
  • Hangar (hangar/) — a native iOS container app (SwiftUI + WKWebView) that holds a library of Glider files, runs each in its own web view, and autosaves them to disk. See Hangar.

How a Glider file works

A built Glider file is ordinary HTML containing:

  • the runtime ("Glider Core"): React 18, Babel standalone, the CodeMirror editor, and the shell chrome, all inlined by vite-plugin-singlefile;
  • the store: a <script id="glider-store" type="application/json"> tag holding the app's name, JSX source code, data blob, and settings;
  • a <meta name="glider-core"> tag stamping the core version that wrote it.

On load, the runtime reads the store, transpiles the JSX source in the browser, and renders the resulting App component. On save, it serializes the live document — current code, data, and settings included — back into a complete HTML string and writes it out. Code, data, and chrome never leave the file.

The app model

An app is one JSX source file defining function App(). It runs against a deliberately tiny injected API:

  • useGlider(defaults?){ data, setData } — the app's single persistent JSON data blob; setData(partial) merges top-level keys.
  • setAppTitle(name) — rename the app (shows in the menubar and file name).
  • useMenu([{ label, onClick }]) — put items in the ☰ menu.
  • requestNetwork() — ask a Hangar host to offer the user a network toggle (apps have no network inside Hangar by default; a no-op in browsers).
  • React 18 plus the common hooks (useState, useEffect, useRef, useCallback, useMemo, useReducer, useId).

No imports, no build step, no package.json — the file is the whole toolchain. Details in Writing apps.

Where changes live

Between visits, changes persist according to a per-browser storage mode: browser-local snapshots (the default), manual (nothing outside the file), or Hangar-managed autosave. Saving the file is always what makes changes portable. Details in Using Glider and File format.

Using Glider

The shell

A running Glider file shows a thin fixed menubar — the app's name on the left (with a dot when there are unsaved changes), a ☰ button on the right — and the app below it. The ☰ menu contains, in order:

  1. any items the app registered via useMenu (e.g. "Clear History");
  2. Edit Code / Close Editor and Settings… / Close Settings — inside Hangar this item reads Edit Code (in Glider) to distinguish it from Hangar's own native editor;
  3. Save, Save As… (only in browsers with the File System Access API), and Eject as Zip… (only in browsers — not inside Hangar).

Keyboard shortcuts: ⌘S / Ctrl+S saves, ⌘E / Ctrl+E toggles the editor. Escape or clicking outside closes the menu. Inside Hangar the HTML menubar is hidden and the same menu appears in the native navigation bar instead.

The three views

  • App — the default; just the app.
  • Editor — a split view: CodeMirror editor on the left, the live app on the right. Edits flow to the app after a ~250 ms pause in typing. The editor header has a read-only Globals section (click ▸ Globals) documenting the injected API, and a Jet syntax toggle (off by default) next to it that lets the app's code use Jet — a Swift-flavored alternative syntax with labeled arguments and trailing closures, e.g. div(class: "app") { h1 { "Hello" } } instead of JSX; the Globals section lists the extra Jet element functions (div, h1, fragment, …) while it's on. See Writing apps for the syntax tour. If an edit doesn't compile, a red banner shows the error (message, line/column, and a code frame) — if the error looks like Jet syntax written with the toggle off, the banner adds a line pointing at the toggle — while the last good version keeps rendering; if the app throws at runtime, an error screen appears with an Open Editor button.
  • Settings — see below. The app stays mounted (hidden) under Settings, so its in-memory state survives.

Saving

Save rewrites the entire document — runtime, current code, data, and settings — as one self-contained HTML file, then hands it off down this priority list:

  1. Hangar bridge — inside Hangar, the file is written directly to the library; there's no download UI and saving is also automatic (see below).
  2. File handle — if you've done Save As… (Chrome/Edge), Save overwrites that same file in place from then on.
  3. Download — otherwise, Save downloads a fresh copy named after the app (e.g. scrabble-scorer.html).

The dirty dot next to the name clears on save. Settings → Storage also offers Download a Copy, which saves a duplicate without touching the Save-As file handle or the dirty state — useful for sharing.

Storage modes (Settings → Storage)

Where changes live between visits, independent of the file:

  • Browser storage (default) — every change is snapshotted to localStorage, keyed by the file's app id. Reopening the same file in the same browser restores your latest state even if you never saved. Saving the file is still what makes changes permanent and portable.
  • Manual save file — nothing is kept in the browser; changes last until the page closes unless you save.
  • Hangar file (forced inside Hangar) — changes autosave straight to the file moments after you make them, and when you leave the app. Browser storage isn't used at all.

The local/manual choice is remembered per browser, never written into the file. Ejected projects (below) are pinned to manual, since their on-disk files are the source of truth.

Settings

  • App — the app's name (also settable from code via setAppTitle). The Jet syntax toggle lives in the Editor view now (next to ▸ Globals), not here — it's a property of the code you're writing.
  • Appearance — follow system / light / dark, plus separate shell-theme and editor-theme choices for light and dark mode. Inside Hangar, extra iOS-look palettes appear in a "Hangar" group.
  • Storage — the storage mode, plus Save / Save As… / Download a Copy.
  • Data — a read-only JSON view of the app's current data (everything useGlider reads and writes) and a confirm-guarded Reset Data….
  • About — the Glider Core version this file carries.

Eject: growing out of the built-in editor

☰ → Eject as Zip… downloads the app unpacked into a three-file project:

  • index.html — the Glider runtime, its store emptied and pointed at the files below;
  • app.jsx — the app source, editable in any editor;
  • data.json — the current data.

Serve the folder over HTTP (npx serve, python3 -m http.server) — the fetches don't work from file://. The page loads code and data from those files, so external edits are the source of truth. Save from an ejected page re-embeds everything into a normal single-file Glider app again. Eject is a pure export: the running file is unaffected.

Writing Glider apps

An app is one JSX source file that defines function App(). Glider transpiles it in the browser (Babel standalone) and renders <App /> below the menubar. There is no module system: no import or export — everything you need is injected as globals, and everything else you define yourself in the same file (helper functions, constants, extra components are all fine).

The injected API

// React 18 — required for JSX; plus these hooks as bare globals:
//   useState, useEffect, useRef, useCallback, useMemo, useReducer, useId

const { data, setData } = useGlider(defaults?)
// The app's single persistent JSON data blob.
//   - `data` is a plain object; with `defaults` given, missing top-level keys
//     are filled from it (stored values win).
//   - `setData(partial)` shallow-merges top-level keys into the blob:
//     setData({ history: [...] }) replaces `history`, leaves other keys alone.
//   - Persistence, dirty-tracking, and save all key off this blob; keep it
//     JSON-serializable (no functions, Dates as strings/numbers, etc.).

setAppTitle('Name')
// Renames the app: menubar title, document title, save filename. Safe to call
// during render. Note the user can rename in Settings too, so calling this
// unconditionally on every render will fight them — call it on an event, or
// not at all (the name is usually set once in Settings).

useMenu([{ label: 'Clear History', onClick: () => ... }])
// Registers ☰ menu items for as long as the component is mounted. Call it
// every render with fresh closures (it's cheap; re-render only happens when
// labels change). Items appear above the built-in Edit/Settings/Save items.

requestNetwork()
// Inside Hangar, apps get no network (http/https/ws blocked). Calling this
// makes Hangar show a per-app network toggle for the user to grant access.
// A no-op in ordinary browsers — safe to call unconditionally if your app
// fetches anything.

That's the whole API, by design. State that shouldn't survive a reload (current input text, an open panel) is ordinary useState; state that should (the score history, the book list) goes in useGlider's blob.

Minimal example

function App() {
  const { data, setData } = useGlider({ count: 0 });
  useMenu([{ label: 'Reset', onClick: () => setData({ count: 0 }) }]);
  return (
    <div style={{ padding: '2rem', textAlign: 'center' }}>
      <div style={{ fontSize: '3rem' }}>{data.count}</div>
      <button onClick={() => setData({ count: data.count + 1 })}>+1</button>
    </div>
  );
}

A fuller worked example ships as every new file's default app (a Scrabble scorer — open the editor on a fresh dist/glider.html), and twelve more live in glider/examples/ (book log, pomodoro, Yahtzee scorecard, receipt splitter, …; see glider/examples/README.md).

Optional: Jet syntax

A Jet syntax toggle in the Editor view (next to ▸ Globals, off by default per app) lets app code use Jet — a Swift-flavored alternative to JSX with labeled arguments, trailing closures, and result-builder bodies. With it on, this means the same thing as the minimal example above:

function App() {
  const { data, setData } = useGlider({ count: 0 });
  return div(class: "app") {
    h1 { `Count: ${data.count}` }
    button(onClick: () => setData({ count: data.count + 1 })) { "+1" }
  }
}

When the setting is on, Jet's compiler runs before Babel and injects one function per standard HTML tag plus fragment (div, h1, button, ul, li, … — the Jet element runtime, jetElements(React); the one exception is var, a JS reserved word that can't be a global — use JSX <var> if you need that element) as extra globals, alongside everything in The injected API above. When it's off, none of this exists and app code is exactly what it always was — the compile pipeline doesn't change at all.

Writing your own components for Jet call syntax: a trailing closure is always the call's last argument. Card { … } compiles to Card(() => […]) — the builder lands as Card's first argument, since there's nothing else to put it after. Card(x: 1) { … } compiles to Card({x: 1}, () => […]) — now it's the second. A hand-written, stateless component meant to be called directly with a trailing closure needs to accept the builder wherever it lands, the same way jetElements' own tag functions do: (propsOrBuilder, builder), shifting on typeof propsOrBuilder === 'function'.

Stateful components — mark them @component: calling a component directly runs its hooks in the caller's hook list, not its own — state won't survive independently once it's called conditionally or more than once. Mark any component that calls a hook, immediately before its top-level function/const declaration:

@component
function Counter({ label }) {
  const [n, setN] = useState(0);
  return button(onClick: () => setN(n + 1)) { `${label}:${n}` }
}

The compiler rewrites every call to a marked name (Counter(), Counter(label: "a"), Counter { … }, all forms — unless the name is locally shadowed) into React.createElement(Counter, …), a real mounted instance per call. Write the component itself exactly like an ordinary React function component; nothing inside it changes, and it needs none of the (propsOrBuilder, builder) shifting above.

Inside a builder body, the identifier __jet is reserved (it names the compiled-in collection accumulator) — using it as a binding or reference there is a compile error; the name is unaffected everywhere else.

See jet/README.md for the full syntax tour (labeled arguments incl. hyphenated/reserved-word keys, same-line-brace closure attachment, builder-body control flow and its three restrictions, the error catalog) and JET_SPEC.md for the canonical grammar.

Theming: use the shell variables

The shell exposes its palette as CSS custom properties, and switches them automatically for light/dark and the user's chosen themes. Style with them instead of hardcoded colors and your app matches every theme:

Variable Role
--g-bg / --g-bg2 / --g-bg3 page background / cards & panels / recessed fills
--g-text / --g-text2 / --g-text3 body text / secondary / disabled-hint
--g-border / --g-border2 structural 1px borders / interactive-control borders
--g-accent / --g-accent-text accent fill / text on accent
--g-danger, --g-error, --g-warning, --g-success, --g-info semantic colors
--g-link, --g-highlight, --g-insertion, --g-deletion links, selection, diff-ish accents

Conventions worth matching (the shell's own design language, GLIDER_DESIGN.md): flat solid fills, 1px borders, 4–6px radii, text-labeled buttons, and accent color for controls/highlights only — never body text. A card is background: var(--g-bg2); border: 1px solid var(--g-border); border-radius: 6px; padding: 1.25rem.

Error behavior while developing

  • Compile error (syntax, etc.): a red banner appears above the app; the last successfully compiled version keeps running. Fix the code and the banner clears.
  • Runtime error (throw during render): an error screen replaces the app with the message and an Open Editor button. Any code change remounts the app fresh.

Portability

The same app code runs unchanged in Glider, in Hangar (which hosts Glider files), and in Aircraft's app windows — useGlider and React hooks are common to both shells. Aircraft additionally injects libraries and dataset hooks that plain Glider doesn't have; apps meant to stay portable should stick to the API above.

The Glider file format

A Glider file is a complete, valid HTML document. Everything specific to your app lives in one embedded JSON store; everything else is the Glider Core runtime, identical across every file of the same core version.

The store tag

<script id="glider-store" type="application/json">
{
  "version": 1,
  "kind": "glider",
  "appId": "app-m3k9xz-4fq81a",
  "name": "Scrabble Scorer",
  "code": "function App() { ... }",
  "data": { "history": [ ... ] },
  "settings": {
    "appearance": "system",
    "lightShellThemeId": "primaryLight",
    "darkShellThemeId": "monokaiPro",
    "lightEditorThemeId": "flatwhite",
    "darkEditorThemeId": "monokaiPro"
  }
}
</script>
  • kind: "glider" marks the file as a (simple) Glider file — Aircraft stores use the same tag id but have no kind field. A store without kind: "glider" is ignored and the file boots as a fresh default app.
  • appId gives each saved file its own localStorage bucket (glider:<appId>), so several Glider apps served from one origin don't share state. It's assigned lazily at first save; null until then.
  • code is the raw JSX source (what the editor shows), data the app's JSON blob, settings the appearance choices. The storage mode is deliberately not in the file — it's a per-browser choice.
  • Ejected projects add "external": { "code": "app.jsx", "data": "data.json" } — paths relative to index.html that are fetched before first render; the embedded code/data are then empty placeholders.

The core version stamp

<meta name="glider-core" content="1.1">

Every built and every saved file records the Glider Core version (glider/src/version.js) that wrote it, so tools like Hangar can read a file's version without executing it. The scheme is major.minor, deliberately not semver:

  • major — a compatibility era; a new major is a fresh lineage with no upgrade path from the old one.
  • minor — a migratable step: core A.m must read and best-effort-migrate files written by A.(m−1) (in practice the store parser accepts every older minor of the same major). Hangar upgrades files one minor at a time using bundled per-version templates.

How saving works

Save rebuilds the document from the live DOM rather than editing the HTML as text: the document is cloned, #root is emptied (saved files never carry a stale render), the clone's store tag gets the freshly serialized JSON with < escaped (so app code containing </script> can't truncate the store), the core-version meta is stamped, and the missing doctype is re-prepended. The result is byte-complete HTML handed to whichever sink applies: the Hangar bridge, a Save-As file handle, or a download.

Because Hangar autosaves on nearly every change, the expensive clone-and-serialize is cached: the multi-megabyte shell around the store JSON is split once and reused until the name, settings, or system color scheme changes.

Compatibility goals

The project goal (see CLAUDE.md) is TiddlyWiki-style behavior end to end: self-contained, self-saving files that could be uploaded to a TiddlyWiki host such as tiddlyhost.com and saved back via the host's own API without the host knowing the difference. The single-file/self-saving side is done; speaking TiddlyWiki server save protocols is future work.

Historical identifiers

Several names predate the Glider/Aircraft split and are kept for file compatibility (see NAMING.md): the glider-store tag id is shared with Aircraft, Aircraft's localStorage keys are glider-data/glider-save-mode, and its export type strings are glider-app, glider-library, etc. Don't rename any of these.

Hangar — the iOS container

Hangar (hangar/) is a native iOS app — SwiftUI, iOS 16+ — that turns a phone into a shelf of Glider apps. It keeps a library of ordinary Glider .html files in its Documents directory (visible in the Files app, syncable via iCloud Drive) and runs each one in its own WKWebView with native chrome. Design rationale lives in HANGAR_PLAN.md; per-file roles and Xcode integration steps in hangar/README.md.

What it does

  • Library — list of installed apps with add (new from template, import from Files or URL, install a bundled example), duplicate, rename (a Hangar-only nickname; the file's own name is untouched), and delete.
  • Running apps — each app opens full-screen; the navigation bar carries the app's name and its ☰ menu (the page's HTML menubar is hidden and the menu model is mirrored over the bridge instead). Under host chrome the page's own edit item reads "Edit Code (in Glider)" to leave room for Hangar's native editor.
  • Native code editor — files on Core 1.2+ get a second menu item, "Edit Code (in Hangar)", next to the page's own editor. It opens a full-height sheet with a native (Runestone) code editor mirroring the app's current theme, plus a two-row keyboard accessory bar (symbols, numbers) for programming punctuation the system keyboard buries. Save injects the edited code back into the running page — which recompiles and autosaves exactly like an in-page edit — and closes the sheet; Cancel discards. Swipe-down with unsaved changes prompts Save / Discard / Keep Editing. Older files (below 1.2) don't get the item.
  • Autosave — the page's save lands directly on the file via the bridge; Glider's hangar storage mode debounce-saves after every change, and Hangar flushes all open apps when backgrounded. Users never think about saving.
  • Isolation — a custom glider://<file-id>/ URL scheme gives every app its own origin, so localStorage and friends can't leak between apps.
  • Network permission — apps get no network by default: a compiled WKContentRuleList blocks http/https/ws. An app that calls requestNetwork() makes Hangar surface a per-app toggle in the nav bar; the user decides.
  • Native palettes — Hangar contributes iOS-look shell palettes over the bridge; they appear in a "Hangar" group in the app's Settings.

The bridge (summary)

Injected at document start into every page:

window.__gliderHost = {
  platform: 'hangar-ios',
  shellPalettes,            // extra --g-* palettes for Settings
  save(html),               // write the file to the library
  menuChanged(payload),     // page → host: { name, items } menu model
  requestNetwork(),         // page → host: ask for the network toggle
}

and the page exposes window.__glider = { save, coreVersion, menuAction } for the host to flush saves and invoke menu items. The presence of __gliderHost.save is what flips Glider into hangar storage mode. The precise contract is specified in ../agent/reference/host-bridge.md and hangar/README.md — the two sides must change together.

Core versions and upgrades

Hangar bundles one Glider build per supported core version (GliderTemplate-<major>.<minor>.html) plus GliderExamples.json (regenerated by npm run build:examples). "New App" uses the newest template; older templates exist so installed files can be upgraded one minor step at a time, per the core versioning contract (file format). Each file's version is read from its glider-core meta tag, not the filename.

Repo specifics

There's deliberately no Package.swift: the Xcode project lives in hangar/Hangar.xcodeproj with sources under hangar/Sources/. App Store readiness material (privacy manifest, review notes) is in hangar/PrivacyInfo.xcprivacy and hangar/APP_STORE_REVIEW_NOTES.md.

Aircraft — the multi-window version

Aircraft (aircraft/dist/aircraft.html) is the original, full version of this project: still one self-contained HTML file, but hosting a whole workspace rather than a single app. It was called "Glider" before the simple one-app version took that name (NAMING.md); its architecture decisions — the authoritative documentation for it — are recorded in DECISIONS.md at the repo root (which predates the rename and says "Glider" throughout while meaning Aircraft).

What it adds over Glider

  • Many entities in one file — apps, libraries (shared code that apps and other libraries can depend on), notes (Markdown with a live inline-rendering editor), and datasets (first-class shared data with dependency-gated access).
  • Window management — every entity opens in a window on a canvas, under one of four pluggable WM styles: River+Stack, Floating, Docked (IDE-style fixed zones), and FlexGrid (shelves with aligned/masonry grids). Windows have macOS-style traffic lights; each style ships a crash-course note.
  • Sidebar & command palette — entity lists with context menus, fuzzy search, and a keyboard-driven command palette.
  • Import/export — entities exchange as JSON with type strings like glider-app, glider-library, glider-note (names kept for compatibility).
  • Richer app runtime — apps get useGlider plus their declared library dependencies and dataset hooks injected as globals.

Relationship to Glider

Glider was extracted as the deliberately-simple sibling, and the two stay compatible where it matters:

  • App code is portable — both shells inject useGlider and the React hooks, so an app written for Glider runs in an Aircraft window (Aircraft apps that use libraries or datasets don't port back).
  • Shared theme sources — Glider imports three leaf modules from Aircraft's tree (transpile.js, shellThemes.js, themes.js), so JSX transpilation, the --g-* shell palettes, and the editor themes are identical in both.
  • Different stores — both embed <script id="glider-store">, but Aircraft's store has no kind field (Glider's says kind: "glider") and holds many entities plus WM state; Aircraft persists to the glider-data localStorage key.

Building and running

npm run dev:aircraft     # dev server
npm run build:aircraft   # → dist/aircraft.html

For feature-level detail (WM style semantics, dataset design, the meta-app dependency tree, theming engine), read DECISIONS.md, WM_STYLE_PROPOSALS.md, and DATASET_SYSTEM_PROPOSAL.md at the repo root.

Developing this repo

Layout

glider/            Glider (simple one-app version)
  index.html         dev entry; carries the placeholder store tag
  src/               shell: store, save, runner, theme, components/
  examples/          example apps (app.jsx + meta.json [+ data.json] each)
aircraft/          Aircraft (original multi-window version)
  index.html, src/
hangar/            Hangar (iOS container): Sources/*.swift, Xcode project,
                   bundled GliderTemplate-*.html + GliderExamples.json
dist/              committed build output: glider.html, aircraft.html,
                   glider-examples/
scripts/           build-examples.mjs
docs/              this documentation (see docs/README.md)

Root-level *.md files are design records: NAMING.md, GLIDER_DESIGN.md (shell design language), GLIDER_PLAN.md, HANGAR_PLAN.md, DECISIONS.md (Aircraft), proposals.

Build & dev

npm install
npm run dev              # Vite dev server for Glider
npm run dev:aircraft     # …for Aircraft
npm run build:glider     # → dist/glider.html (single file)
npm run build:aircraft   # → dist/aircraft.html
npm run build:examples   # → dist/glider-examples/*.html + hangar/GliderExamples.json
npm run build:docs       # → dist/docs.html (from docs/ + docs/template.html)
npm run build            # all four, in that order

Both Vite configs (vite.glider.config.js, vite.config.js) use vite-plugin-singlefile to inline everything, write into the shared dist/ with emptyOutDir: false (so they don't delete each other's output), and stamp the glider-core meta tag. build:examples merges each example's source into the already-built dist/glider.html, so run build:glider first when the core changed.

Process rules (from CLAUDE.md — binding)

  • No browser testing — this is a siloed dev machine; verify by static reading/headless techniques, not Playwright or a browser.
  • Commit after every change, including regenerated dist/ files whenever a change affects build output. Someone else pulls and tests.
  • Document decisions in the repo (like this manual, DECISIONS.md, proposal files) rather than relying on chat history. For large architecture changes, write and commit a root-level pros/cons proposal .md first.

Conventions

  • Shell UI follows GLIDER_DESIGN.md: flat fills, 1px borders, 4–6px radii, text over icons, accent color only for controls — and generous, card-based layouts.
  • Cross-tree imports: Glider may import Aircraft's leaf modules only — currently aircraft/src/lib/transpile.js, shellThemes.js, themes.js. Nothing shell-related, and nothing in the other direction.
  • Compatibility identifiers (store tag id, localStorage keys, export type strings, useGlider/GliderContext, package name) keep their historical "glider" names even where they belong to Aircraft — see NAMING.md before renaming anything.
  • Core version (glider/src/version.js): bump the minor for store-shape or runtime changes that old files must survive; keep normalize() in store.js accepting older minors; add (never replace) a GliderTemplate-<version>.html in hangar/ when a new minor ships.

Where agents should look

CLAUDE.md is the agent entry point; the precise runtime/format/bridge specs live in ../agent/reference/.

For agents

To have an AI model (or any coding agent) write a Glider app for you, paste the block below into its prompt — as a system prompt, as project instructions, or simply ahead of your request. It is self-contained: the model needs no access to this repo or this page. Then ask for the app you want (“a tip calculator”, “a reading log grouped by author”).

  • The model's entire deliverable is one JSX source file defining function App() — paste it into Glider's editor (☰ → Edit Code), replacing what's there.
  • The block is the contents of docs/agent/PROMPT.md in the repo; this page renders it verbatim, so the copy button always matches the committed version.

The prompt block

# Glider app authoring context

You are writing an app for Glider: a self-contained single-HTML-file app runner. Your entire deliverable is ONE JSX source file that defines `function App()`. Glider transpiles it in the browser and renders `<App />` under a thin menubar. Output only the JSX source — no HTML, no build config.

## Hard rules

- Define `function App() { ... }` at top level. It is the root component.
- NO `import` or `export` statements — there is no module system. Helper functions, constants, and extra components are defined in the same file.
- No external resources: no CDN scripts, no npm packages, no stylesheets. Styling is inline `style={{...}}` objects (a `<style>` tag rendered by the app is acceptable for pseudo-classes/animations).
- Assume no network. In some hosts http/https/ws are blocked by default; if the app genuinely needs fetch, call `requestNetwork()` once (see below).

## Injected globals (everything available — nothing else is)

- `React` (v18) and bare hooks: `useState`, `useEffect`, `useRef`, `useCallback`, `useMemo`, `useReducer`, `useId`.
- `useGlider(defaults?) -> { data, setData }` — THE persistence API. `data` is one JSON object that survives reloads and travels with the file. If `defaults` is passed, missing top-level keys are filled from it (stored values win). `setData(partial)` shallow-merges top-level keys: `setData({ items: next })` replaces `items`, leaves other keys alone. Keep `data` JSON-serializable. Use `useGlider` for anything worth keeping (logs, lists, scores); use plain `useState` for transient UI state (current input text, open/closed panels).
- `useMenu([{ label, onClick }, ...])` — adds items to the app's ☰ menu. Call it every render (top level of App, like a hook) with fresh closures. Good for infrequent actions like "Clear History" or "Reset".
- `setAppTitle(name)` — renames the app. Users can also rename in Settings, so call it from an event handler if at all, not unconditionally every render.
- `requestNetwork()` — asks a native host to offer the user a network permission toggle; no-op in browsers. Call once (e.g. in an effect) if the app uses fetch.

## Optional: Jet syntax

Default to plain JSX per the rules above unless told this app has Jet
syntax turned on (off by default). If it is, you may write app code in
Jet instead: a Swift-flavored alternative syntax with labeled arguments
and trailing closures.

- **Labeled arguments** fold into a props object on any call, not just
  elements: `div(class: "app", id: x)` means `div({class: "app", id: x})`.
- **Trailing closures**: `tag { ... }` appends a closure as the call's
  last argument — `div { ... }` compiles to `div(() => [...])`;
  `div(class: "x") { ... }` compiles to `div({class: "x"}, () => [...])`.
  The `{` must be on the same line as whatever precedes it, or it's just
  an ordinary block statement.
- **Builder bodies**: each statement in `{ ... }` becomes a collected
  child of the array the receiving function gets — reaching through
  `if`/`else`, `switch`, and `for...of`. `return` and every loop other
  than `for...of` (`while`, `do-while`, C-style `for`, `for...in`,
  `for await`) are compile errors inside one; assignments and `++`/`--`
  execute normally and are not collected. `__jet` is reserved inside
  builder bodies (it's the name of the compiled-in collection
  accumulator) — don't declare or reference a variable with that name
  there.
- Injected tags (only when the setting is on): one function per standard
  HTML tag plus `fragment` (`div`, `h1`, `button`, `ul`, `li`, …), same
  call syntax as any other Jet call — except `var`, a JS reserved word,
  which is not injected (use JSX `<var>` if you need that element). JSX
  still works too and can coexist with Jet in the same file — prefer one
  style per component.
- **Parenless-component wrinkle**: `Card { ... }` compiles to
  `Card(() => [...])` — the builder lands as Card's FIRST argument, since
  nothing else precedes it — while `Card(x: 1) { ... }` compiles to
  `Card({x: 1}, () => [...])`, where it's the SECOND. Any component you
  define must accept the builder wherever it lands. This only applies to
  components called directly (see next bullet for stateful ones).
- **Stateful components need `@component`**: calling a component directly
  (`Counter()`) runs its hooks in the CALLER's hook list — fine for a
  stateless helper, but state won't survive independently once it's called
  conditionally or more than once. Mark any component that calls a hook:
  ```jsx
  @component
  function Counter({ label }) {
    const [n, setN] = useState(0)
    return button(onClick: () => setN(n + 1)) { `${label}:${n}` }
  }
  ```
  Put `@component` immediately before the `function`/`const` declaration
  (module top level only). The compiler rewrites every call to `Counter`
  (`Counter()`, `Counter(label: "a")`, `Counter { ... }`, all forms) into a
  real mounted instance — write `Counter` as an ordinary React component,
  nothing else changes.

```jsx
function App() {
  const { data, setData } = useGlider({ count: 0 });
  return div(class: "app") {
    h1 { `Count: ${data.count}` }
    button(onClick: () => setData({ count: data.count + 1 })) { "+1" }
  }
}
```

## Theming

The shell sets CSS variables that follow the user's light/dark theme. Use them instead of hardcoded colors:

- Backgrounds: `--g-bg` (page), `--g-bg2` (cards/panels), `--g-bg3` (recessed)
- Text: `--g-text`, `--g-text2` (secondary), `--g-text3` (hints/disabled)
- Borders: `--g-border` (structural), `--g-border2` (interactive controls)
- Accent: `--g-accent` fill with `--g-accent-text` on it — for primary buttons/highlights only, never body text
- Semantic: `--g-danger`, `--g-error`, `--g-warning`, `--g-success`, `--g-info`, `--g-link`, `--g-highlight`

Visual conventions: flat solid fills (no gradients/translucency), 1px borders, 4–6px border radius, text-labeled buttons. Card pattern: `{ background: 'var(--g-bg2)', border: '1px solid var(--g-border)', borderRadius: '6px', padding: '1.25rem' }`. Center content with a max-width and give it generous padding; inputs/buttons inherit `fontFamily` and use `1rem` text.

## Shape of a typical app

```jsx
function App() {
  const { data, setData } = useGlider({ history: [] });
  const [input, setInput] = useState('');

  useMenu([
    { label: 'Clear History', onClick: () => setData({ history: [] }) },
  ]);

  const add = () => {
    if (!input.trim()) return;
    setData({ history: [{ text: input.trim(), at: Date.now() }, ...data.history] });
    setInput('');
  };

  return (
    <div style={{ padding: '2rem 1.25rem', maxWidth: '26rem', margin: '0 auto' }}>
      {/* cards, inputs, lists — styled with var(--g-*) */}
    </div>
  );
}
```

Errors are forgiving during development (compile errors banner over the last good version; runtime errors show an error screen), but ship code that renders sensibly from `useGlider` defaults on first run with empty data.

Working on Glider itself

Agents contributing to this codebase (rather than writing apps for it) should start from CLAUDE.md at the repo root and the long-form specs in docs/agent/reference/ — repo map and invariants, the app runtime API, the store format and save pipeline, and the host bridge contract. Those pages assume repo access and are deliberately not rendered here.