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:
- any items the app registered via
useMenu(e.g. "Clear History"); - 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;
- 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:
- Hangar bridge — inside Hangar, the file is written directly to the library; there's no download UI and saving is also automatic (see below).
- File handle — if you've done Save As… (Chrome/Edge), Save overwrites that same file in place from then on.
- 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
useGliderreads 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 nokindfield. A store withoutkind: "glider"is ignored and the file boots as a fresh default app.appIdgives 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;nulluntil then.codeis the raw JSX source (what the editor shows),datathe app's JSON blob,settingsthe 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 toindex.htmlthat are fetched before first render; the embeddedcode/dataare 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
hangarstorage 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
WKContentRuleListblocks http/https/ws. An app that callsrequestNetwork()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
typestrings likeglider-app,glider-library,glider-note(names kept for compatibility). - Richer app runtime — apps get
useGliderplus 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
useGliderand 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 nokindfield (Glider's sayskind: "glider") and holds many entities plus WM state; Aircraft persists to theglider-datalocalStorage 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.mdfirst.
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
typestrings,useGlider/GliderContext, package name) keep their historical "glider" names even where they belong to Aircraft — seeNAMING.mdbefore renaming anything. - Core version (
glider/src/version.js): bump the minor for store-shape or runtime changes that old files must survive; keepnormalize()instore.jsaccepting older minors; add (never replace) aGliderTemplate-<version>.htmlinhangar/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.mdin 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.