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.