packages

Package what a page needs

A package may change the theme, add one lf-* element, carry a related widget family, supply browser behavior and libraries, or carry guidance for the roles using the page. page init combines the kernel and packages, checks the result, and vendors one layer into the page. Leaf's content defaults use the same contract. The bundled default/ package is a worked example of the directory described here.

One contract, several roles

Role Directory Initialize
Selected independently packages/<name>/, or any directory leaf package init PACKAGE
Current project .leaf/ leaf package init .leaf
Every project for this user $XDG_CONFIG_HOME/leaf/, or ~/.config/leaf/ leaf package init ~/.config/leaf

A package may contain any subset of this layout. One widget and a theme-only package use the same contract:

package/
├── registry.json       widget entries and shared $ declarations
├── theme.css           rules appended to the cascade
├── guidance/           Markdown guides named for their audiences
├── runtime/            private modules and replacements
├── widgets/            entry modules and private helpers
├── vendor/             third-party libraries or data
├── icon.svg            optional replacement
└── leaf.js             optional runtime replacement

package init creates the registry, theme, and contribution directories without replacing existing contents. After editing any part, package check validates the directory through the same composition gate that page init uses.

leaf package init packages/callout
leaf package check packages/callout

Run project commands from the project root. During page init, the kernel and bundled default package load first, followed by explicit packages in command order, the user package, and the project package. Theme files and same-audience guidance join in that order. A later registry entry or same-named runtime, widget, or vendor file replaces the earlier one; members of a shared $ entry compose individually. Files may be symlinked, but package and page-owned paths must resolve separately so one scope can never overwrite another.

Files directly under guidance/ are named for their audience. A filename matches [a-z][a-z0-9-]*\.md; examples include author.md, reviewer.md, and worker.md. Packages define the audience names; Leaf does not keep a role list. leaf page guidance PAGE lists the vendored audiences, and leaf page guidance PAGE AUDIENCE prints one. The author guide also follows the merged vocabulary in page catalog.

--package accepts a project-relative or ~ package path. It says nothing about widget count. Leaf records explicit paths under $layer.packages, so a plain re-init preserves their order. Absolute paths are refused because the vendored registry is public. page init --no-packages clears all explicit packages.

Adjust the theme

A package's theme.css loads after the kernel and default package rules. The bundled file remains intact, so a package can state only what it changes.

Token overrides change every surface that consumes them. Ordinary selectors can tune one element or widget.

:root {
  --accent: #7c3aed;
  --r: 3px;
  --serif: "Iowan Old Style", Georgia, serif;
}

lf-chip {
  border-radius: var(--r);
}

A rule that draws an inset — padding, a border, a tinted field — says --lf-frame: 1 in the same declaration. Without it the box shows the inset it stated plus whatever room its first and last blocks reserved for neighbours they haven't got, so a card stating 16px comes out 16 above its title and 29 below its last paragraph, and the difference moves with what the author wrote inside. The declaration also decides what holds a wide widget: an x-wide element takes the page's room standing in the page's own flow, and the width of the box it stands in where that box declares the frame. The shipped theme states both once for every layer; a box opts in where it draws the frame, and version check --render reports one that hasn't.

lf-note {
  padding: var(--sp-3);
  background: var(--field);
  --lf-frame: 1;
}

An answerable lf-options group is cells sharing hairlines rather than boxes standing apart, so the line between two cells is the whole of what separates them, and each cell holds its own words off the frame. A gap or a margin added between them is a second way to say what the line already says, and what it draws is a rule floating in a band of nothing. Tune what a cell holds — its inset, its fill, its type — rather than the room between cells.

Three facts a rule cannot read off the element in front of it are declared in the registry instead, and the runtime paints each onto the page where a selector reaches it. data-lf-exhibit marks quoted material — an element whose entry declares x-exhibit — and data-lf-inline marks a widget set among the words around it; both travel wherever the widget renders, a reply included. data-lf-wide holds the width kind an x-wide entry declared, and only in the document, the room it hands out being the document's. Which medium reads which differs: an exported copy still lays a wide widget out and still asks whether a change's slots hold blocks, while the rules answering data-lf-exhibit are the live page's alone — a copy can take no pick, so it is handed no offer to withhold in the first place.

The rules that draw a shipped question as something to answer read the first, so a group quoted inside an exhibit is drawn as one that was never offered. Anything your own package draws as pressable stands down there by asking the same thing — for a widget you have given a module to, since a hand over something that answers nothing is the lie these rules exist to prevent. A widget from any layer joins that answer by declaring, which is the part a tag name in the selector could never do.

lf-callout:not(:where([data-lf-exhibit]) *) {
  cursor: pointer;
}

Type is three tokens. --serif sets what the page says, --sans what it labels — and what the comment panel, banner, and every injected control are set in, so the chrome follows the page. --mono sets code, diffs, and timestamps. Name families the reader's machine already has: the stylesheet is inlined into each version export, and a font fetched by URL is one a standalone copy opens without.

The default stylesheet remains intact, so changing one token needs no copied theme. Its opening token block is the reference. Presentation unique to one page still belongs in that version's own <style>; the layer is for a look reused across pages.

The tab carries a mark too. A package may drop in its own icon.svg, replacing the shipped mark. It has to keep one promise: an element marked class="lf-tone", which the page paints in the color the banner's status dot is wearing. A row of tabs then says which leaf is working, which is waiting on you, and which has nobody behind it, without opening any of them.

Add a CSS-only widget

Most new document shapes need a schema and CSS, with no browser code. Add the tag's complete entry to registry.json and its rules to theme.css in the package that owns it.

The entry's example appears in catalog, so an agent working in the project can discover and use it:

<lf-callout id="migration-warning">
  <strong>Migration window</strong>
  Writes pause for about two minutes.
</lf-callout>

Edit the package's registry.json to declare attributes, child relationships, or a different content model. The entry is JSON Schema over the element's attributes. Leaf's x-* keys connect that schema to the rest of the page — the registry documents each under $keys, which page catalog prints and this table is written from:

Key What it declares
x-ask What x-ask declares: an instance is the complete reading and arrival region for exactly one non-quoted nested x-awaits widget. The region carries the heading, context, and evidence the reader needs before the control; the nested widget still owns the answer and its fold. The banner, Asks tray, n/p walk, address chord, standing ring, and page state name the region while action eligibility and replay name the nested widget. An arrival focuses that widget's answering control but scrolls to the region's start. version check requires one non-quoted nested x-awaits source so the two identities cannot be ambiguous.
x-awaits What x-awaits declares: an instance of this tag is a standing request to the reader — a question with no pick, a change nobody has decided, a piece of work the page says is waiting on them. One declaration is the whole of it. The banner's count, the n/p walk that steps them, the ? overlay's row, and conditional x-state actions are readings of one projection, so nothing names a tag and the twelfth widget joins by declaring. Quoted material asks nothing — an exhibited decision is a mention (x-exhibit). A widget inside a thread asks like one on the page, with one bound set by frozen thread markup: only an action can answer it there. when selects asking instances, answers names exactly the x-state verbs that settle one, rollup derives a nested request from ordinary interventions and child roll-ups, until can require a distinct completion verb for a matching thread ask, and all names a blanket answer offered by the banner.
x-content The content model: prose (children are the author's markup), items (children are this widget's own item elements and nothing else), data (one <pre> holding text in the notation the description names, < > escaped — <pre> because it is the only thing in HTML that says whitespace is load-bearing without a stylesheet to read), or none. Required.
x-conversation The module places a conversationBox in each page instance matching authored, static when attributes (an attribute written by an x-state or x-report value record cannot drive this predicate). It projects exact-section threads textually from the whole log; Comments keeps the complete view and is the only place interactive reply markup renders. Both views share the reply draft. Thread markup and quoted material take no second view. hold labels a stronger send route whose comment carries holds naming this widget: that route remains available after ordinary roots, the unresolved held thread is the hold, resolution releases it, and undoing resolution restores it atomically. Dropping the widget drops only the inline view.
x-example Authored markup exhibiting the widget, valid against the merged registry: page catalog shows it, it is what an agent writes from, and the lint refuses one that does not validate.
x-exhibit The element quotes what it holds rather than speaking it — a mention, not a use: interactive widgets inside take no input (a choose group no clicks, a board no drags, a suggestion no decision), and an ask inside it counts for nothing. A module asks quoted(el) before wiring any input, and the theme's affordance rules — the hand, the lift, the joined shape — stand down inside it on their own.
x-inline The element is set among the words around it, so the render gate holds its box to no width of its own and the rule that a block widget stands in a block slot passes over it.
x-language The attribute that names a code language, held to $languages.names — the one list this page colors from — so a second tag taking a language costs a declaration and no new reader.
x-lines Attributes holding 1-based line references into the nearest data body — the element's own <pre>, or its holder's (a note's at names a line of its code block) — which version check refuses outside the body.
x-paints Attributes the theme renders as paint alone — a status marker's tint, an event's kind, the ring on the recommended option. The runtime speaks each to a reader listening (the value, or a flag attribute's own name), silently on screen and out of every selection, so a fact stated in ink is not silent to whoever cannot see it.
x-parent The widgets an instance may be a direct child of — an item's holder — and, read the other way, what a container is made of: a gesture a container takes on its whole box stops at what it holds, and this is where it reads which children are its own rather than things it merely contains.
x-refers Attributes whose values name another element on the page (an option's for), which version check holds to an id the version carries and the runtime renders as a reference the reader can follow.
x-report What x-report declares, once per widget with an agent channel: report verbs a worker folds onto the page through leaf report, each with a detail schema (the payload half of the log's forever-contract, exactly as x-state's), a semantic facet, a fold unit, and a required record form drawn from $state's vocabulary minus body. An optional update names the required non-empty string detail field that is the report's short human-readable news; naming it lets the canonical update feed expose those words without guessing from a widget, verb, or field name. The owning widget instance, unit, and facet together name one independently standing fact; verbs on the same tag and facet share their unit and record form, and an action outranks a report at that coordinate. A report moves declared state only, never the page's words, which is what keeps the passage reading untouched by it. The runtime paints a standing report live (data-lf-reported); it is provisional news, standing only until a version answers it. How a version answers a standing report is $report's.
x-retired-when On a slot: the holder's action outcome that takes this element off the page (a suggestion's lf-old under accept). Requires x-parent. Every outcome named by one holder's slots belongs to the same facet, so only one settlement can stand. The layer renders the settlement whole, following the log's standing state both ways — reconciliation paints data-lf-state="<outcome>" on the holder and data-lf-retired on the slots that outcome retires, takes both off when a later widget-unit verb on that facet displaces the decision in the fold, and one theme rule hides the marked slots — so no module owes any of it, though one may paint the same sooner as its own gesture (renderRetired). The file's reading takes the fact from the log, and version check licenses dropping the slot's ids on it; version check --render reports a settled slot still showing words, or a mark the log never decided.
x-says Attributes whose values the reader sees as words, and where each renders. after goes inside the element's own content, ahead of the apparatus that ends it, so a word does not land on the far side of a control that closes the row. before goes at the element's start, ahead of chrome as well: leading chrome is not something the words stand beside, it is a module speaking for the whole element, so an element's opening words go above it; where an entry declares two at that edge they render in the order the entry lists them. The runtime writes them as selectable text the user can quote and comment on, where content: attr() would paint glyphs into no text node.
x-shadow The module renders the page's words into an open shadow tree, which the passage walk, the selection capture and every question asked by id cross; a shadow root on a host whose entry lacks it is a render-gate finding.
x-state What x-state declares, once per widget with an action channel: each verb's detail schema (the payload half of the log's forever-contract), its semantic facet, fold unit, optional current-state requires, and the record form its state takes in markup. The owning widget instance, unit, and facet together name one independently standing fact: verbs on the same tag and facet share their unit and record form, competing verbs there supersede one another, and distinct facets coexist. Every applyAction is absolute, so the user's standing state is the last surviving action per coordinate, and one declaration drives the POST and re-vendor contract gates, version check's state gate (a version whose markup actively contradicts the fold without restated is refused; writing the folded state is honoring; re-emitting the previous version's state is blessed silence), the record-lag report, the runtime's decided-but-unhonored mark, and the version diff's state half. The browser uses requires for its controls and common send door; POST interprets the same prerequisite against the authoritative fold under the append lock. One detail field name is reserved across every verb: resolves means the comment thread this action answers, thread settlement reads it on any action, and the lint holds a verb declaring the name to a plain string on a widget-absolute unit — a thread is answered by the ask, and an ask is a widget instance ($awaits). Because thread history can outlive its markup, every action verb on a resolves-bearing tag shares one answer facet. A report verb spelling the name is refused outright: settlement reads actions, so the field there would declare an answer nothing gives. What settles the thread is the widget's standing answer rather than the fact an answer was once given, so an answer that a later action at that coordinate supersedes, or that a restated takes back, leaves the thread open again. The facet, unit, requires, and record forms are $state's.
x-tone The attribute that names one of the layer's semantic tints, held to $tones.names, which the theme has a rule for.
x-upgrade A module at widgets/<tag>.js enhances the element in the browser. page init refuses a layer declaring it with no module beside it, and the render gate a module that defines no element. Required.
x-verbatim An upgraded element whose body reaches the reader as its own words — the module leaves it in place — which is what makes it quotable; a body without it is source the widget renders (a diagram's notation). The render gate refuses an x-verbatim whose rendered words differ from the file's.
x-visual The rendered element takes a whole-widget comment by click, there being no text in it to select, and the click's anchor is the widget rather than a generated part inside it.
x-wide The width model, for evidence set to its content rather than to the measure prose is read at: box (the widget lays its content out into whatever width it is given, a board's columns, so it stands at the one width the whole vocabulary shares) or drawing (the widget renders one thing drawn at a size of its own, a diagram's graph, so its box is the clip around it — the room the page has, with the drawing as near the column's axis as the claimed margins allow, and scrolling where even the room is short). A drawing declares no x-says: that box is laid out as a row holding what was drawn, so a word the layer writes into it would stand beside the drawing rather than over it, and the registry refuses the pair. Nothing is authored for it and no page asks: the theme grows the element out of the column into whichever margins are free. A margin holding something of the page's gives nothing where it holds it, and only there: an exhibit level with a note drops below it and then takes the whole width; one level with a suggestion's controls keeps its place and declines their side, a positioned row being nothing an exhibit can drop below; and one that nothing reaches keeps its place and grows both ways.
x-withdrawn-as On a holder with retirable slots: the outcome an undecided instance stands as when a later version takes it back unanswered — a withdrawn suggestion leaves the page where a reject would — which is what lets that version drop the ids inside the slots the outcome retires. Hedged, because there the author asserts a state the user never gave: only whole, and never while a thread is anchored in it.
x-word What x-word declares: this tag's own word for what it is comes from its module, which answers lfWord() on the element, rather than from the tag's name. Almost no widget declares it, and none needs to while one tag is one kind of thing: a name away from the page is then the tag with its prefix dropped. It is wrong where a tag holds several kinds. Every shape of lf-suggestion came out as "suggestion", so a row on the asks tray named a deletion by the words it proposed to remove and read exactly like the insertion above it. The word is asked for wherever the page names an element away from itself — a tray row, a comment's anchor label — and an element that has not upgraded yet, or answers nothing, falls back to the tag.
x-work Where this widget can show a transient local line for leaf status --on: content appends it to a block prose widget; conversation places it at the start of the widget's declared x-conversation box and therefore inherits that box's own predicate. An optional when narrows the instances that have the seat under their replayed attributes. The declaration is permission for runtime chrome, not a consequence of x-content: prose may itself be a holder gesture or sit in a hidden panel.

Each package entry is complete. Replacing a shipped widget means copying and editing that widget's whole entry; new widgets only add their own entries.

Add browser behavior

Set the registry entry's x-upgrade to true and add widgets/lf-callout.js. Inside a repository dedicated to that package, . is its path.

The module starts with the same one-shot upgrade guard used by shipped widgets:

import { once } from "/leaf.js";

customElements.define(
  "lf-callout",
  class extends HTMLElement {
    connectedCallback() {
      if (!once(this)) return;
    }
  },
);

The widget owns its supporting code. Put relative imports beside the entry module under widgets/, and third-party or data files under vendor/; page init carries both directories into the page. /leaf.js is the whole Leaf API, not a limit on a widget's own modules.

The authored element should remain readable before the module runs. Set x-verbatim when the module preserves its body; omit it if the module renders different words. Runtime helpers cover fail-soft rendering, asynchronous settling, controls, announcements, and actions. actionSequence(this, "verb") reads one widget's version-bounded action history, and watchActions(this, "verb", render) reruns a renderer after replay; use them when a widget needs to explain how its folded state was reached rather than copying or retaining the event log itself. updateSequence(this) and watchUpdates(this, render) read the shared update feed about an item. A report and a thread work claim have the same typed envelope but keep their own source and settlement rule. Their closed disposition is effective when the update contributes to current state on its semantic coordinate, standing while it still needs settlement but is presently outranked, and settled once its authority answers it. Settled report history remains available, so a row can still say when the log last heard something after a version answered it. Pass an explicit {kind, id} target for a thread; bare ids are rejected because they are not canonical identities. ago(ts) is the page's one wording for elapsed time and quietSince(ts) its one judgment about a claim gone unrefreshed; use both rather than spelling either again, or one page ends up with two clocks on it. The shipped options module and its registry entry form a complete action-backed example: the registry validates each action and records its absolute state, while applyAction makes replay idempotent.

Export keeps the DOM produced by an upgrade and removes its scripts. Durable state therefore belongs in attributes, and the resulting markup must still make sense after its event handlers are gone.

Vendor and check the result

leaf package check .leaf
leaf page init <page-dir>
leaf page catalog <page-dir>
leaf page guidance <page-dir>

Then use the custom element in the page's next versions/vN.html file and run the same gate as any other version:

leaf version check --render <page-dir>

Re-running page init is the explicit update for an existing page. It composes the current packages, checks the full registry, requires a module for every upgraded tag, and refuses a vocabulary change that would strand actions already recorded in the page's log. Pages that are not re-initialized keep their vendored theme and widgets unchanged.