customize
Customize leaf
A customization layer can change the theme, add lf-* elements,
and supply browser behavior. page init combines that layer with
the shipped ones and vendors the result into each page, where the normal
catalog and checks cover the additions too. Leaf's own content widgets ride
this same mechanism — the shipped
bundled/
layer is a complete worked example of everything this page describes.
Choose where it applies
| Scope | Directory | Scaffold command |
|---|---|---|
| Current project | .leaf/ |
leaf customize theme |
| Every project for this user |
$XDG_CONFIG_HOME/leaf/, or
~/.config/leaf/
|
leaf customize theme --user |
Run project commands from the project root. Both theme and widget scaffold
commands accept --user to write the user layer instead. During
page init, the shipped layers load first — integrated, then the
bundled widgets — the user layer next, and the project layer last. Theme files
join in that order. A later registry entry or module with the same name
replaces the earlier one. Files may be managed through symlinks, but user,
project, and page-owned paths must resolve separately so one scope can never
overwrite another.
Adjust the theme
The scaffold creates a short theme.css that loads after all
shipped rules:
leaf customize theme
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-options {
gap: var(--sp-2);
}
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 layer 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. This
command creates both in the project layer. Choose this form when CSS is
enough; if the widget needs browser behavior from the start, use the
--upgrade form in the next section instead.
leaf customize widget lf-callout
The generated registry entry accepts an id and prose. Its 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 .leaf/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:
| Key | What it declares |
|---|---|
x-awaits |
An instance of this tag is a standing request to the reader — counted in
the banner and stepped through with a.
|
x-content |
Prose, child items, a data body, or no body. |
x-example |
Valid authored markup shown by catalog. |
x-exhibit |
This region quotes its interactive descendants rather than offering their controls. |
x-inline |
The element is set among the words around it, so the render gate holds its box to no width of its own. |
x-parent |
The required direct parent for an item element. |
x-retired-when |
The parent action outcome that removes this child from the page. |
x-says |
Attributes rendered as selectable page text. |
x-language |
The attribute that names a language from the shared tokenizer list. |
x-upgrade |
A module at widgets/<tag>.js enhances the element.
|
x-verbatim |
An upgraded element leaves its authored body readable as written. |
x-visual |
The rendered element takes a whole-widget comment by click. |
x-state |
Action verbs, payload schemas, and how the user's edits record in markup. |
Each overlay 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
On the widget's first scaffold command, --upgrade also creates
widgets/lf-callout.js and marks the registry entry for dynamic
import. It is an alternative to the CSS-only command above, not an upgrade of
an existing scaffold:
leaf customize widget lf-callout --upgrade
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 authored element should remain readable before the module runs. The
scaffold sets x-verbatim because this starter preserves its body;
set it to false 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. 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 page init <page-dir>
leaf page catalog <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 merges the current layers, 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.