extending

Extending Leaf

A page is ordinary HTML that Leaf's widgets, themes and layouts extend, and packages add reusable widgets, themes and behavior to Leaf. Their contracts let each contribution work alongside other packages and the page's own content.

This guide is for whoever builds on Leaf: what a page can hold, how it takes a theme and how it is laid out, then the interfaces for implementing widgets, themes and packages, with a package tutorial and CSS contracts. How it works introduces Leaf's core concepts and how they fit together. Using covers working on a page.

In this topic

A page is an HTML document an agent writes around the work. This tab covers what a page can hold and how it takes a theme, and Layout covers which layout fits the work.

Parts of a page

The agent writes an HTML document containing the page's title, description, content, and layout. When the server delivers it, Leaf adds the page identity, visual theme, browser JavaScript, and canonical URL. The browser runtime supplies the commenting and collaboration interface. Authors build the content with these tools, starting with ordinary HTML:

  1. Ordinary HTML. Leaf's reading theme, which a page takes by default, styles headings, paragraphs, tables, code, and details without requiring CSS classes. These elements provide most of a page's content.
  2. Theme classes. The theme supplies classes for common presentation patterns, including .eyebrow, .lede, .callout, aside.note in a layout-note row for a margin note, and ol.steps for a numbered sequence like this list.
  3. Widgets. A widget is a complete page feature, authored as a custom HTML element, such as <lf-options> for choices and <lf-board> for cards in columns. Other lf-* elements provide structure, such as <lf-pane> for a pane. The registry declares each element’s attributes and behavior using JSON Schema. The browser, validator, and agent instructions read those same declarations.
  4. Page-specific presentation. A local <style> and inline SVG can define arrangements and drawings specific to the page. They use the theme's shared color and type tokens to stay consistent with the rest of Leaf. The Layout tab's drawings use this approach.

Leaf elements load their JavaScript modules when the page uses them. Behavior specific to one page can use inline module scripts or local modules under page/. Reusable behavior belongs in a package, a directory containing element declarations and their supporting files. The page-behavior reference defines module and dependency rules.

Widgets

These examples are interactive parts of this page. Their choices, card moves, and comments share its event log and Undo; as samples, they ask nothing, so On Me stays empty. Each activity feed shows the same page history, including actions in other examples.

Example page: a release review

Choose a rollout audience, then one follow-up job. Open Show activity to inspect both selections. Choose Undo in the page’s leaf menu to withdraw the job choice, then press Activity changed in the feed. The rollout audience remains selected.

Start with canary clients; expand after the error rate settles.

recommended Canary clients Start with a small group and compare its error rate before expanding. All clients Enable the release for everyone once the checks pass. Hold the release Gather more evidence before enabling it for any clients.

Which job should start first?

Prepare the rollback flag Add a concurrency test Set up the error-rate comparison

Recorded actions

Options with explanations render as cards; options containing only labels render as rows. The content determines the presentation. A row can link to a section that explains the choice. Clicking an option immediately shows the selection and records it for the agent. A later revision preserves that choice. The group also offers an empty option for an answer the author did not list.

Move “Flaky auth test” to “This week” by dragging its grip. With a keyboard, focus the grip, press Enter to grab, Right to move, and Enter to drop. On a narrow touch screen, use the card’s Move to buttons. Open Show activity to inspect the recorded move. Choose Undo in the page’s leaf menu to return the card to Incoming; press Activity changed in the feed to see the move marked undone.

Flaky auth test Fails one run in twenty on CI only. Dark-mode contrast pass Rate-limit the export Tokenize the palette

Recorded actions

In a live Leaf, cards drag between columns by a grip, and each drop reaches the agent as a structured action.

The widget catalog also includes diagrams, charts, diffs with collapsible files, annotated code, editable drafts, playgrounds, and recorded browser traces. The examples gallery opens complete pages using them.

Asking the user

Any widget can ask, whether bundled, from a package or the page’s own, by declaring x-awaits in its registry entry, which names the actions that answer it. To ask about content that asks nothing, such as a paragraph or a chart, or to leave the user a step such as trying a build, the agent opens a thread on that content or the page with a message sent with --obligation, and the user’s reply answers it. Nothing goes On Me that neither a widget nor a thread answers, so no item carries a generic Done, and Leaf adds no answer to a widget beyond those the widget declares.

Each ask joins the user's On Me list until the widget or thread that asks it is answered (On Me and the agent’s list).

External data

External values, such as benchmarks or build status, use one JSON publication per named source. leaf data set validates the value against the page’s declaration, records the contract beside the value in the source file, and replaces the publication atomically. Any process can call it. Leaf validates the value against the recorded contract on read and displays a source error for a mismatch.

A source replacement updates the open page without creating a revision; each publication holds its latest value. A later page revision may bind the source to another contract, which its next write takes. An lf-num instead freezes a number into authored text. It retains that number when the source changes, and leaf page check reports when its source has changed.

Styles and overrides

The user chooses the visual direction. A theme supplies shared colors, type and reusable styles; widget packages draw their own controls; the author arranges the page and makes local adjustments.

flowchart TD
  accTitle: A page captures its selected theme
  accDescr: The author selects Editorial from the user's themes. Leaf captures that theme in the release-review page, and the export carries the same theme. Changing the user's default later changes neither existing copy.
  Themes["User's themes<br/>Editorial · Dense"] -->|select Editorial| Page["Release review<br/>Captured: Editorial"]
  Page -->|export| Export["Exported review<br/>Same Editorial theme"]
A page keeps the theme it was made with. leaf theme list shows the user’s named themes. Changing the default later leaves this page and its export intact.
Choosing a theme and honoring browser preferences

leaf theme list exposes the user's named themes and their author instructions. page init --theme NAME selects one; a new page otherwise uses the user's default, or Leaf's reading theme. A page with a design wholly its own takes none with --no-theme and starts from the browser's defaults, while Leaf's widgets, threads and panel keep their faces. Re-initializing keeps the captured choice unless explicitly changed. The author follows the user's request first, then the task and the theme's instructions.

The reading theme supports light and dark schemes. Authors preserve supported platform preferences when composing a page; an explicit visual comparison may need a fixed appearance, declared with color-scheme. CSS precedence determines which rule wins, not who has authority over the work.

Make a local adjustment: colors, width or controls
To change Write
Leaf's colors :root { --accent: teal; }
A Layout's width :root { --col: 46rem; }
A widget lf-card { padding: 8px; }
Controls a widget builds lf-board .lf-grip { color: teal; }

A Layout reads tokens such as --col, --sidebar and --page-pad-start, and any other rule changes it too, such as main { padding-top: 28px; }.

A bare button or input rule styles every one in the document, the controls a widget builds included, so name the widget to aim a rule at its controls. Leaf’s panel and thread cards keep their own look against a rule as light as one class, and inherit theme inputs such as --accent and --sans from :root.

In this topic

Layouts

Leaf supplies page layouts and a layout for blocks within a page. A column, sidebar, or workspace sets the page’s shape; tiles arrange a block’s children and can be used within any of them. The table and drawings below show the same classes in the same order.

Class Arrangement
layout-column Page layout Text in a centred 720px column; figures, boards and tables may extend past it. Suits plans, reviews and explanations, including this page.
layout-sidebar Page layout A body beside a narrower side column, with the header across both, as wide as the window up to 1600px. A board and its status, a release dashboard, or a review with its verdict beside the diff.
layout-workspace Page layout Task regions kept together: a queue beside its selected item, or controls beside a preview. In a large window, the workspace fills the height and its panes scroll separately.
layout-tiles Block layout Equal cells, as many to a row as fit. A row of metrics or status tiles.
wide block
A column page layout-column on main. Text keeps the 720px column, and a block with a declared width, such as a diagram, extends past it.
main.layout-sidebar board aside
A sidebar page layout-sidebar on main widens the page and sets a body beside its side column. Every block starts at its region's left edge, and text retains its reading width.
queue detail
A workspace layout-workspace keeps task regions together. In a large window it fills the height, and its panes scroll separately.
Ready · 24 Review · 8 Blocked · 6 Done · 3
A tile block layout-tiles arranges children in equal cells, wrapping as the available width changes. The surrounding page keeps its own layout.

Leaf's Layout classes are ordinary CSS, applied beneath the page's own stylesheet in the cascade. A page can adjust them with CSS rules, for example to change a column's share of the width or a gap. It can also use its own grid when the supplied layouts do not fit.

The reading column

On a layout-column page, headings, paragraphs, lists, and code occupy a centred column up to 720px wide. On a sidebar page too, the theme limits paragraphs, list items, and headings to that reading width. A workspace applies neither the reading width nor the limits below: every block fills its pane.

A diagram, board, or wide table can extend into the margins beyond the reading column. The text above and below it keeps its width and position. These width settings control the block:

  • column is the text measure. In a wider region, such as a sidebar page's body, a column block starts at the same left edge as the text.
  • wide starts at the edge the text starts from (the left, in a left-to-right page) and extends toward its end, up to about 910px, enough for 100 columns of code. Wide blocks share this width so they align with one another.
  • available uses the window's width after subtracting the outer page margins and space for margin notes and controls.

The registry, which declares each element's behavior, also gives it a default width. For example, lf-options uses column, lf-diff uses wide, and lf-board uses available. A data-width attribute asks for wide or available on any block, overriding the default. If the margins have unequal space, an available block extends farther into the wider margin. A table given one of these widths uses its columns' natural width, up to that limit, rather than stretching across the extra space.

main figure data-width
data-width on a block The block extends past the column, and the paragraphs around it keep their width.

A log, feed, or long listing with data-bound scrolls within a limited height and opens at its newest entry. Its growing content therefore does not make the whole page longer.

Margins

A sufficiently wide window places these controls and notes beside the reading column:

  • The rail, on the right, where Leaf places a marker beside each commented or decided passage. It needs a window about 960px wide, or about 1010px on a touch screen. A sidebar page shows it from about 1920px, or from 640px with data-rail="right" on body. A workspace has no rail; a comment there shows as a pin on its block.
  • A sidebar, an aside beside the body in layout-sidebar. Its authored layout-track holds contents, a legend or reference facts, and stays in view while the body scrolls when both fit beside each other.
  • Sidenotes, each an aside.note paired with its passage in a layout-note row, on the right when the row has enough room.

The browser places these compositions as soon as the HTML arrives. In a narrow window, the sidebar stacks in source order and each note follows its passage. Notes never shift the reading column.

Workspace

layout-workspace fits the page to the window above Leaf's panel. A single-row header holds the title and status tags. The body takes the remaining height and contains an lf-pane, a widget that arranges its own panes, or a grid of panes defined by the page's CSS. Each pane scrolls its content beneath its own header. A pane's footer stays visible, keeping its actions within reach.

An lf-tabs list="side" in the workspace body displays a list of tabs as a queue, such as tickets or findings. The selected item's content appears beside that list.

The workspace fills the window's height when the window is at least 720px wide and 480px tall. In a smaller window, panes expand to fit their content and the whole page scrolls.

queue detail footer
A large window The workspace fills the window. Each pane scrolls under its header, and a footer's actions stay in view.
queue detail
A small window Below 720px wide or 480px tall, the panes take their natural height and the page scrolls.
<main class="layout-workspace density-working">
  <header><h1>Payments alerts</h1></header>
  <div id="regions">
    <lf-pane label="Queue">…</lf-pane>
    <lf-pane label="Alert">…</lf-pane>
  </div>
</main>

<style>
  #regions { display: grid; grid-template-columns: 1fr 2fr; }
  @media (width < 720px) { #regions { grid-template-columns: 1fr; } }
</style>

density-working reduces type size and spacing for a compact working surface. It is independent of the Layout: a workspace can use ordinary reading sizes, and a pane or table on another page can use working density.

Examples: Notification playground, Merge a worktree, Alert review.

Tiles

layout-tiles arranges an element's children in equal cells at least 9rem wide, placing as many cells in each row as fit. Cells in the same row have equal heights. A page can set its own columns when it needs a particular grouping, such as four tiles in two rows of two. A row of headline numbers can use a layout-tiles block of native dl elements with labels in dt and values in dd.

Tabs and parts

An lf-tabs at the top of a page creates tabs like the ones on this page. Their tab strip stays at the window's top while the page scrolls, and one tab's content is visible at a time. All tabs share the page's width, Layout, threads, and version history.

When different parts of the work need different layouts, lf-parts holds an lf-part for each. Each part contains its own layout container; main itself carries no Layout. For example, one part can hold an interactive workspace and another a proposal read in a column. Parts share the page’s history, threads, and On Me list. The page-layout reference gives the markup.

Narrow windows

In a narrow window, page regions stack vertically in HTML order. Margin notes move into the reading column, a side column appears above or below its body, and workspace panes expand to fit their content. Tiles can still fit two per row on a 390px phone.

1 2 3 4 1 2 3 4
A side column on a phone The side column stacks under the body, so region 3 follows region 2.
In this topic

Who is responsible for what

The user sets the goal and preferences. The author composes the page; packages supply its visual language and widget behavior. Leaf core keeps the shared record of what the user does.

flowchart TD
  accTitle: Ownership boundaries in a Leaf page
  accDescr: The user gives the author a goal and selects a theme. Within the authored page, the author owns content and layout, and the widget package owns controls and action meaning. Theme inputs style both. Widgets send actions to Leaf core, which owns the record, current state and Undo.
  User([User])
  Theme["Theme package<br/>Color, type and spacing"]
  subgraph Page["Authored page"]
    Content["Page author<br/>Content and layout"]
    Widget["Widget package<br/>Controls<br/>Action meaning"]
    Content -->|places and configures| Widget
  end
  Core["Leaf core<br/>Record, state and Undo"]
  User -->|goal| Page
  User -->|theme preference| Theme
  Theme -.->|visual inputs| Page
  Widget -->|actions| Core
  Core -->|current state| Widget
Ownership architecture. The author places widgets inside the page; each package owns its widget’s behavior. Theme inputs style the page and its controls. Leaf core owns the record behind them.
Each owner’s authoring interface
Owner Responsibility Interface
User Goals, preferences, decisions, and edits. Instructions to the agent, actions on the page, and named personal themes with a default. Theme colors can follow the browser's color-scheme preference.
Page author Content, initial values, package selection, and composition. Semantic HTML, declared widget attributes, data bindings, page CSS, and optional page modules.
Widget package Domain meaning, controls, and rendering within the allotted box. Registry declarations, browser modules, scoped styles, and author instructions. The package connects its controls to Leaf's state, commands, and annotation APIs.
Theme package Reusable visual choices and presentation idioms. Theme inputs and explicit page-wide styles, independently of whether the package declares widgets.
Leaf kernel Shared state, event admission, Undo, and interaction services. The registry contract and public module APIs. Core interprets declarations; packages supply each widget's domain behavior.

These are contribution boundaries, not a sandbox: package and page scripts run in the same document. Each package's selectors decide where its rules match, and inheritance connects nested content's presentation. The CSS contract describes selector reach and cascade precedence. The package checks validate declarations and rendered behavior.

Choose the extension boundary

  1. One page. Semantic HTML, existing Leaf elements, a page-local <style>, and inline SVG shape one document. Keep presentation here when no other page needs it.
  2. A package. A reusable element, theme, browser behavior, or author instructions belong in a package. A project-local package, an installed package, and a package bundled with Leaf all use the same composition format.
  3. Leaf's kernel. Runtime, chrome, and protocol behavior shared by every page belongs in Leaf itself. A package consumes those capabilities; it does not add a parallel runtime path.

Package interfaces

Packages connect through declared inputs, outputs and actions. A release review can use one package to read a patch and another to navigate its files, without either package knowing the other’s implementation.

Data connects widgets

flowchart TD
  accTitle: A typed output connects independent widgets
  accDescr: The author supplies a patch. The Diff widget publishes its changed files using the file-list contract. The page binds a File tree widget to that output, without either widget knowing the other's implementation.
  Patch["Author supplies a patch"] --> Diff["Diff widget"]
  Diff -->|files: file-list| Tree["File tree widget"]
The page connects these widgets with for="release-patch". The producer and consumer agree on the file-list value’s shape. The file tree can also read a stored source with that contract. A published output is a browser-local derivation, not a decision in the page’s log.

Widgets delegate recorded state to Leaf

flowchart TD
  accTitle: A choice is drawn immediately and recorded by Leaf
  accDescr: The user's choice starts two paths. The browser draws it before the next frame, while Leaf validates the action and appends it to the log. Shared state then reconciles the browser's selection.
  Choose["Choose Canary clients"]
  Choose --> Draw["Browser<br/>Draw now"]
  Choose --> Admit["Leaf<br/>Admit action"]
  Admit --> State["Shared state"]
  Draw --> Reconcile["Browser<br/>Reconcile choice"]
  State --> Reconcile
The package declares choose and the schema for its selected IDs. The browser shows what the gesture decides while admission completes; a refused action restores the server’s state. Undo withdraws the admitted action, and the widget draws the resulting selection. Packages share these guarantees. How it works explains how Leaf derives state from the record.
Independent capabilities, validation and package selection

Leaf's core, called the kernel, supplies the browser runtime, shared controls, and collaboration protocol. Packages supply reusable elements, JavaScript modules, styles, and authoring instructions. The default package provides common widgets and structural elements; optional packages add capabilities such as diagrams and diffs. Project packages use the same format and can replace or extend the page's selected layer. The package catalog shows the bundled packages; the tutorial builds a small custom package.

A package declares capabilities independently: what an action records, whether it decides a proposed change, which typed data it consumes or publishes, and where its styles apply. Ordinary data fields do not acquire decision meaning from their names. Two occurrences of the same widget can select different input contracts without replacing the widget's declaration. Leaf validates lasting input where it enters the event log; the render check validates the outputs a widget actually publishes during the check. The state and data contract defines those interfaces.

page init records the page's package and theme selection. The Leaf serving, checking or exporting the page composes its layer from that selection and supplies the browser runtime. Each revision captures the authored content, page-owned declarations and modules, and the package selection. Historical revisions run with the current Leaf's runtime and composed layer while retaining their authored content and recorded decisions. The package contract defines how packages are combined and replaced.

In this topic

Build a package with two files

Create a package in the project, then check the empty composition:

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

The widget in this tutorial needs only these files:

packages/callout/
  registry.json
  defaults.css

Every package beyond the bundled default joins a page with --package. In the commands below, PAGE is the page directory to create or update:

$ leaf page init --package packages/callout PAGE

The complete package contract covers installed and bundled packages, contribution directories, composition order, and replacement rules.

package init also creates empty contribution directories for instructions, browser modules, widgets, and vendor files. The CSS-only widget here leaves them untouched.

To start with behavior instead, name the first element while creating the package:

$ leaf package init packages/risk-notes --widget lf-risk-note

Leaf writes a valid markup-widget element declaration and its one-shot browser module, then checks the complete candidate before changing the package. The tutorial below stays with the smaller CSS-only form so each contract remains visible. A behavior module imports Leaf's package API from /runtime/widget-api.js; it declares each interactive capability once, as the lfCommands of the element it applies in. The same live command rows feed keyboard dispatch, help, accessibility shortcuts, and—when decision names the action—the controls shown inline for an obligation.

Style the element

A package's defaults.css joins Leaf's existing cascade as written, so its rules name the elements they style, starting with lf-callout itself:

lf-callout {
  display: block;
  padding: var(--sp-3);
  border: 1px solid var(--rule);
  border-radius: var(--r);
  background: var(--field);
  --lf-block-frame: 1;
}

--lf-block-frame: 1 tells the shared layout that this rule draws the widget's inset. The style contract distinguishes theme inputs such as --accent from layout declarations, runtime outputs, and vendor bridges. The CSS contract explains how widget, theme and control sheets compose.

Declare the element

The page will author the callout as ordinary readable markup:

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

Its complete registry.json entry validates the attributes and tells Leaf that the body is prose. x-example is the sample an author queries with the entry:

{
  "lf-callout": {
    "description": "A short warning or constraint that belongs in the reading flow.",
    "type": "object",
    "properties": {
      "id": { "type": "string" }
    },
    "required": ["id"],
    "additionalProperties": false,
    "x-content": "markup",
    "x-upgrade": false,
    "x-example": "<lf-callout id=\"migration-warning\"><strong>Migration window</strong> Writes pause for about two minutes.</lf-callout>"
  }
}

x-content and x-upgrade are the two Leaf declarations every widget requires. The ordinary JSON Schema fields validate its attributes, and Leaf adds the grammar an id follows; description and x-example make the merged registry useful to an author.

The registry-key reference lists every x-* declaration. A CSS-only widget stops here.

Compose and check the page

$ leaf package check packages/callout
$ leaf page init --package packages/callout PAGE
$ leaf page vocabulary PAGE | jq '.["lf-callout"], .["$keys"]'
$ leaf page check PAGE --render

A page keeps its own copy of a package it selects by path, so re-running page init --package is the explicit update for an existing page.

leaf package install packages/callout copies the checked package into ~/.local/state/leaf/packages/, after which --package callout reaches it by name from any project on this machine.

Registry keys
The generated reference for declarations such as x-data and x-state.
Event log
How a widget's x-state verbs become logged events and folded state.
Package contract
Composition, instructions, browser modules, external data, and validation.
Bundled default package
A complete package containing the widgets used across Leaf pages.
In this topic

CSS contracts

Widget packages style their own elements; theme packages supply presentation across the page. Stylesheet roles set where rules reach, and the cascade sets which rule wins. Styles and overrides, in the Pages tab, covers selecting a theme and making local page adjustments.

Which rule wins

Leaf sorts CSS into tiers by who wrote it. The tiers are CSS cascade layers (@layer), and a rule in a higher tier wins. Each switch below puts the rule beside it into the tier its page or package writes into, for the same card, so the card shows what the browser decides.

Rules in tier order, highest first. The card takes its padding from the highest rule that is on, which is outlined.

A lower tier's rule loses however specific its selector is, as the widget package's lf-card.featured shows. Within a tier, the more specific selector wins, and between equally specific selectors the later rule wins. !important reverses the order of the tiers, so a page's !important loses to Leaf's own; an ordinary rule is all an override needs.

The tiers

The page’s document

flowchart TD
  accTitle: CSS precedence in the page document
  accDescr: For normal declarations, unlayered page rules have the highest precedence, then the page's own layers, lf-state, lf-overrides, lf-layouts, lf-base and lf-reset. Each arrow points from a higher tier to a lower tier.
  Page["Page rules · unlayered"] --> Layers["Page's own layers"]
  Layers --> State["lf-state · Leaf"]
  State --> Theme["lf-overrides · packages"]
  Theme --> Layout["lf-layouts · Leaf"]
  Layout --> Base["lf-base · Leaf and widgets"]
  Base --> Reset["lf-reset · Leaf"]

A widget’s shadow tree

flowchart TD
  accTitle: CSS precedence inside a widget's shadow tree
  accDescr: For normal declarations, Leaf's marks have the highest precedence, then lf-state, lf-overrides, lf-shadow and lf-base. Page rules and Layouts do not enter the shadow tree.
  Marks["Marks · Leaf"] --> State["lf-state · Leaf"]
  State --> Theme["lf-overrides · theme packages"]
  Theme --> Shadow["lf-shadow · Leaf and widgets"]
  Shadow --> Base["lf-base · widget defaults"]
Normal declarations, highest precedence first. Leaf’s lf-state hides what a decision removed; lf-reset clears the browser’s form-control styles; marks are the highlights Leaf draws. !important reverses the layer order.
Which styles contribute to each tier?

A page’s unlayered rules come first. Leaf’s panel, cards and marks are also unlayered and appear later, winning a tie. The page’s own @layer blocks follow Leaf’s layers, so they outrank them.

Tier Contributions
lf-overrides Each package's overrides.css, in package order.
lf-layouts Leaf’s Layout classes, such as layout-column.
lf-shadow Leaf’s shared shadow rules and each package’s widget-shadow.css.
lf-base In the document: Leaf’s base.css, then packages’ defaults.css and widget-shadow.css. In a shadow tree: defaults the widget adopts with inBaseLayer().

Page rules and Layouts stop at the shadow root. Inherited inputs such as --accent cross it. A widget can also expose a part for styling with ::part.

A nested widget owns its interior

Put a choice inside a release review: the review package arranges the choice's box, and the default package styles its Options widget inside. Leaf adds no boundary between them, so the review package keeps its rules off the choice's interior by naming its own structure: a child combinator such as lf-release-review > p, or its own classes. Shared theme inputs, such as color and type, flow through both.

flowchart TD
  accTitle: A nested widget keeps its own styling when the outer package names its own structure
  accDescr: Shared theme inputs inherit through the review widget and its nested Options widget. The review package styles its own heading and evidence with selectors that name them, and arranges the nested widget's host box. The default package styles the Options interior.
  Theme["Shared theme inputs<br/>Color, type and spacing"]
  subgraph Review["Review widget"]
    Content["Review package<br/>Heading and evidence"]
    subgraph Host["Nested widget host"]
      Options["Default package<br/>Options interior"]
    end
    Content -->|arranges| Host
  end
  Theme -.->|inherit through both| Review
Each package names what it styles. The review package's rules reach the nested host and stop short of its interior because they name the review's own children and classes; a bare descendant rule such as lf-release-review p would reach inside. The theme's colors and type reach both widgets.

These are styling conventions, not a security sandbox. Page and theme rules deliberately have wider reach; packages run in the same document. A widget's presentation depends on its allotted box and supported theme inputs, so it can move between page layouts.

Choose a stylesheet by where its rules should reach

A package can contribute widget, theme and control styles independently. Declaring a widget does not change the reach of its other sheets.

Stylesheet Reaches
defaults.css The page document, below Leaf's Layouts.
widget-shadow.css The page document and every declared shadow tree; :host(lf-tag) keeps a rule to its widget's own tree.
overrides.css The page document, above Leaf's Layouts.

Leaf adds no scope to a sheet, so each rule names the widget it styles: lf-callout .title in the document, and :host(lf-callout) .title in a widget's shadow tree. Shadow trees form a browser boundary: document selectors do not enter them, but inherited inputs and public ::part selectors can.

The package reference defines the stylesheet roles and supported inputs.

Choose a bundled package

The core product gallery shows Leaf’s default widgets and controls. The pages below demonstrate optional packages; the Code review package provides authoring instructions.

Playground also powers the structured data explorer and the built-output A/B comparison. Those pages keep their task-specific state, schema, and behavior while reusing the same package mechanics.

developer tools

Inspect the product

These developer pages are for debugging Leaf itself. The core product gallery keeps Leaf’s controls and its core interactions on one page; optional widgets live with their working examples under Choose a bundled package.

Core product gallery Leaf chrome and every default-package widget on one synthetic page, with core interactions to try live. Open gallery Threads gallery Four live panel views: open threads, On Me, resolved, and a summarized discussion. Open gallery

Registry keys

An element declaration begins as JSON Schema. Its x-* keys tell Leaf how the element reads, renders, changes, and joins the collaboration loop. The index jumps to a key's summary, and a longer contract opens in place.

This reference is generated from the registry's $keys, the same entry an agent can query in a page's merged registry.json. The complete package contract says how the keys combine.

x-awaits

The widget asks the user for input while its authored attributes match when, which puts an obligation on their list, On Me.

Full contract

The obligation's id is widget:<source-id> and never changes. value names the x-state verb whose state is the typed answer, and answered says when the obligation is complete ($awaits); they are separate because a partial value can exist while the obligation is open, and an empty value can be a complete answer. all names the outcome one control can give every obligation of this kind at once. Once created, an obligation stays answered or withdrawn after when stops matching; an element that never matched creates none, and neither does a widget inside an exhibit (x-exhibit). A restated event asks again. While the agent owes a reply in a thread about the obligation, anchored inside the widget, or inside the widget enclosing it but no other obligation, the agent is its next actor and the obligation stays unanswered. A widget in a thread message keeps its obligation after the thread closes. A question the user answers in words is an agent message's obligation, not a widget's (--obligation). Any widget may declare it, whether bundled, from a package or a page's own; the actions it names are how the user answers, and Leaf adds no answer of its own, such as a generic Done, to a widget or anything else On Me.

x-bound

The element has its own height and scrolls inside it, so a long log or feed never grows the page.

Full contract

start opens at the first line; end follows the newest entry while the user is at the end, and stays where the user scrolled back to otherwise. An authored element may set or override it with data-bound=start|end; delivery writes it as data-lf-bound.

x-content

The required content: markup (the author's markup), members (only this element's declared member elements), data (one <pre> holding text in the notation the description names, with & < > escaped as &amp; &lt; &gt;, which an element whose x-data input the markup binds leaves out), or empty.

x-data

The widget's named typed data inputs.

Full contract

Each names a $data.contracts contract and an optional source attribute for a page source id. Optional reference and output name an x-refers attribute and the producer's declared output. Markup binds the source or the relation, never both; an unbound input reads null, except that an x-content: data element's one input reads the text of its <pre>. A document binds each source to one contract; later documents may rebind it and read the value against their own contract. Optional property names the element property Leaf sets to the delivered value, or null while there is none; a module reads the input only there, never through a package-owned source name, and dispatches drawn naming that property and the value it drew once its marked records stand. Widget outputs are ephemeral browser derivations, without source revisions or run receipts.

x-encloses

true: the widget frames the obligations of the widgets inside it and asks nothing itself.

Full contract

Its leading heading gives their prompt, and going to one of them lands on it. Each obligation it frames keeps its own id, answer and completion, and where frames nest, an obligation uses the nearest. It requires an id and markup content. lf-ask declares it; a widget that asks on its own needs no frame.

x-example

Authored markup exhibiting the widget, valid against the merged registry: an agent reads it with the element declaration, writes from it, and the lint refuses one that does not validate.

x-example-data

Sample JSON values keyed by source id bound in x-example, validated against those sources’ contracts and supplied to package check --render.

x-exhibit

The element shows content rather than asking: widgets inside it never put an obligation on the user's On Me list, whatever the declaration.

Full contract

A boolean also quotes them unconditionally: they take no input. {when: {attribute: [values]}} quotes them only while those authored attributes match, using the same presence and value rules as x-awaits.when; elsewhere they take input but still ask nothing. Modules check quoted(el) before enabling input.

x-flow

true: the element draws no box of its own around its authored blocks, so in a column or sidebar Layout they stand in the page's flow, as a layout-flow wrapper's do: words keep the prose's start edge, a block declaring more room takes it, and the element's box grows round that block. The element has no width of its own, so a data-width written on it has no effect; it belongs on the block that needs the room.

x-height

The height in CSS pixels the page reserves for the element before its module draws: the declared number, or an authored data-height, which overrides it; true reserves only where data-height is given.

Full contract

The theme holds the box to it from first paint until the module marks the element drawn (lf-rendered). Declare it on a widget whose drawing can't be sized from its markup. A module that can draw at any height, such as a chart's, draws at this one, so nothing moves; for content with its own height, the reserved height is the author's estimate of it.

x-history

true: the module renders the page's history, so a page that contains one is served the history reading with its state.

Full contract

The reading is the newest 50 logged events, newest first, each with who acted, what they did, the id of the thread it was in, whether a later undo took it back, and, for a widget gesture, the words its ids had when it was made. The module reads it with readHistory.

x-inline

The element sits inline among the surrounding words. The render check therefore requires neither a width of its own nor a place where a block may stand.

x-instructions

Instructions for an author using this widget, read after choosing it and before writing markup. One non-empty string, separate from the short description and the schema.

x-language

The attribute that names a code language. Composing the vocabulary gives it $languages.names as its enum, so a new tag that takes a language needs only this declaration.

x-measured

A value in authored content tied to the data run that produced it.

Full contract

input names the widget's x-data input, and run its required receipt attribute. When the source holds a different run, page check warns that the measurement is stale, and page state reports it. Rewriting the source counts as a new run even with the same value; age alone never makes a measurement stale.

x-name

The attribute that holds the element's title, which the page uses to name the element elsewhere, as the activity feed does.

Full contract

Declare it only where an attribute is the title, as a column's or a tab's label is; a section is already titled by its heading, a disclosure by its summary, and a titled member by its leading <strong>. An attribute the element merely shows first, such as a metric's value, is not its title.

x-output

The widget's named typed derived outputs.

Full contract

Each output names one $data.contracts contract. publishOutput publishes its complete value, or null while unavailable, for other widgets' x-data inputs. Outputs follow the publishing element's lifetime and remain browser-derived values without a server source revision or run receipt.

x-owners

The element declarations that may directly own this element. An owner's direct children of these kinds are its members, and a gesture on the owner's whole box stops at them.

x-patch

How a revision reaches an upgraded widget the user has open.

Full contract

By default any change to the widget's markup replaces the whole widget, rebuilding whatever the user had open in it. members says the module builds beside its direct authored children, leaves them where the author wrote them or moves them all into the authored body its initial drawing builds, and reads only their attributes; those children may be native markup or declared member widgets. While a revision keeps the element's attributes and each child's tag and attributes in order, Leaf edits inside those children in place. A module that moved them into an authored body reads none of them, so there only the element's own attributes need to stay the same. Requires x-content: members or markup, and x-upgrade.

x-prepaint

The attribute that names the notation of the element's written <pre>.

Full contract

Where it names markdown, Leaf draws that text as a Markdown file, its soft line breaks read as spaces, in a .lf-markdown-body beside the <pre> before the element's module draws: the server in the first document, the browser in a revision or a message. So the element first paints at the size its module will draw that rendering, and the module replaces it when it draws.

x-reading-role

The structural role the element plays.

Full contract

The only role is pane: a reading region, named by its required label, whose body scrolls on its own while the workspace is full-height. A pane holds exactly one body element, optionally between a direct header and footer; a visible heading goes in the header. Validation reads this declaration, not tag names, so a package can contribute panes under its own tags.

x-refers

A map from each attribute whose value names another element to the contract its target must meet.

Full contract

An empty contract accepts any element id, as an option's for does. A typed contract names a shared registry map with via and an equality test with where, and page check requires a target whose tag's entry in that map passes the test, so a package can tell a goal reference from a worker reference without Leaf knowing either. With owns: true the referrer fills its target: page check requires the target in the referrer's own document, and requires each element there that the test selects to be named by exactly one referrer. A keyed contract {tag, key} names a member by an attribute instead of an id: the tag element whose key holds the value, inside the referrer's nearest owner in which key is unique (x-unique), as a playground preset's setting names a control by name. In the browser a module passes the attribute to navigateToDatum to reach the named widget; it looks in the referrer's own document (its message, for a widget in a reply) before the page.

x-retired-when

On a member: the outcome of its owner's decision that removes it from the page, as accept removes a suggestion's lf-old.

Full contract

Requires x-owners. The outcome is a value of the outcome field on the owner's one deciding verb. Leaf renders the decision itself, including its undo: it writes data-lf-state="<outcome>" on the owner and data-lf-retired on the removed members, which one theme rule hides. A widget reads the decision from its state, not these attributes. Once the log decides, page check lets a revision drop the member's ids, and page check --render reports a removed member that still shows words.

x-says

Attributes whose values show as selectable text: after inside the element after its content, before at its start, in declaration order. The runtime writes real text nodes, unlike CSS content: attr(), so the user can select, quote and comment on the words.

x-shadow

The module renders the page's words into an open shadow root, and Leaf's passage reading, selection capture and id lookups reach into it.

Full contract

A shadow root attached without this key, as a page script's own custom element attaches one, is opaque: its own words can't be quoted, and the passage reading reads its host by its light children.

x-shows

The element is a view of another widget's answer, which stays that widget's: attr names the widget, in the element's own page or message (x-refers), and value the attribute of the element's compound owner that names the answer the owner holds.

Full contract

The element stands in the owner whose value its widget's answer names, or in one without value while the answer names none, or one no owner beside it holds: none is an empty pick, a member without the widget's x-awaits key (one the user added), or members naming different values. The answer names a value when the widget's x-awaits value verb records one string, or picks members whose key all name the same one. Moving the element sends that widget's own command through shownAnswer (module-reading.md), so the view keeps no fact of its own: the append door refuses a position move of the element, and page check refuses markup that places it apart from its widget's authored answer, owners without one that holds an answer naming no value, and an owner value the widget cannot answer. It keeps its authored order among its owner's members.

x-space

The width past the reading measure the element takes in a column or sidebar Layout: wide takes the shared, capped width for a block slightly wider than the column, from the prose's start edge; available takes all the width the page shell can give, into both margins.

Full contract

Without it the element fills its container, as every element does in other Layouts; an element read as lines of text keeps to the measure in its own stylesheet (max-inline-size: var(--lf-measure)). An authored element may override it with data-width=wide|available, which the theme applies without moving the prose.

x-spine

true: the element can rest as a spine, a box too narrow for words, and show the rest of itself over the page when the reader points at it, focuses inside it or arms go-to, as lf-toc rests as the contents spine.

Full contract

On a screen under a mouse, a sidebar before a layout-column body whose track holds only this element narrows to that spine rather than taking a column's room, so the prose starts beside it, and the Layout lets what the element shows lie over the body. Anywhere else the element fills its box as any other does.

x-state

Declares each verb of a widget that has state: the unit of state it changes, who writes it, and either its record (how the state reads in markup, $state.record) or the schema of its detail.

Full contract

A verb is the user's, sent from the page as action events, unless it declares writer: "agent"; the agent writes it with leaf page report, as report events. A user verb may create a child element (creates). An agent verb with text: true requires each report to carry a non-empty detail.text, a short note the report feed shows as written. The page marks a reported widget data-lf-reported until a version answers the report ($report). For each [widget, unit, verb] key, the latest action not taken back wins, and different verbs on one unit coexist. Leaf combines those actions with the authored values and hands the widget its complete state, keyed by verb, as this.state, which its render() reads. A past action keeps its key after the widget leaves the page. When an action makes the widget's x-awaits answered condition hold, Leaf records meaning.answer on it: the thread named by the widget's resolves attribute in the document the action came from, which the answer closes, or null. A decision whose outcome is the widget's x-withdrawn-as value also records null, and the page reads as if nothing had been proposed. A verb with decides: true selects which members leave the page (x-retired-when); its required outcome field names the choice. An outcome field on any other verb is ordinary domain data.

x-text-format

How the element's text is formatted.

Full contract

inline-markdown formats its direct text nodes; markdown renders its data body as safe block Markdown, read as a file, whose soft line breaks are spaces. A body record keeps the Markdown source, while passages read the visible words. Child elements stay HTML. In a generated child, the user's line breaks are hard breaks.

x-thread-seat

The module puts a thread box (threadBox) in each element that matches when on authored attributes, not on attributes an x-state record writes.

Full contract

The box shows, as text, the threads anchored exactly to that element; Threads keeps the full view. An element inside thread markup or quoted content gets no box, and in an export no element gets one. On a widget that also asks (x-awaits), the box is where the user answers in words: while a thread there waits on the agent, the obligation leaves the user's On Me list, but it is still unanswered and the widget's controls still work. hold labels a second send button whose comment carries holds, naming the widget. The widget is held while that thread is open; resolving the thread releases it. Removing the widget removes only the box, not its threads.

x-thread-surface

The upgraded widget can show Leaf's comment box and threads inside itself, beside the data items they are about.

Full contract

On each pass, Leaf calls the element's threadOutlets(targets, {signal, collection, onCommit}) with the targets anchored exactly to its items, in order, and the method returns for each an element to place it in (an outlet), or null. The widget reads its registration with threadSurface(element): update() says its outlets changed, and open(datum) opens Comment on one item. Leaf still owns anchors, drafts, sending, thread rendering, replies, reactions and focus, and decides placement inside each outlet. An item that isn't shown (not loaded, filtered out, collapsed or outdated) gets no outlet, and its threads stay in Threads and the margin.

x-tone

The attribute that names one of the layer's semantic tints. Composing the vocabulary gives it $tones.names, which the theme has a rule for, as its enum.

x-unique

A map from each attribute whose value must differ between members of one owner to that owner's tag.

Full contract

page check and message markup refuse a repeat inside the same nearest owner, such as two controls of one playground sharing a name. A keyed x-refers contract names members by such an attribute.

x-upgrade

A module at widgets/<tag>.js enhances the element in the browser, loaded only where a page's markup uses the tag. page init refuses a layer that declares it without the module, and the render check refuses a module that defines no element for a widget on the page. Required.

x-verbatim

The upgraded element keeps its authored words, and its nested upgraded widgets in authored order, so the passage reader can quote those words; each nested widget's own declarations cover the words it generates.

Full contract

Without this key the passage reader treats the widget as opaque, since its module may insert or replace words. The render check refuses a page where the wrapper's own words changed or a nested widget was removed or reordered.

x-views

An object with members, a CSS selector matching direct authored children shown one at a time, and optional parts: true for a page's parts.

Full contract

Delivery carries the selector; the browser owns their membership and hidden attribute from first paint onward; authored hidden does not select a view. openingView returns the fragment's member, else the tab's saved member, else its supplied fallback (first member by default); reload prefers the saved member. keepView both shows a member and remembers it, or shows none when passed null without forgetting the last member. Its optional third argument keeps an outgoing member visible for an animation without selecting or saving it. A parts holder stands directly in a main with no Layout, each member holding its own Layout. module-authoring.md owns view-state behavior.

x-visual

How a rendered picture offers Comment targets.

Full contract

Declaring it also tells the theme the widget is a picture (painted data-lf-visual), so an lf-ask lays out options that hold one as it does options holding images. whole makes the picture one target. With {parts: ATTR} or {prefixes: [PREFIX, …]}, the module calls registerVisualParts(source, read) once, and read returns the current list of {id, element, label, surface?}; a comment on a part stores its id as anchor.visual. Under parts, ATTR names an attribute of the widget, as in <lf-diagram parts="all">: the registered parts become targets where the widget carries it, and without it the picture stays one target. Under prefixes, Leaf accepts any longer id starting with one of them, such as commit:3f2a under commit:. surface may name a descendant whose own paint, without decoration, gives the outline Leaf marks; element stays the target for hits and travel. A comment whose part is gone detaches on replay instead of widening to the whole picture. A visual that draws its parts over time also passes {reveal}: reveal(id) must synchronously show a state that holds that part, cancelling any pending change that would remove it, and Leaf calls it before going to the part from a thread or navigateToDatum. While a declared part is missing from the current state, its threads sit on the whole picture. Call the registration's update() after any change to rendering or geometry, including an attribute or style change in place. The render check validates each registered record.

x-withdrawn-as

The deciding outcome that declines the proposal.

Full contract

It answers the obligation without closing the widget's resolves thread. A revision may also remove an unanswered proposal outright, whether or not it has retirable members; ids from earlier versions don't prevent it.

x-within

The regions (element declarations) inside which a member may sit at any depth instead of as its owner's direct child.

Full contract

It belongs to the nearest owner x-owners names around that region, and page check refuses it anywhere else. A playground control written inside lf-playground-preview stays where it is written, beside the item it changes, instead of in the rail.

x-word

The widget's word for its kind, which Leaf uses when naming the widget in a panel row, a walk or a comment anchor: a fixed word, as lf-ask says "question", or module, where the module gives one through lfWord(), as lf-suggestion returns "insertion", "deletion" or "rewrite" depending on its children.

Full contract

Without one, Leaf calls the widget by its tag name without the prefix. With module, the module may also describe the widget through lfSays(), where its text alone doesn't convey its structure, such as a rewrite's old and new text; otherwise Leaf reads the element's text.

x-work

true: the agent may open its own to-do on this widget with leaf task create even when no update on it is open, and the to-do shows beside the widget.

Full contract

Only a block widget whose x-content is markup may declare it, and markup content does not imply it: markup can itself be a control, or sit in a hidden panel.