1# Jibb — an Elm-architecture UI language for Urbit
7Jibb is a Hoon dialect for Urbit frontends. `.jibb` files live in the desk under the `%jibb` mark; each desk's vendored Jibb compiles them, run by one shared host, so `|commit` is the only build step. Elm architecture, a small layout vocabulary, and three stages fixed by module kind:
9- **Build**, per commit: pages without data, as HTML, plus the stylesheet.
10- **Publish**, per request, in the host: pages with data, from a fresh scry. The host serves every page, to the owner only.
11- **Client**, in the browser: islands' `update`, `subs` and `view`, and stores, as JavaScript.
13A page without islands ships no JavaScript.
15**Goals**: no prebuild; familiar to Hoon programmers; a typed Elm architecture; Hoon's data end to end; no HTML or CSS outside the theme; static by default.
17**Non-goals**: agent logic, refinement types, transactions, JavaScript interop beyond Jibb's effects.
19*Name*: four letters like the vanes, after the jib, a boat's front sail.
23Everything needed exists. The one limit: marks can't follow imports, so the host compiles.
25| Mechanism | Gives Jibb | Limit |
27| [Marks](https://docs.urbit.org/urbit-os/kernel/clay/marks) | `%jibb` stores text, merged by line | A conversion can't scry, so can't follow imports |
28| Clay | The host reads each desk's `/ui`, static files included, and hears of commits | Every version of every file is kept |
29| Gall | One host agent serves every Jibb desk | Agent names are ship-wide |
30| [Eyre `%set-response`](https://github.com/urbit/urbit/releases/tag/urbit-os-v2.139) | Responses without an Arvo event | Keyed by the full request target; entries outlive their agent; auth-flagged entries give non-owners a bare 403 |
31| [Eyre noun channels](https://github.com/urbit/urbit/pull/6472) | Pokes and watches as nouns | Needs `jam`, `cue`, `@uw` and an event stream over `fetch` in the browser |
32| [Docket](https://docs.urbit.org/build-on-urbit/userspace/dist/docket) | `site+/path`: an agent-served site | One `site` or glob clause per desk |
36| Project | What it is | Jibb takes |
38| [Sail](https://docs.urbit.org/hoon/sail) | Hoon's HTML markup | Rendering in the agent |
39| [Udon](https://docs.urbit.org/hoon/udon) | Markdown-like source under its own `%udon` mark, compiled by `;>` | A source language under its own mark |
40| [Rudder](https://github.com/Fang-/suite) | Routing for Sail pages | Page routing |
41| [Mast](https://github.com/R-JG/mast) | Display state in the agent, HTML diffs over a channel | One generic client script; channels as transport |
42| [Docket](https://docs.urbit.org/build-on-urbit/userspace/dist/docket) | One agent serving every desk's front end | One shared host |
43| [Elm architecture](https://guide.elm-lang.org/architecture/) | `Model`, `Msg`, `update`, `view`, [effects](https://guide.elm-lang.org/effects/), [keyed nodes](https://guide.elm-lang.org/optimization/keyed.html) | The program model, effects as data, keyed diffing in `each` |
44| [elm-ui](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/) | Layout without HTML or CSS | Rows, columns, `el`, wrapping, paragraphs, padding, fill |
45| [elm-pages frozen views](https://elm-pages.com/docs/frozen-views/) | `Html Never` content kept out of the bundle | `(element !!)` |
46| [Astro islands](https://docs.astro.build/en/concepts/islands/) | HTML by default; marked components hydrate | Islands |
47| [Qwik resumability](https://qwik.dev/docs/concepts/resumable/) | State serialised into HTML; the client resumes | A jammed model; `init` never runs in the browser |
48| [@urbit/nockjs](https://github.com/urbit/nockjs), [@urbit/aura](https://github.com/urbit/aura-js) | Nouns and aura printing in JavaScript; aura-js lacks `@dr` | References for Jibb's own codec |
52Clay stores files; each desk's compiler, run by the shared host, places each module at its kind's stage; only island and store code reaches the browser.
54[embedded content: Jibb pipeline · source to browser in three stages\]
58Hoon's runes and literals in an indentation layout, over Hoon's nouns and molds, so pokes, facts and scries need no conversion. The **Grammar** tab defines it.
60### Importing Hoon types
62`/-` and `/%` translate each mold a module uses from `/sur` files and marks:
66| `$%`, `$?` | A union if its cases differ as nouns, else an error where used |
67| `$@` | A union with an atom case |
68| `$^` | A union if its cases differ as nouns, else an error where used |
70| `map`, `set`, `qeu`, `jar`, `jug` | The same, by name, checked with `apt` on arrival |
71| `mop` | `(mop K V)`, read with `on` |
72| Other `$\|` | Its base type |
73| `%0`, `%&`, `%\|` | Tags |
74| Recursive molds, such as `json` | Recursive, matched by name |
75| Records with duplicate field names; cores, gates, vases, signed and float auras | An error where used |
77Unused molds are skipped, so unsupported constructs elsewhere in a `/sur` file are fine. A union without a name, as a field's `?(%approve %deny)`, imports too, though Jibb can't write one. Messages and printed types show its cases separated by spaces, as `%approve %deny`; this notation is print-only.
81Hoon's standard library regrouped, written in Jibb and vendored as `/lib/jibb/prelude`; an island's bundle carries only what it uses. The exported inventory includes all names from the Hoon library sections listed below, excluding the groups explicitly left out. Jetted prelude functions have working Jibb implementations; the compiler recognizes their shapes and uses the Hoon versions so jets can fire. They may take gates as arguments (Core language). Private helpers and low-level primitives live in a separate internal prelude module, imported without exposing them; foundational public gates are implemented in Jibb on top of those primitives. This source-backed internal module defines `poke`; the public prelude re-exports it as an ordinary transparent type, so ordinary modules can construct and inspect its values. The prelude also holds `eq`, `not`, `page`, `program`, `store`, `pure`, `island`, and the layout elements and molds.
83| Group | Hoon | Jibb |
85| Arithmetic | [1a](https://docs.urbit.org/hoon/stdlib/1a), [2g](https://docs.urbit.org/hoon/stdlib/2g) | Jibb gates over shared atom-constrained type variables, using private low-level primitives: `add`, `sub`, `div`, `mod`, `gth`, `max`, `pow` |
86| Units | [2a](https://docs.urbit.org/hoon/stdlib/2a) | `fall`, `need`, `bind`, `biff`, `clap`; `clap` takes two units of the same type and a gate combining two values of that type into one of that type |
87| Lists | [2b](https://docs.urbit.org/hoon/stdlib/2b) | `turn`, `weld`, `snag`, `lent`, `sort`, `skim`; `(roll xs init f)` takes a start |
88| Maps, sets, queues | [2h](https://docs.urbit.org/hoon/stdlib/2h)–[2m](https://docs.urbit.org/hoon/stdlib/2m), [2o](https://docs.urbit.org/hoon/stdlib/2o) | Plain gates over Hoon's trees, such as `(get.by m k)`; `rep.by` and `rep.in` take a start; ordered maps through lull's `on`, as `(get.on lte m k)` |
89| Function helpers | [2n](https://docs.urbit.org/hoon/stdlib/2n) | `same`, `head`, `tail`, `cork`, `cury` |
90| Text | [4b](https://docs.urbit.org/hoon/stdlib/4b) | `crip`, `trip`, `cass`, `cuss`, `trim`, `tufa`, `tuba` |
91| Atom printing | [4a](https://docs.urbit.org/hoon/stdlib/4a), [4j](https://docs.urbit.org/hoon/stdlib/4j)–[4m](https://docs.urbit.org/hoon/stdlib/4m) | `scot`, `scow`, `slav`, `slaw`. `print-date` prints Urbit-style in the theme's zone, to the minute, as `~2026.10.29..14.30`, from a zone table jibb-dev keeps current. Numbers print as Hoon does, as `1.000` |
92| Time | [3c](https://docs.urbit.org/hoon/stdlib/3c) | `yore`, `year` |
93| Data molds | [1c](https://docs.urbit.org/hoon/stdlib/1c), [2q](https://docs.urbit.org/hoon/stdlib/2q), [3g](https://docs.urbit.org/hoon/stdlib/3g) | `unit`, `list`, `pair`, `tape`, `path`, `coin`, `dime` and the rest; Hoon's 3g `tone` is `tone.hoon`, distinct from the styling type, and its 1c `step` is `step.hoon` |
94| Bits | [2c](https://docs.urbit.org/hoon/stdlib/2c), [2d](https://docs.urbit.org/hoon/stdlib/2d) | Jibb's `not` negates a loobean; Hoon's bitwise `not` is exposed as `not.bit` |
95| Hashing and ordering | [2e](https://docs.urbit.org/hoon/stdlib/2e), [2f](https://docs.urbit.org/hoon/stdlib/2f) | Natives behind maps and sets; `aor` exposed |
96| Signed and floats | [3a](https://docs.urbit.org/hoon/stdlib/3a), [3b](https://docs.urbit.org/hoon/stdlib/3b) | Left out; `@s…` and `@r…` auras are errors, imported ones included |
97| Parsers | [4d](https://docs.urbit.org/hoon/stdlib/4d)–[4i](https://docs.urbit.org/hoon/stdlib/4i), parser helpers in 4j and 4l | Left out, except the explicitly listed `slav` and `slaw` |
98| The rest | [1b](https://docs.urbit.org/hoon/stdlib/1b), [2p](https://docs.urbit.org/hoon/stdlib/2p), [3d](https://docs.urbit.org/hoon/stdlib/3d), [3f](https://docs.urbit.org/hoon/stdlib/3f), [4c](https://docs.urbit.org/hoon/stdlib/4c), [4n](https://docs.urbit.org/hoon/stdlib/4n), [4o](https://docs.urbit.org/hoon/stdlib/4o), [5a](https://docs.urbit.org/hoon/stdlib/5a)–[5f](https://docs.urbit.org/hoon/stdlib/5f) | Left out; `jam`, `cue` and `fein` run inside the runtime |
100The prelude's `(like A)` mold describes a result with a product of type `A`. Its value gate `(like convert result)` maps a present product with `convert=$-(A B)` and returns `(like B)`, preserving the position and remaining input; an absent result stays absent.
102Maps are Hoon's trees on both sides, so equal maps are equal nouns. Requests check incoming maps and sets with `apt`.
104## The Elm architecture on Urbit
106A module's kind is the type of its `main`:
108| Kind | `main` | Runs | Ships JavaScript |
109| --- | --- | --- | --- |
110| Page | `(element !!)` | At commit | Only its islands' |
111| Page with data | `(page load view)` | In the host, per request | Only its islands' |
112| Island | `(program init update view subs)` | `init` and the first view with its page; the rest in the browser | Yes |
113| Store | `(store init update)` | In the browser, shared by a page's islands | With its islands |
115A module without `main` is a library. The host reads a few other names per file kind, listed in the Grammar tab. Pages embed islands as `island main.todo-list ts`, an `(element !!)`:
117- `main.todo-list` is a record of exactly `init`, `update`, `view` and `subs`, whose types fix the data, model and messages. `subs` is required, as in Elm's `Browser.element`; `|=(m ~)` means none.
118- `ts` is the island's data, checked against `init`'s sample.
119- `(element !!)` means no messages reach the page. It also keeps islands out of islands, since an island's view is an `(element M)`.
121`init` runs on the ship at the page's stage, and the first view renders into the page with the model jammed in. On start, the browser runs `view` on that model, binds the result to the server's nodes by key, and replaces the subtree if they differ. Island data and models can't hold gates; the checker names the field.
123**Loading.** A page with data renders in the host per request, from a fresh scry.
125- `load`, `page`'s first argument, maps route parameters to one `ask`: `++ load |=(~ (todos.todo same))` without parameters.
126- The host scries the ask's path per request, so the agent must answer a peek there; nothing is watched or kept. A path case can name a separate scry path.
127- Pages read full state only; folds matter only to islands.
128- Route parameters are `load`'s sample, bound by name and parsed by aura: `/ui/blog/_slug.jibb` has `++ load |=([slug=@ta] (post.api slug same))`.
129- `(pure x)` is an ask without an agent; an app's catch-all page passes its path with it.
130- A rejected path is a 404.
132Loads and renders run in Nock.
134**Access.** Every page is private: the host serves it per request, only to the owner, and sends anyone else to `/~/login?redirect=…`. Public pages are left for a later version.
136**Errors.** A rejected path, an unparseable parameter or a crashing render gets Jibb's error page, 404 or 500; no error is cached. A control rendered outside an island is rejected: at commit for pages without data, per request otherwise. There's no step limit yet, so an endless render stalls the ship, as in Hoon.
138- A crashing island shows the standard crash notice: in the page's HTML for `init` or the first view. In the browser, a crash in `update`, `view` or `subs` drops the message and keeps the model, DOM and watches.
140**Effects.** Commands and subscriptions are library values:
144 [%poke p=raw-poke reply=$-((unit @t) M)]
146 [%wait gap=@dr msg=M]
147 [%now reply=$-(@da M)]
148 [%who reply=$-(@p M)]
151 [%every gap=@dr reply=$-(@da M)]
152 [%url reply=$-(path M)]
155An island's `update` returns `[model (list (cmd M))]`, so one message may produce several commands. The runtime processes the returned commands one at a time, in list order. A store's `update` consumes exactly one poke with the current state and returns the next state.
157- `raw-poke`, `ask` and `qath` are transparent types defined in the internal prelude and re-exported normally. Derived gates and `pure` also construct requests. A derived poke helper such as `(poke.todo [%add draft.m] |=%acked)` checks its value and supplies the agent and mark, returning a command whose reply becomes an island message, so one island can poke several agents. The agent's mark rechecks on the ship; a nack's `tang` reaches the reply as text. Store helpers poke the store in the browser.
158- A request such as `(todos.todo |=%got)` comes from a declared path or a store's `state`; its reply always gets full state. A path with diffs also takes a fold, `(changes.todo |=%got apply)`, which the runtime applies first. Facts are checked; a bad one crashes the reply, which counts as a crashed `update`.
159- Islands read agents only by watching. A watch's first fact must be full state, so watching a request whose path case has a separate `scry=` path is a type error in an island. The checker must prove a request is watchable; unknown watchability is also a type error. The runtime acks facts, rewatches after a kick or a dropped connection and reseeds from the first fact; a refused watch shows the crash notice.
160- Watches match by agent and path, timers by gap, and the newest reply is always used. After each `update`, `subs` is diffed to start and stop watches and timers. `%now` and `%every` give the time, `%who` the owner's `@p` from `/~/name`; `%wait` is a one-shot delay.
162Inputs send a message per `input` event, and one per IME composition, so validation runs as you type; only commands reach the ship. After every event, controls reset to the latest view, so a toggle whose poke fails unchecks. A field's text is rewritten only when it differs, which keeps the caret. Fields are read-only until their island starts.
166- `init` runs on the ship with the page's data and returns only the model, jammed into the page; the browser resumes from it at start. A reload starts from the model the page is served with.
167- A crash in `update` drops the message, keeps the model and shows the standard notice.
168- Stores share state between a page's islands. A store module, written like an API file, has `state`, `+$ poke` and `++ main (store init update)`, where `update` applies a poke. The store's local `+$ poke` is an ordinary action mold; the runtime envelope is `raw-poke`. The builder is `+$ store |$ [S] [init=S update=*]` with `++ store |= [init=S update=$-([A S] S)] ^- (store S) [init update]`: it checks action `A`, while `main` exposes only state `S`; the store kind check verifies `A` against the declaration. Islands import it with `/=` and use its derived gates: the generic `poke` command helper, and `(state.cart |=%got)`, whose first fact is the current state and each later fact the next.
169- A store runs in the browser from its `init` when its first island starts, and lasts as long as the page. Islands keep what they watch in their own models.
170- An app is a page whose view is one island under a catch-all route; its `load` passes the path with `pure`. The island's `%url` subscription delivers the path at start and after every change, back and forward included. `go` pushes history, and `go` or `link` within the section changes the island without a reload. Focus then moves to the new view's first heading, which screen readers announce.
174Elements are ordinary gates with their own signatures. Containers take attribute lists, and `el` adds attributes to anything. Spaces and colours are theme tokens. Text elements come from `text`, as `(text "hi")`. The attribute union is `attr`, so helpers can declare `^- (list attr)`.
176| Element | Arguments | Purpose |
178| `row`, `column`, `wrap` | `attrs kids` | Containers |
179| `stack` | `attrs kids` | A column, then a row from the `wide` breakpoint up |
180| `el` | `attrs kid` | Adds attributes; `el`'s values win |
181| `para` | `kids` | Flowing text |
182| `text` | A cord | Text |
183| `heading` | `level words` | `<h1>` to `<h6>` |
184| `each` | `(list [key=K el=(element M)])` | Keyed children; repeated keys match in order |
185| `table` | Columns `(list [header=@t width=length align=align])`, rows `(list [key=K cells=(list (element M))])` | A table with keyed rows |
186| `image`, `image-unsafe` | Application path and description; `[url=@t width=@ud height=@ud]` and description, respectively | Boxed to its aspect ratio before loading, never wider than its container; static images are sized from the file; `""` marks it decorative |
187| `link`, `link-unsafe` | Application path and label; unchecked cord and label, respectively | See Links and URLs |
188| `button` | `label msg` | A control |
189| `toggle` | `on label msg` | A control |
190| `field` | `label value reply` | A control; `reply` turns its text into a message, as `\|=%typed` |
191| `form` | `msg kids` | A control; Enter in a field sends the nearest form's `msg` |
192| `island` | `program data` | Embeds an island |
194Controls work only in islands; the host rejects a page that renders one outside an island.
196| Attribute | Values |
198| `pad`, `gap` | `%s0` to `%s3`; `gap` on containers only |
199| `width` | `[%chars n]`, `[%fill n]`, `%shrink`; `[%fill n]` below 1 counts as 1 |
200| `align` | `%start`, `%center`, `%end` |
201| `role` | `%body`, `%caption`, `%label` |
202| `tone` | `%normal`, `%muted`, `%accent`, `%danger`; danger is also marked `!` and announced |
204The same attribute twice in a literal list is an error; otherwise the last wins.
206- Rows lay out left to right, centred vertically; columns top to bottom. Children take their content's width unless they fill.
207- `[%fill n]` shares leftover space in proportion to `n`, `%shrink` takes the content's width, and `[%chars n]` is `n` widths of `0`.
208- Empty elements take no room and no gap.
209- Nothing exceeds the page's width: long words break, and wide content scrolls in its own box.
211The theme, `++ theme` in `/ui/theme.jibb`, uses the prelude's `theme` mold, with a default for desks without one. It requires `s1` to `s3` (`s0` is zero), five tones, with `normal` the text and `background` the page colour, the page's language, and the time zone dates print in. `wide` is the breakpoint where `stack` becomes a row. Dark tones go under `dark`, used when the reader prefers dark. An optional `font` is an application path to a static font. Tokens become CSS custom properties and `wide` a media query; values must be CSS lengths or colours, and each tone needs 4.5:1 contrast with its background.
215 [ space=[s1="4px" s2="8px" s3="16px"]
216 tones=[normal="#222" background="#fff" accent="#17734f" danger="#b3261e" muted="#5f5f5f"]
220 font=`%/static/fonts/inter.woff2
221 dark=`[tones=[normal="#eee" background="#111" accent="#4c9" danger="#f28b82" muted="#aaa"]]
224Style attributes compile to atomic classes in one stylesheet built from the theme, so islands can't compute a missing class. `[%chars n]` and `[%fill n]` become inline styles.
228Both backends escape all text and attributes. In-app links are application paths, `%/blog` or `%/blog/(slug.p)`, checked at compile time against routes and static files; parameters print by aura, and the base path is implicit. They carry no query or fragment, so apps keep their state in the path.
230Links, images and navigation also accept two kinds of unchecked cord, through declarations separate from the application-path forms: a root-relative path to another app on the ship, such as `"/apps/groups"`, and an `https`, `mailto` or `tel` URL. `link-unsafe`, `image-unsafe` and `go-unsafe` are the unchecked forms. Both navigation declarations produce the `[%go url=@t]` command; `go` converts the application path to a cord, including the app's base path. A cord starting `//` or `/\` is neither. Any other cord, such as `javascript:`, is drawn as text, and `go` ignores it. External links open in a new tab with `noopener`. Pages send a Content-Security-Policy allowing scripts only from the ship.
234A module's kind fixes where it runs; nothing infers stages.
236| Stage | Runs | Where | Output |
237| --- | --- | --- | --- |
238| Build | Pages without data | The host, per commit | HTML and the stylesheet |
239| Publish | Pages with data | The host, in Nock, per request | HTML |
240| Client | Islands' `update`, `subs` and `view`; stores | The browser, in JavaScript | DOM patches from a keyed diff |
242- **Islands follow their page**: `init` and the first view run wherever the page renders, and the model is jammed into the HTML.
243- **Re-render, then diff**: each message that changes the model re-runs `view`, and a virtual-DOM diff patches the DOM. Children of `each` are keyed, as in Elm's `Html.Keyed`, so focus survives reordering.
244- **Functions have no stage**: a gate compiles for whichever side calls it.
245- **No JavaScript needed**: pages render without it, and islands show their server-rendered state, with controls `aria-disabled` and fields read-only until they start.
246- **Holes later**: compiling views to static HTML plus small functions of the model is an optimisation that changes no types or errors, so it waits.
250`%jibb` stores source as text and nothing else. Conversions can't follow imports, so the host compiles, and `|commit` never loads the compiler.
258 ++ mime |=([p=mite q=octs] q.q) :: from Unix on |commit
260 ++ txt of-wain:format
264 ++ mime [/text/x-jibb (as-octs:mimes:html src)]
265 ++ txt (to-wain:format src)
268++ grad %txt :: line diffs and merges, as %hoon does
272- Text, like `%hoon`: `|commit` never fails on a half-written file, and Clay merges by line.
273- Compiled modules are cached by their hash, their imports' hashes, Hoon files included, and the desk's compiler, so a commit recompiles only what changed. Import cycles are errors.
274- Relinking prints each error with its file, line, column and an excerpt.
275- Each app desk vendors `jibb-dev`, as with `base-dev`: compiler, prelude, browser library, and the `%jibb` and static-file marks. It also needs base-dev's `txt`, `txt-diff`, `js`, `bill` and `kelvin` marks, docket's `docket-0`, and a `sys.kelvin`. The `%jibb` desk holds only the shared host and its threads.
279On every commit, the host reads each Jibb desk's `/ui`, and that desk's compiler rebuilds what changed and relinks, checking modules in dependency order and resolving `/-` and `/%` through Clay. There's no other build path: marks can't follow imports, and `/~` builds only `.hoon` files.
281The host is one Gall agent on the `%jibb` desk, serving every Jibb desk, so app agents stay plain Hoon; it never reads their state. A desk opts in by vendoring Jibb and naming its base path in docket's `site+`. The host is thin: it binds paths, watches desks, caches and serves. Each desk's Jibb builds and renders everything else, HTML shell, CSP and error pages included, through a few entry points fixed across releases, so desks upgrade independently. The host clears a desk's cache entries when it's suspended or uninstalled, and accepts pokes only from the ship.
285Each agent Jibb talks to has `/ui/api/<agent>.jibb`, declaring its pokes in `+$ poke` and its paths in `+$ paths`, over types from the agent's own marks through `/%`. Importing it gives derived gates: a generic `poke` command helper, which checks the action and reply and pokes with the action's mark, and a request per path, which checks what comes back. Clashing derived names are errors.
298 [%todos /todos todos]
299 [%changes /changes (facts todos todo-update)]
300 [%todo-by-id /todo/[id=@ud] todo-item]
303A path case is a request name, a path and a type. A plain mold is full state, which a watch's first fact must carry. `(facts full diff …)` adds diffs, told apart by mark, and its request takes a fold, here `$-([[%todo-update todo-update] todos] todos)`. Pages scry the path, so the agent must answer a peek there. A case can name another scry path, as in `[%changes /updates (facts todos todo-update) scry=/todos]`, used only by pages.
305`poke` and `paths` are declarations, not molds. An API module re-exports its `/%` types, not `/-` faces or `/=` modules.
307Any mark name works: `/% %todo-action-1` gives the type `todo-action-1`, poked with `%todo-action-1`. Helper molds such as `item` come from the `/sur` file through `/-`. The marks are the agent author's:
310:: /mar/todo-action.hoon, written with the agent
313++ grab |% ++ noun action:todo --
314++ grow |% ++ noun a --
319Jibb calls this type `todo-action`, after its mark. Jibb's own types never cross to the ship, so they need no marks.
321### Routing by file path
323Pages route by their path under `/ui`; islands, stores and libraries are only imported.
327| `/ui/index.jibb` | `/apps/my-app` |
328| `/ui/blog/index.jibb` | `/apps/my-app/blog` |
329| `/ui/blog/_slug.jibb` | `/apps/my-app/blog/{slug}`; `_` marks a parameter |
330| `/ui/settings/__rest.jibb` | `/apps/my-app/settings` and below; `__` marks a catch-all, passed to `load` as a path |
332- One URL per page, with no trailing slash; other forms and query strings get the same page.
333- A literal beats a parameter, which beats a catch-all. Two parameters at one level, `blog.jibb` beside `blog/index.jibb`, and a catch-all beside `index.jibb` are errors. Folders can be parameters: `/ui/_user/posts.jibb`.
334- Parameters bind to `load`'s sample by name, which must have exactly those fields; a catch-all's is a `path`.
335- `/apps/my-app/~jibb/` and `/apps/my-app/static/` belong to the host.
337Typed segments share one notation across routes, application paths and agent paths. `@ta` and `@tas` are raw text, checked as knots, as `/blog/my-post`; other auras print and parse as Hoon does, so `@ud` is `1.000`. The host splits the raw URL, so dots stay inside segments; a segment that doesn't parse is a 404. `%/todo/(id)` and `[%todo-by-id /todo/[id=@ud] todo-item]` print the same way.
343/mar/jibb.hoon /mar/png.hoon … marks from jibb-dev
344/lib/jibb.hoon /lib/jibb/prelude compiler and prelude from jibb-dev
345/lib/jibb/runtime/js browser library from jibb-dev
346/mar/txt.hoon /mar/js.hoon … marks from base-dev
347/mar/docket-0.hoon docket's mark from landscape
348/sys.kelvin the desk's kelvin
349/sur/todo.hoon /mar/todo-*.hoon the %todo agent's types and marks
350/app/my-app.hoon your agent, unchanged
351/ui/api/todo.jibb the %todo agent's API
352/ui/index.jibb /ui/blog/_slug.jibb pages
353/ui/todo-list.jibb an island
354/ui/stores/cart.jibb a store
356/ui/static/img/logo.png static files
357/ui/tests/todo.jibb tests: each test- arm is one test
359desk.docket-0 site+/apps/my-app, with docket's other clauses
361:: the %jibb desk, installed once per ship
362/app/jibb.hoon the shared host
363/ted/clear.hoon /ted/test.hoon -jibb!clear, -jibb!test
366The `%todo` and `%blog` agents run on their own desks; this desk copies their `/sur` and `/mar` files.
368## Client runtime and Eyre
370Islands compile to small JavaScript modules sharing one hand-written library; pages without islands load neither.
372- **Two backends.** Each module lowers to the core language (Core language), which both backends compile: Nock on the ship for rendering and `init`, with natives calling jets, and JavaScript in the browser for `update`, `subs` and `view`. Differential tests under Node, from jibb-dev, keep them in agreement.
373- **Shared library**, under 20 KB: its own noun codec (`jam`, `cue`, `@uw`, treaps, aura printing), the channel client, a keyed DOM differ, stores and URL subscriptions. It reads events over `fetch`, since `EventSource` can't send `x-channel-format`, deletes its channel on `pagehide`, and opens a fresh one when a reconnect gets a 404.
375### What a page with an island looks like
381 <meta charset="utf-8">
382 <meta name="viewport" content="width=device-width, initial-scale=1">
383 <title>My ship</title>
384 <link rel="stylesheet" href="/apps/my-app/~jibb/s/0v1a2b3.css">
388 … server-rendered HTML …
389 <div data-jibb-island="0v4c5d6">
391 <script type="application/x-jibb-state">… jammed model, @uw-encoded …</script>
394<script type="module" src="/apps/my-app/~jibb/lib.0v7e8f9.js" async></script>
399The title is the first `heading 1`, and the language comes from the theme. `data-jibb-island` names the module by hash, so a shared island loads once; each instance keeps its state block in its own element. Every island starts as soon as the library loads.
403- The host binds each desk's base path, from its `site+` clause, with `%connect`.
404- The host serves every page itself, per request and only to the owner, sending anyone else to the login page; no page enters Eyre's cache.
405- CSS, the library, island modules and static files sit at content-hashed, immutable paths, cached with `%set-response` and served without an Arvo event. They're public, since only pages carry data, and each build's files stay a day after replacement so open tabs keep working.
406- A commit re-renders every page without data, which the host then serves from its copy.
407- The cache is ship-wide and outlives its agent. The host caches only under each desk's base path and clears a desk's entries on suspend or uninstall; run `-jibb!clear` before uninstalling `%jibb`.
408- Cache hits skip Eyre's other handling, such as HTTPS redirects and CORS.
412Images and fonts live under `/ui/static/`, committed with the desk, so installers get them; jibb-dev's marks store them as they are.
414- Pages refer to them by application path, as `(image %/static/img/logo.png "The logo")`. A missing file is a compile error, and images are sized from the file.
415- Each file is served from Eyre's cache at a URL with a short hash of its bytes, such as `/apps/my-app/static/0v1.7rq2a/img/logo.png`, marked immutable; a changed file gets a new URL.
416- Clay keeps every version, and cache hits ignore `Range`, so audio, video and large image sets stay out for now.
422### 1. Static: `/ui/about.jibb`
428 column ~[width+[%fill 1] pad+%s3 gap+%s2] ~[
429 heading 1 "About this ship"
430 para ~[(text "Hosted on Urbit. This page ships no JavaScript.")]
431 el ~[tone+%accent] (link %/blog "Read the blog")
434An `(element !!)`: build stage, one HTML file plus the stylesheet, rendered on commit, titled "About this ship".
436### 2. Published: `/ui/blog/index.jibb`
441/= card /ui/components/card
443++ main (page load view)
444++ load |=(~ (posts.api same))
446++ view |= ps=(list post.blog)
447 column ~[gap+%s2] (turn ps card.card)
450A `page`: each request scries `%blog` and renders in Nock.
452### 3. Interactive: `/ui/todo-list.jibb` and `/ui/index.jibb`
457++ main (program init update view subs)
459+$ model [todos=(list item.todo) draft=@t error=(unit @t)]
464 [%got todos=(list item.todo)]
465 [%acked err=(unit @t)]
467++ init |= ts=(list item.todo)
468 [todos=ts draft="" error=~]
470++ update |= [=msg m=model]
473 ?- (gth (lent (tuba (trip t))) 80)
474 %.y [m(draft=t error=`"Too long") ~]
475 %.n [m(draft=t error=~) ~]
476 %submit [m(draft="") ~[(poke.todo [%add draft.m] |=%acked)]]
477 [%toggled id] [m ~[(poke.todo [%toggle id] |=%acked)]]
478 [%got ts] [m(todos=ts) ~]
479 [%acked err] [m(error=err) ~]
482 ~[[%watch (todos.todo |=%got)]]
487 each (turn todos.m row-of)
489 el ~[width+[%fill 1]] (field "New todo" draft.m |=%typed)
491 el ~[tone+%danger] (text (fall error.m ""))
493++ row-of |= t=item.todo
494 [id.t (toggle done.t text.t [%toggled id.t])]
499/= todo-list /ui/todo-list
501++ main (page load view)
502++ load |=(~ (todos.todo same))
504++ view |= ts=(list item.todo)
507 island main.todo-list ts
510| Part | Stage | Becomes |
512| The page's view, "My ship" | Publish | HTML from a fresh scry per request, served only to the owner |
513| The island's `init` and first view | Publish, with its page | HTML in the page, with the jammed model |
514| `update`, `subs`, `view` | Client | A JavaScript module, loaded with the page; model changes re-run `view` and patch the DOM |
515| `[%add …]`, `[%toggle …]` | Ship | One poke each: an Arvo event per command, not per keystroke |
519A desk becomes a Jibb app by vendoring jibb-dev and naming its base path in docket, here `site+/apps/my-app`. The host on the `%jibb` desk binds it, builds `/ui` with the desk's compiler and serves it. Install `%jibb` first; until then the tile opens Eyre's 404. The app's agent, `/app/my-app.hoon`, is unchanged.
523- [Marks](https://docs.urbit.org/urbit-os/kernel/clay/marks) and [fas runes](https://docs.urbit.org/hoon/rune/fas), docs.urbit.org
524- [Sail](https://docs.urbit.org/hoon/sail) and [Udon](https://docs.urbit.org/hoon/udon), docs.urbit.org
525- [Docket files](https://docs.urbit.org/build-on-urbit/userspace/dist/docket), docs.urbit.org
526- [urbit-os v2.139 release: Eyre cache and noun channels](https://github.com/urbit/urbit/releases/tag/urbit-os-v2.139); [eyre: add cache, PR #5927](https://github.com/urbit/urbit/pull/5927); [eyre: noun channels, PR #6472](https://github.com/urbit/urbit/pull/6472)
527- [Publication caches for Eyre](https://gist.github.com/Fang-/1939a5b51fb2f821d960e05b74ad5f9f), an earlier design proposal; Eyre shipped `%set-response` instead
528- [Mast](https://github.com/R-JG/mast), [Fang-/suite (Rudder)](https://github.com/Fang-/suite)
529- [Elm architecture](https://guide.elm-lang.org/architecture/), [effects](https://guide.elm-lang.org/effects/) and [keyed nodes](https://guide.elm-lang.org/optimization/keyed.html); [elm-ui](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/)
530- [elm-pages frozen views](https://elm-pages.com/docs/frozen-views/), [Astro islands](https://docs.astro.build/en/concepts/islands/), [Qwik resumability](https://qwik.dev/docs/concepts/resumable/)
531- [@urbit/nockjs](https://github.com/urbit/nockjs), [@urbit/aura](https://github.com/urbit/aura-js)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list