# Forms for Typst templates

Give your own Typst functions a form, so people who fill in a report see labelled fields in Visual mode instead of function-call syntax.

A Typst template often ships its own functions: a cover page, an info box, a sign-off block. Whoever writes the report calls them, as in `#info-box(title: "Supply risk", tone: "risk")[...]`. If you maintain the template, you can describe each function's arguments once. Visual mode then shows every call to that function as a form: labelled fields, a list for fixed choices, a switch for yes or no. Editing a field rewrites only that one argument in the `.typ` file, so the report stays ordinary Typst source that compiles the same way.

## What the reader sees

A call on its own lines becomes a **card**. The card's header shows the form's name and, if you choose one, a short summary such as the box's title. Each argument gets a row with its label and a control.

A call inside a sentence, such as `#badge("review")`, becomes a **chip** in the line of text. Clicking the chip opens the same fields in a small popover.

Text fields and markup fields save when you press Enter (⌘ Enter or Ctrl Enter in a multi-line field) or leave the field. Esc puts the old value back. Lists and switches save at once. Every change is one undo step.

Moving the cursor into a call shows its source, like any other Visual mode element. **Edit in source** on the card does the same.

## Where to declare forms

There are two places, with the same format:

- **In a package.** Add a `[tool.oleafly]` section to the package's `typst.toml`. Typst keeps `[tool.*]` sections for other tools, so the package still installs and compiles everywhere. This works for `@local` packages and for `@preview` packages from Typst Universe.
- **In the project.** For templates kept as plain `.typ` files inside the project, create `oleafly-forms.toml` at the project root. Each form also names the file that defines the function, with `module`.

A package form:

```toml
[tool.oleafly]
schema = 1

[[tool.oleafly.forms]]
function = "info-box"
label = "Info box"
summary = "title"

  [[tool.oleafly.forms.fields]]
  arg = "title"
  kind = "text"
  label = "Heading"
  placeholder = "Short heading"
  required = true

  [[tool.oleafly.forms.fields]]
  arg = "tone"
  kind = "choice"
  label = "Tone"
  default = "note"
  options = [{ value = "note", label = "Note" }, { value = "risk", label = "Risk" }, "neutral"]

  [[tool.oleafly.forms.fields]]
  arg = 0
  kind = "content"
  label = "Text"
```

A project form, in `oleafly-forms.toml`:

```toml
[tool.oleafly]
schema = 1

[[tool.oleafly.forms]]
module = "template/memo.typ"
function = "memo-header"
label = "Memo header"
fields = [
  { arg = "to", kind = "text", label = "To" },
  { arg = "subject", kind = "text", label = "Subject" },
]
```

## The keys

`schema = 1` is required. A file without it, or with another number, is ignored.

Each `[[tool.oleafly.forms]]` entry has:

| Key | Required | Meaning |
| --- | --- | --- |
| `function` | Yes | The function's name, as one Typst identifier. Hyphens are fine. |
| `module` | In `oleafly-forms.toml` only | The project file that defines the function, relative to the project root, with `/` between folders. |
| `label` | No | The form's name on cards and chips. Defaults to the function's name. |
| `summary` | No | The argument whose value appears next to the label, written like a field's `arg`. |
| `fields` | Yes | One or more fields. |

Each field has:

| Key | Required | Meaning |
| --- | --- | --- |
| `arg` | Yes | A name for a named argument (`"title"`), or a number for a positional one (`0` is the first). A trailing `[...]` block counts as a positional argument after the ones in parentheses. |
| `kind` | Yes | `text`, `content`, `choice`, `number` or `toggle`. |
| `label` | Yes | The field's label. |
| `help` | No | A short note under the field. |
| `placeholder` | No | Grey text in an empty field. |
| `required` | No | `true` marks an empty field as missing and hides its **Clear** button. Oleafly never adds the argument by itself. |
| `default` | No | The value your function uses when the argument is left out. It is shown, never written. |

Kinds and their extra keys:

- `text` writes a Typst string. Add `multiline = true` for a larger box.
- `content` edits Typst markup and writes it as a `[...]` block. A string argument stays a string.
- `choice` needs `options`: a list of strings, or `{ value, label }` tables when the label differs from the value. Values are written as strings.
- `number` takes `integer`, `min`, `max` and `step`. Numbers have no units.
- `toggle` writes `true` or `false`.

## Which calls get a form

Oleafly follows the report's own `#import` and `#let` lines at the top level of the file:

- `#import "@local/acme-report:0.1.0" as acme` makes `#acme.info-box(...)` a form.
- `#import "@local/acme-report:0.1.0": info-box, status-badge as badge` makes `#info-box(...)` and `#badge(...)` forms.
- `#import "@local/acme-report:0.1.0": *` makes every function the package declares a form for.
- `#import "template/memo.typ": memo-header` matches a project form whose `module` is `template/memo.typ`. Paths are read from the report's own folder, or from the project root when they start with `/`.

The package version must match exactly. A `#let` with the same name, later in the file, replaces the form from that point on. Calls reached through another file that re-imports the package keep their source text.

A function named like a Typst element that Visual mode already draws, such as `table` or `figure`, keeps the built-in look when called by its bare name. `#acme.table(...)` or a renamed import still gets the form.

## Fields shown read-only

Some arguments can't be edited safely from a form. Their row shows the source with the reason, and **Edit in source** jumps to it:

- **Computed in the source:** the value is an expression, such as `title: data.title`.
- **Written as a different kind of value:** for example a number where the form expects text.
- **Not one of the choices:** a value your `options` don't list.
- **Set by a spread argument:** a call with `..args` passes values the form can't see, so every field is read-only.

When a field can't be added where it would go, for example a positional argument after a missing one, the form says so and offers **Add in source**.

## Mistakes in a definition

Open `oleafly-forms.toml`, or a vendored package's `typst.toml`, in Oleafly and each problem shows as a marker on its line: an unknown kind, a missing label, a duplicate form, a `module` outside the project. A broken form is left out and the others keep working. A file larger than 256 KB is not read.

## Sharing templates

Oleafly reads package forms from the same folders the compiler uses. A collaborator who doesn't have the `@local` package sees plain source and no errors. To share forms with everyone on a project, open the **Typst packages** dialog, turn on **Use packages from the project folder** and choose **Vendor packages into project**. The package, and its forms, then travel in the project's `typst-packages` folder (see [Typst projects](/docs/typst/)).
