> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getcliq.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Defining a hug

> define_hug for what a hug is asked and answers; mount_hug and the UI kit for its page; React; testing.

Hugs

A hug has two parts you write: its **definition** (`hug.ts`, with `define_hug`) and its **page** (`src/main.ts`, with
`mount_hug`). Frontend authors install one package, `@getcliqio/hug-frontend`. The [tutorial](https://docs.getcliq.io/hugs/tutorial.md)
builds one end to end; [mount\_hug in depth](https://docs.getcliq.io/hugs/mount-hug.md), [UI kit](https://docs.getcliq.io/hugs/ui-kit.md), [React](https://docs.getcliq.io/hugs/react.md),
[Files and links](https://docs.getcliq.io/hugs/files-and-links.md) and [Testing](https://docs.getcliq.io/hugs/testing.md) go further.

***

## The definition — `define_hug`

```ts hug.ts theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { define_hug, file } from '@getcliqio/hug-frontend/define';
import { z } from 'zod';

export default define_hug({
    name: 'review-batch',
    title: 'Review a batch of documents',
    description: 'One verdict per document; a note for each rejection; optional evidence.',
    params: z.object({ company: z.string(), documents: z.array(z.object({ id: z.string(), name: z.string() })) }),
    values: z.object({
        verdicts: z.array(z.object({ id: z.string(), ok: z.boolean(), note: z.string().max(500).optional() })),
        evidence: file({ accept: ['image/*', '.pdf'], optional: true }),
    }),
    summary: (v) => `${v.verdicts.filter((x) => x.ok).length} of ${v.verdicts.length} OK`,
});
```

| Field | |
| - | - |
| `name` | **Required.** The hug's name — its folder's name: lowercase letters, digits and `-` |
| `params` | **Required.** What it is asked (a Zod schema). Checked before the Hug opens; at most 256 KB |
| `values` | **Required.** What a valid answer is — a Zod schema, or **a function of the params** that returns one |
| `title`, `description` | For lists and for models: an LLM phase asking with this hug is told them, with the params as JSON Schema |
| `files` | Upload slots — by default, the top-level `file()` fields of `values` when it is a plain object. For an answer of several shapes (a union), list them: `files: (p) => [{ name: 'evidence', accept: ['.pdf'] }]` — see [`checklist-signoff`](https://github.com/getcliqio/cliq/tree/main/examples/hugs/checklist-signoff) |
| `summary` | `(values, params) => string` — the one line shown in the inbox, notifications and the responses list (default "Responded") |

* **`file({ accept, max_bytes, multiple, optional })`** makes a field an upload slot. `accept` takes MIME types
  (`image/*`) and extensions (`.pdf`); `multiple` takes up to 10 files. The answer holds a reference
  (`{ artifact_id, name, size, content_type, sha256 }`), never the bytes.
* **`.describe('…')`** on a field is its label in the default read-only view.
* **Parameterized hugs** — `values`, `files` and `summary` may depend on the params, for an answer whose shape depends
  on what was asked: one verdict per document asked about; `form`'s fields, listed in its params. Both checks — the
  page's and the agent's — use the Hug's own params.
* **Rules across fields** go in the schema: `superRefine` with paths (`ctx.addIssue({ path: [i, 'note'], … })`), or
  `discriminatedUnion` for answers of different shapes (approve or reject).
* `define_hug` is pure (Zod only), so the same file runs in the page, in agent code and — compiled to
  `dist/definition.mjs` — in cliqd and LLM agents.

The definition's methods, for tests and agents: `check_params(raw)`, `check(raw, params)` (both return
`{ ok: true, value }` or `{ ok: false, problems: [{ path, message }] }`), `values_schema(params)`, `files(params)`,
`summary(values, params)`, `describe()`.

***

## The page — `mount_hug`

```ts src/main.ts theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { html, mount_hug } from '@getcliqio/hug-frontend';
import '@getcliqio/hug-frontend/ui';
import review_batch from '../hug';

mount_hug(review_batch, {
    initial: (p) => ({ verdicts: p.documents.map((d) => ({ id: d.id, ok: true })) }),
    submit: (v) => (v.verdicts.every((x) => x.ok) ? 'Approve all' : 'Send verdicts'),
    edit: ({ params, value, set, problems }) => html`…`,
    // view: optional — a default read-only view is drawn from the definition
});
```

| Option | |
| - | - |
| `edit(args)` | **Required.** Draws the editor; redrawn after every change |
| `initial(params)` | The answer before the person does anything — used when there is no draft and no earlier answer (default `{}`) |
| `submit(value)` | The Hub's Submit button: a label, or `{ label, tone: 'danger' }` (default "Submit") |
| `view(value, { params, answer, hug })` | Draws a submitted answer read-only (default: drawn from the definition) |
| `summary(value)` | Overrides the definition's summary for this page — rarely needed |
| `target` | Where to draw (default `#app`, else the body) |

`edit` receives:

| | |
| - | - |
| `params`, `value` | What the Hug was asked, and the answer so far |
| `set(path, v)` | Change one part: `set('severity', 'SEV1')`, `set('verdicts[2].note', '…')`. `undefined` or `null` clears it |
| `replace(v)` | Swap in a whole new answer — for answers that take different shapes (approve or reject; *Can't answer*) |
| `problems` | The first problem per path (`problems['verdicts[1].note']`; `''` for the answer as a whole) |
| `problem_list`, `valid` | Every problem, and whether the answer is valid |
| `hug` | The runtime API, for anything else (attachments, `read_file`, `open_link`, …) |

**What `mount_hug` does for you:** connects to the Hub page and applies its theme; starts from the person's draft,
else their earlier answer, else `initial`; checks the params (a bad one shows an error panel); after every `set` or
`replace`, checks the answer with the definition and tells the Hub whether it's valid, its problems, the Submit label
and the summary; switches between `edit` and `view`; keeps the frame's height fitted.

**Fields that depend on other fields** are plain conditions in `edit` (it redraws on every change). Clear a field you
hide with `set(path, undefined)`, and put the same rule in the definition.

**`html`** is [lit-html](https://lit.dev/docs/libraries/standalone-templates/): `@event=${fn}`, `.property=${v}`,
lists with `map`, `nothing` for an empty branch.

***

## The UI kit

`import '@getcliqio/hug-frontend/ui'` registers web components that match the Hub — its font, colours (light and
dark) and spacing — and are keyboard-reachable and labelled. `form` and `approval` are built only from these.

| Element | Draws |
| - | - |
| `<cliq-field .input=${input}>` or `<cliq-field label type>` | One field — text (`multiline`), number, boolean, enum, file — with label, help, required mark and its problem (`.problem`); emits `change` |
| `<cliq-fields .inputs=${inputs}>` | A list of fields, required first; emits `change` with `{ name, value }` |
| `<cliq-choice-group .choices=${[{ value, label, tone }]}>` | Radio or segmented choices; `tone: 'danger'` for a red one |
| `<cliq-file key="evidence">` | An upload slot: the Hub's file picker, the uploaded files, remove |
| `<cliq-attachment .ref=${file}>` | One file: name, type and size; **Preview** (text, markdown, images) and **Download** |
| `<cliq-context .blocks=${blocks}>` | Context blocks — text, markdown, files, a conversation (what `form` shows with its question) |
| `<cliq-markdown>` | Sanitized markdown |
| `<cliq-problems>` | A list of problems |

**Theme:** the kit uses CSS variables the Hub sets — `--cliq-bg`, `--cliq-panel`, `--cliq-ink`, `--cliq-ink-2`,
`--cliq-ink-3`, `--cliq-line`, `--cliq-acc`, `--cliq-ok`, `--cliq-warn`, `--cliq-bad`. Use them in your own CSS too.
A page that doesn't use the kit can still load the Hub's font from `@getcliqio/hug-frontend/ui/fonts.css`.

***

## React

`cliq hug new <name> --react` scaffolds a React page:

```tsx src/main.tsx theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { HugRoot, useHug } from '@getcliqio/hug-frontend/react';
import review_batch from '../hug';

function Editor() {
    const { params, value, set, problems } = useHug(review_batch, {
        initial: (p) => ({ verdicts: p.documents.map((d) => ({ id: d.id, ok: true })) }),
    });
    // …draw with React; call set(path, v) on change
}
```

`useHug(definition, { initial, submit })` returns `{ params, value, set, replace, problems, hug }`; `<HugRoot>` mounts
it and `<DefaultView>` draws the default read-only view.

***

## What a page can't do

The page runs in a sandboxed frame with no network ([Security](https://docs.getcliq.io/hugs/security.md)):

| Not allowed | Instead |
| - | - |
| `fetch`, WebSocket, outside images, fonts or scripts | Everything in the bundle; data in params; files through attachments (`hug.read_file`, `<cliq-attachment>`) |
| `localStorage`, cookies, IndexedDB | State in memory — the Hub keeps the person's draft |
| `window.open`, navigating links, form posts | `hug.open_link(url)` — a URL not in the params is shown to the person first |
| `alert`, `confirm`, `prompt` | In-page UI |
| Downloads | `hug.download(ref)` |
| Submitting by itself | The Hub's Submit button |

***

## Testing

The definition is plain code — test `check`, `summary` and `files` directly (see the [tutorial](https://docs.getcliq.io/hugs/tutorial.md#8-test-the-definition)).

For the page, **`mock_host`** from `@getcliqio/hug-frontend/testing` stands in for the Hub:

```ts theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { mock_host } from '@getcliqio/hug-frontend/testing';

const host = mock_host({ definition: review_batch, params: PARAMS });
mount_hug(review_batch, { connect: { transport: host.transport }, edit, initial });
// …interact with the DOM, then:
expect(host.last_values()).toMatchObject({ valid: false, submit: { label: 'Send verdicts' } });
```

`mock_host({ params, mode, mine, viewing, draft, attachments, upload, read, … })` records everything the page sends
(`sent`, `last_values()`); `upload` and `read` say what the person picks and what a file holds.

**In the browser:** `cliq hug dev <dir> [--params p.json] [--attachments dir] [--view answer.json]` shows the hug on
cliqd's local Hug page, rebuilding and reloading as you edit, with a panel of what it sends.

***

## Building

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
cliq hug build hugs/review-batch           # runs the hug's own build, then checks the bundle
cliq hug build hugs/review-batch --check   # in CI: fails if dist/ is missing or out of date; writes nothing
```

`cliq hug build` runs the hug's build (`npm run build`), writes `hug.json` and `definition.mjs`, and checks the
bundle: **text files only** (binary assets inlined), every file it loads in the bundle, **at most 5 MB and 200
files**, `hug.json` and `definition.mjs` present. CliqHub checks the same rules when the bundle is uploaded.

The scaffolded `build.mjs` passes `hug_esbuild_plugins()` from `@getcliqio/hug-frontend/build` to esbuild, for both
the page and `definition.mjs`. Today it keeps only zod's English error messages — `import { z } from 'zod'` would
otherwise bring about 40 languages, some 500 KB, into each. Keep it in your build so later build fixes reach your hug
too; leave it out only if your hug switches zod to another language.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.