# The component contract

> What defineTaggorComponent takes (props, events, slots, theming, data) and the limits the build enforces.

> Esta página ainda não tem tradução para o português; este é o texto em inglês. (This page is not translated yet: the English text follows.)

A component is one file whose default export is a `defineTaggorComponent` call, from the
`@taggor/component` SDK. The call declares everything Taggor needs to know about the component, its
_manifest_, and a `render` function that returns a Preact view. The build reads the manifest from the
source, checks it against the contract, and the runtime mounts the component in a Shadow DOM on your
site, with an error boundary around it.

The numbers and tables on this page are generated from the contract package, and the code is copied
from templates that have their own tests, so the page cannot drift from what the build checks.

## The shape of a component

```tsx title="src/index.tsx (excerpt of the faq-accordion template)"
import { defineTaggorComponent, prop, useLocale, useTrack } from "@taggor/component";

export default defineTaggorComponent({
  name: "faq-accordion",
  tag: "faq-accordion",
  title: "FAQ accordion",
  description: "Lists questions whose answers open on click or with the keyboard.",
  props: {
    headline: prop.string({ maxLength: 120, description: "Replaces the default heading." }),
    items: prop.array({
      description: "The questions and answers, in the site's language.",
      items: {
        type: "object",
        fields: {
          question: { type: "string", minLength: 1, maxLength: 200 },
          answer: { type: "string", minLength: 1, maxLength: 2000 },
        },
        required: ["question", "answer"],
      },
      minItems: 1,
      maxItems: 30,
      default: [
        {
          question: "Como funciona a entrega?",
          answer: "Enviamos para todo o Brasil em até 5 dias úteis.",
        },
        {
          question: "Posso trocar um produto?",
          answer: "Sim, em até 30 dias após o recebimento, sem custo.",
        },
        {
          question: "Quais formas de pagamento vocês aceitam?",
          answer: "Pix, boleto e cartão de crédito em até 12 vezes.",
        },
      ],
    }),
    multiple: prop.boolean({ description: "Lets several answers stay open at once." }),
  },
  events: {
    toggle: {
      description: "The visitor opened or closed an answer.",
      detail: {
        type: "object",
        fields: {
          index: { type: "number", integer: true, min: 0 },
          open: { type: "boolean" },
        },
        required: ["index", "open"],
      },
    },
  },
  data: {
    track: { "faq-open": { props: { index: { type: "number", integer: true, min: 0, max: 29 } } } },
  },
  parts: {
    question: { description: "Each question button." },
    answer: { description: "Each answer panel." },
  },
  cssVars: {
    "--faq-accent": { type: "color", default: "#0f766e", description: "Open question color." },
    "--faq-border": { type: "color", default: "#d1d5db", description: "Divider color." },
  },
  // locales and styles: see the template
  // render(props, context): next block
});
```

- `name` and `tag`: the component's id and the custom element's tag. A tag is lowercase, has a hyphen,
  and cannot start with `taggor-`.
- `title` and `description`: shown in the portal and to agents.
- `props`, `events`, `slots`, `parts`, `cssVars`, `data`: the sections below.

```tsx title="render of the same component"
  render: (props, context) => {
    const { t } = useLocale(context);
    const track = useTrack(context);
    const { part, emit } = context;
    const [open, setOpen] = useState<ReadonlySet<number>>(new Set());

    const toggle = (index: number) => {
      const opening = !open.has(index);
      const next = new Set(props.multiple ? open : []);
      if (opening) next.add(index);
      else next.delete(index);
      setOpen(next);
      emit("toggle", { index, open: opening });
      if (opening) track("faq-open", { index });
    };
    const allOpen = open.size === props.items.length;

    return (
      <section class="card">
        <div class="top">
          <h2>{props.headline ?? t("headline")}</h2>
          {props.multiple ? (
            <button
              type="button"
              class="all"
              onClick={() => {
                setOpen(new Set(allOpen ? [] : props.items.map((_item, index) => index)));
              }}
            >
              {allOpen ? t("closeAll") : t("openAll")}
            </button>
          ) : null}
        </div>
        {props.items.map((item, index) => {
          const expanded = open.has(index);
          return (
            <div class="item" key={index}>
              <h3>
                <button
                  type="button"
                  id={`q${String(index)}`}
                  part={part("question")}
                  aria-expanded={expanded ? "true" : "false"}
                  aria-controls={`a${String(index)}`}
                  onClick={() => {
                    toggle(index);
                  }}
                >
                  {item.question}
                </button>
              </h3>
              <div
                id={`a${String(index)}`}
                role="region"
                aria-labelledby={`q${String(index)}`}
                part={part("answer")}
                hidden={!expanded}
              >
                {item.answer}
              </div>
            </div>
          );
        })}
      </section>
    );
  },
});
```

`render` receives the typed `props` and a `context` with `emit` (events), `part` (theming) and what
the hooks need (`useLocale`, `useTrack`, `useTaggorData`, `useSlot`).

## Props

A prop is a typed, bounded value the host page sets as an attribute (`max-value="10"`) or as a
property on the element. The attribute name is the kebab-case form of the prop name. Every prop has a
type, and a value that does not fit it falls back to the default.

| Type | SDK builder | Options |
| --- | --- | --- |
| `string` | `prop.string()` | `minLength`, `maxLength`, `format` (`email`, `url`, `tel`, `date`, `date-time`), `default` |
| `number` | `prop.number()` | `min`, `max`, `integer`, `default`, `step` |
| `boolean` | `prop.boolean()` | `default` |
| `enum` | `prop.enum()` | `values`, `default` |
| `color` | `prop.color()` | `default` |
| `url` | `prop.url()` | `protocols`, `default` |
| `date` | `prop.date()` | `min`, `max`, `default` |
| `array` | `prop.array()` | `items`, `minItems`, `maxItems`, `default` |
| `object` | `prop.object()` | `fields`, `required`, `default` |

## Events

An event is declared with a description and the shape of its `detail`. The component calls
`emit("toggle", { index, open })`, and the page receives a DOM `CustomEvent` named
`<tag>:<event>`, here `faq-accordion:toggle`, with that `detail`:

```js
document.querySelector("faq-accordion").addEventListener("faq-accordion:toggle", (event) => {
  console.log(event.detail.index, event.detail.open);
});
```

## Slots and fallback

A slot is a place where the page can put its own HTML inside the component. Declare it in `slots` and
render a native `<slot name="…">` where it should appear. `useSlot("title").hasContent` tells the
component whether the page filled it.

```ts title="a component with one slot (imports left out)"
export default defineTaggorComponent({
  name: "fallback-card",
  tag: "fallback-card",
  title: "Fallback card",
  description: "A call to action whose title the page can replace; plain HTML stands in for it.",
  props: { headline: prop.string({ maxLength: 60, default: "Simule seu financiamento" }) },
  slots: { title: { description: "Replaces the default headline." } },
  styles: [
    ":host { display: block; min-height: 120px; padding: 8px; box-sizing: border-box }",
    "h3 { margin: 0 0 8px }",
  ],
  render: ({ headline }) => {
    const title = useSlot("title");
    return h(
      "section",
      // Exposed for the browser test: what `useSlot` reported on the last render.
      { "data-title-slot": title.hasContent ? "filled" : "empty" },
      h("h3", null, title.hasContent ? h("slot", { name: "title" }) : headline),
      h("button", { type: "button" }, "Simular"),
    );
  },
});
```

The HTML the page writes inside the tag that has no `slot` attribute is the _fallback_: it is what a
visitor sees before the component loads, or if it never does. How to write it is in
[Install the snippet](https://taggor.dev/docs/snippet.md).

## Fonts and the component's box

A font is a `.woff2` file in the component's folder, imported and registered with `useFont`;
`fontFamily` gives the name to write in the CSS. Every component that shows the font calls `useFont`,
and the file is one cached object for all of them. A `@font-face` rule in the component's CSS does not
apply inside a Shadow DOM.

Every component starts with `:host { display: block; contain: content }`. The containment keeps the
component from painting over the page, and it also cuts what an element paints outside the component's
own box: a `box-shadow`, a `drop-shadow()`, a focus outline pushed outward. Leave room inside the component (padding on an inner wrapper), or add
`:host { contain: none }` to its styles when the paint must leave the box.

## Theming

`parts` names the elements a page may style with `::part(name)`, and `cssVars` the CSS variables it may
set (`style="--faq-accent: #7c3aed"`), each with a type and a default. Nothing else of the component's
styles is reachable from the page, and the page's CSS cannot reach into the component.

## Data

A component that collects or records something declares it in `data`, and reads and writes it through
the `useTaggorData` and `useTrack` hooks. The data belongs to the customer; the visitor is never
tracked across sites, and no metric carries personal data.

```ts title="the data declaration of the contact-form template"
  data: {
    collections: {
      contacts: {
        fields: {
          name: { type: "string", minLength: 1, maxLength: 120 },
          email: { type: "string", format: "email" },
          message: { type: "string", maxLength: 2000 },
        },
        required: ["name", "email"],
      },
    },
    track: { "contact-submit": { props: { ok: { type: "boolean" } } } },
    personal: true,
    privacyPolicyUrl: "privacyPolicy",
  },
```

- `collections` are the tables the component can submit to, with typed fields. The component's data
  can be read back with `taggor data export`. The whole flow, with a complete example, is in
  [Forms and data](https://taggor.dev/docs/forms-and-data.md).
- `track` names the interactions to measure, with a few small typed props; never free text.
- `personal: true` makes every submission need the visitor's consent, and `privacyPolicyUrl` names the
  prop that holds the link to the site's privacy policy.

## Limits

The build refuses a manifest above these limits.

| What | Limit |
| --- | --- |
| Props | 50 |
| Events | 20 |
| Slots | 10 |
| Parts | 30 |
| CSS variables | 30 |
| Data collections | 10 |
| Fields per collection | 50 |
| Tracked interactions | 30 |
| Values of one enum | 50 |
| Manifest size (characters of JSON) | 65,536 |

## What the build checks

On `publish` the builder checks the manifest, types, size, CSS and security rules. A failure prints a
code with a page under [build diagnostics](https://taggor.dev/docs/diagnostics.md): what it means and how to fix it. The
JSON Schema of the manifest is public at
[`/schema/component-manifest/v1.json`](https://taggor.dev/schema/component-manifest/v1.json), so an editor can validate
a manifest and an agent can read the contract.
