# The component contract

Version 0.2. This document is the whole idea: if you can write a file that follows it, any host that implements it can run your component, and any model that reads [SKILL.md](SKILL.md) can write one.

## 1. What a component is

A component is **one self-contained file** that draws one piece of UI into a box it is given, and cleans up after itself when the box goes away.

There are two kinds:

| Kind | File | Runs as | Can touch the vault | Use it for |
| --- | --- | --- | --- | --- |
| **module** | `name.js` | An ES module in the host's page | Yes, through `ctx` | Anything that reads your notes |
| **html** | `name.html` | A document in a sandboxed iframe, no network | No | Code you did not write and have not reviewed, pasted AI artifacts, self-contained tools |

A module component looks like this:

```js
export const meta = {
  name: "Clock",
  version: "1.0.0",
  description: "Current time, updating every second.",
  settings: { seconds: true },
};

export default function clock(ctx) {
  const time = ctx.h("div", { class: "clock-time" });
  ctx.host.append(time);
  ctx.css(`.clock-time { font-size: 2em; color: var(--text-normal); }`);

  const tick = () => {
    time.textContent = new Date().toLocaleTimeString([], {
      timeStyle: ctx.settings.seconds ? "medium" : "short",
    });
  };
  tick();
  ctx.interval(tick, 1000);
}
```

That is the entire format: an optional `meta` export and a default-exported function that receives `ctx`. The function may be `async`. It may return a cleanup function, although registering cleanup through `ctx` is preferred.

A component file:

- has **no imports**. Everything it needs comes from `ctx` or the platform (DOM, `Intl`, `Math`, `Date`, `crypto.randomUUID`, canvas).
- uses **no globals**: not `window.*` state, not `document.body`, not `localStorage`, not `app` directly.
- keeps **data out of code**. Anything a user might change goes in `meta.settings` and is read from `ctx.settings`.

## 2. `meta`

All fields are optional. Hosts show `name` and `description` wherever a component is picked or trusted.

| Field | Type | Meaning |
| --- | --- | --- |
| `name` | string | Human name |
| `version` | string | Semver of the component itself |
| `description` | string | One sentence |
| `settings` | object | Default settings. Every key a component reads from `ctx.settings` should have a default here. |
| `needs` | string[] | What it uses, for disclosure: `"vault-read"`, `"vault-write"`, `"navigate"`, `"app"`. Hosts show this when asking for trust. It is a declaration, not a sandbox (see [Trust](#6-trust)). |
| `contract` | string | Contract version it was written for, currently `"0.2"` |

## 3. The `ctx` object

Every member is optional for a host to *enhance*, but every member listed here must *exist*, so a component never has to feature-test.

### Drawing

| Member | Signature | Notes |
| --- | --- | --- |
| `ctx.host` | `HTMLElement` | The component's box. Empty when the function is called. Append into it; never reach outside it. It is a size container (`container-type: inline-size`), so `@container` queries and `cqi` units work. Its width comes from the host; its height does not: it is as tall as what you put in it. So nothing in it can be `height: 100%` of it. Give charts and canvases a height through `aspect-ratio`, or an `em` height. |
| `ctx.h` | `(tag, props?, ...children) => Element` | Creates an element. `props` may hold `class`, `style` (an object, custom properties allowed), `on` (an object of event listeners), `dataset`, and any attribute. Children may be nodes, strings, numbers, arrays, `null`, `false` or `true`; the last three are skipped. |
| `ctx.svg` | `(tag, attrs?, ...children) => SVGElement` | Same, in the SVG namespace. |
| `ctx.css` | `(text) => void` | Adds a style sheet scoped to this component. Selectors are nested under the host automatically, so `.clock-time { … }` only matches inside this component. Removed on teardown. Scoping keeps your rules in, not other rules out: prefix class names with the component's name. |

### Theme

| Member | Signature | Notes |
| --- | --- | --- |
| `ctx.palette.dark` | `boolean` | Current theme mode. Kept up to date. |
| `ctx.palette.color` | `(name) => string` | A resolved colour string (`rgb(…)`), for canvas and computed styles. Names: `text`, `muted`, `faint`, `accent`, `bg`, `bgAlt`, `border`, `red`, `orange`, `yellow`, `green`, `cyan`, `blue`, `purple`, `pink`. |
| `ctx.palette.series` | `(n) => string[]` | `n` distinguishable theme colours for charts. |
| `ctx.palette.ramp` | `(t) => string` | A CSS colour for intensity `t` between 0 and 1, from background to accent. For DOM and SVG. |
| `ctx.palette.onChange` | `(fn) => void` | Called after the theme or accent changes. Unsubscribed on teardown. |

In CSS, prefer the host's own variables directly (`var(--text-normal)`, `var(--background-secondary)`, `var(--interactive-accent)`). They update with no code at all. `ctx.palette` is for the places CSS cannot reach, mainly canvas.

### Lifecycle

Everything registered here is undone automatically on teardown, in reverse order.

| Member | Signature | Notes |
| --- | --- | --- |
| `ctx.teardown` | `(fn) => void` | Runs `fn` when the component is removed or re-rendered. |
| `ctx.interval` | `(fn, ms) => stop` | `setInterval` that is cleared on teardown. Call `stop()` to clear it earlier. |
| `ctx.timeout` | `(fn, ms) => stop` | `setTimeout` that is cleared on teardown. Call `stop()` to cancel it earlier. |
| `ctx.frame` | `(fn) => stop` | Calls `fn(timestamp)` on every animation frame until teardown, or until `fn` returns `false`. Paused while the host is off screen. |
| `ctx.listen` | `(target, type, fn, options?) => stop` | `addEventListener` that is removed on teardown. Needed only for targets outside `ctx.host`, such as `document`. |
| `ctx.onResize` | `(fn) => void` | Calls `fn({ width, height, em })` once when the host is first laid out, and again whenever its size changes. Sizes are in CSS pixels; `em` is the pixels in one `em` at the host's font size, for fitting a whole number of em-sized cells. Do not change the host's size from inside `fn` (for example by setting a canvas's CSS height): that calls `fn` again, in a loop. |
| `ctx.signal` | `AbortSignal` | Aborted on teardown. Pass it to `fetch` or to your own listeners. |

### Data

| Member | Signature | Notes |
| --- | --- | --- |
| `ctx.query.notes` | `(filter?) => Note[]` | Notes from one cached snapshot of the vault, shared by every component on the page. Filter by `folder`, `tag` or a `where(note)` function. Returns plain arrays of plain objects (see below). |
| `ctx.query.text` | `(path) => Promise<string>` | The text of one file: a note, or a data file such as JSON or CSV. Empty if there is no such file. |
| `ctx.query.url` | `(path) => string` | A URL for a file in the vault, for `<img src>`, `<audio>` and similar. Each host makes its own kind of URL; never build one yourself. Empty if there is no such file, so check it before setting `src`: an empty `src` loads the page itself. |
| `ctx.note` | `{ path, frontmatter } \| null` | The note this component is mounted in, if any. |
| `ctx.settings` | `object` | `meta.settings` merged with the settings given at mount time. Read-only. |
| `ctx.state` | `object` | A plain object that survives re-renders of the same mount, but not a restart. A mount is this file, in this note, with this `key`; two mounts never share state. Use it for "which tab was selected". |
| `ctx.storage` | `{ get(key, fallback), set(key, value) }` | Small JSON values that persist, scoped to this component and mount. |

A `Note` is:

```ts
{
  path: string;          // "Journal/2026-03-04.md"
  name: string;          // "2026-03-04"
  folder: string;        // "Journal"
  tags: string[];        // ["journal", "health"], no leading "#"
  frontmatter: object;   // plain values, dates left as strings
  created: Date;
  modified: Date;
  day: string | null;    // "2026-03-04" when the name starts with a date, else null
  size: number;          // bytes
}
```

Plain arrays and `Date` objects, on purpose. No proxy arrays, no library date types.

### Host

| Member | Signature | Notes |
| --- | --- | --- |
| `ctx.navigate` | `(path, { newTab? }) => void` | Opens a note. |
| `ctx.runtime` | `string` | Which host is running this: `"dataviewjs"`, `"web"`, or a host's own name. For display and debugging only; do not branch behaviour on it. |
| `ctx.app` | `object \| undefined` | The raw host application (the Obsidian `App`), only when `meta.needs` includes `"app"`. An escape hatch. A component that uses it only works in that host. |

## 4. Lifecycle

1. **Mount.** The host creates an empty `ctx.host`, builds `ctx`, and calls the default export once.
2. **Re-render.** Some hosts re-run components when data changes, and a component cannot know when. DataviewJS, for example, re-runs every visible block a few seconds after any note in the vault changes, not only the note the block is in, and it does so without unloading the previous run. Whatever the host's runtime does, the host must make a re-render a full teardown followed by a fresh mount with a new `ctx.host`. That is the host's job, so a component never needs to detect a re-render. `ctx.state` carries over; nothing else does.
3. **Teardown.** Everything registered through `ctx` is undone, then any function the component returned is called.

The one test every component must pass: **mounted twice on the same page and re-rendered in the middle of an animation, nothing leaks and nothing collides.** No timers left running, no listeners left behind, no element ids shared between the two copies, no style bleeding from one into the other.

## 5. html components

An html component is a complete HTML document (or fragment) run inside an iframe with `sandbox="allow-scripts"` and a content security policy that blocks all network access. It cannot see the vault, the host page, cookies or storage.

Before the file runs, the host defines `window.cf`:

| Member | Meaning |
| --- | --- |
| `cf.settings` | The settings given at mount time |
| `cf.data` | Any data the host chose to pass in at mount time (for example, a list of note titles). Plain JSON. |
| `cf.dark` | Current theme mode |
| `cf.onTheme(fn)` | Called when the theme changes |

The host also sets the same CSS variables used in the host application (`--text-normal`, `--background-primary`, `--interactive-accent` and so on) on the iframe's root element, keeps them in sync, and sizes the iframe to its content.

This is the kind to use for code from a model or a person you do not trust, and it is how a pasted AI-generated HTML artifact becomes a component.

An html component is its own document, so the rules that protect a shared page do not apply inside it: ids and `px` are fine. The rules about the theme do: use the CSS variables, so it looks right in both themes. And it cannot use the network.

## 6. Trust

A module component runs with the same privileges as the host page. In Obsidian that means it can read and write any file in the vault. `meta.needs` states intent; it does not limit anything. The rules below exist because of that.

Hosts **must**:

1. Never run a component the user has not explicitly mounted.
2. Make the source one step away from the rendered component.
3. Run html components sandboxed as described in section 5.

Hosts **should**:

4. Support **pinning**: the user records a hash of the file they reviewed, and the host refuses to run it if the file changes, showing the new hash and the source instead. This matters most in synced vaults, where a file can change without you touching it.
5. Show `meta.needs` and a plain "runs JavaScript with access to your vault" notice for module components.

Users **should** read a component before mounting it, or have an AI read it for them with a prompt such as: "Review this component for anything that reads or writes files, touches the network, runs shell commands, or uses globals. List every such line."

## 7. What a host provides

A host is anything that can give a component a box and a `ctx`: a DataviewJS block, a web page, an Obsidian plugin with a layout grid. To conform to contract 0.2, a host:

- implements every member in section 3 (members it cannot support return empty values rather than being absent),
- follows the lifecycle in section 4,
- scopes `ctx.css` to the host element,
- follows the trust rules in section 6,
- supports html components as described in section 5, or refuses them clearly.

Two hosts exist so far, both in `cf.js`: a DataviewJS host for Obsidian (`mount`), and the generic `run` and `runHtml` that the article page uses to run the examples.

## 8. File conventions

- One component per file. Name the file after the component: `clock.js`, `heatmap.js`.
- A folder is fine when a component has an example or a README beside it: `clock/clock.js`.
- Keep components small. Past about 300 lines, split the idea into two components.
- Settings that hold lists or records are better kept in the host note's frontmatter and passed in, so the data lives with your notes and not in code.

## Changes

- **0.2** (2026-10-15). Added `ctx.query.url` for images and other files, and `em` in `ctx.onResize`. Stated that the host's height comes from its content, and that resizing the host inside `ctx.onResize` loops. Said which rules apply inside html components. A 0.1 component runs unchanged on a 0.2 host. These changes came from having a model write fourteen components from this document alone: it got stuck on images, two components grew without limit or redrew forever, and it had to read `cf.js` to learn what this page left out.
