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.