<!-- AUTO-GENERATED by scripts/generate-docs.mjs — do not edit; edit docs/llm/reference/_fragments/<name>.md and run `pnpm docs:generate`. -->

# Inputs & Forms

Use when you capture data from the user.

## Index

- [`choice`](#choice) — Checkbox, switch and radio-group form toggles
- [`color-picker`](#color-picker) — Color selection with hex input and native panel
- [`combobox`](#combobox) — Autocomplete/multiselect input with filtering
- [`date-picker`](#date-picker) — Calendar date selection with single/range modes
- [`file-dropzone`](#file-dropzone) — File upload with drag & drop and type/size validation
- [`form`](#form) — Form/field orchestrator with validation and ARIA wiring
- [`nested-select`](#nested-select) — Hierarchical multi-select with tri-state checkboxes
- [`rich-text-editor`](#rich-text-editor) — Contenteditable region with a formatting toolbar
- [`select`](#select) — Single-select dropdown (no search) with listbox ARIA
- [`slider`](#slider) — Single or range slider with keyboard and pointer drag
- [`sms-editor`](#sms-editor) — Textarea with live SMS encoding + segment counting
- [`tags-input`](#tags-input) — Token/tag entry with validation and dedupe
- [`textarea-emoji`](#textarea-emoji) — Textarea with character counter and emoji picker
- [`time-picker`](#time-picker) — Time selection with 12h/24h and minute stepping
- [`whatsapp-editor`](#whatsapp-editor) — Textarea + formatting toolbar + live preview for WhatsApp messages

---

## choice

Form toggles: `Checkbox` (tri-state), `Switch` (on/off), `RadioGroup` (roving radios).

```html
<button data-c42-checkbox>Accept</button>
<button data-c42-switch>Notifications</button>
<div data-c42-radio-group>
  <div data-c42-radio data-value="a">A</div>
  <div data-c42-radio data-value="b">B</div>
</div>
```

```ts
import { Checkbox, Switch, RadioGroup } from '@42/core/choice';
new Checkbox(el, { checked: false, indeterminate: false, disabled: false });
new Switch(el, { checked: false });
new RadioGroup(el, { defaultValue: 'a' });
```

Events: `checkbox:change` → `{ checked, indeterminate }`, `switch:change` → `{ checked }`, `radio:change` → `{ value: string | null }`

---

## color-picker

> Deep dive: [`docs/llm/color-picker.md`](../../../docs/llm/color-picker.md)

Color selection with hex input and native color picker panel.

---

## combobox

> Deep dive: [`docs/llm/combobox.md`](../../../docs/llm/combobox.md)

Autocomplete/multiselect input with filtering and keyboard navigation.

---

## date-picker

> Deep dive: [`docs/llm/date-picker.md`](../../../docs/llm/date-picker.md)

Calendar date selection with single/range modes, min/max constraints, locale formatting.

---

## file-dropzone

> Deep dive: [`docs/llm/file-dropzone.md`](../../../docs/llm/file-dropzone.md)

File upload with drag & drop, type/size validation, previews, multi-file support.

---

## form

> Deep dive: [`docs/llm/form.md`](../../../docs/llm/form.md)

Form/field orchestrator. Validates fields, wires ARIA, reflects `data-state` — no styling. Coordinates native inputs and other `@42/core` controls.

```html
<form data-c42-form>
  <div data-c42-field>
    <label for="email">Email</label>
    <input id="email" name="email" type="email" data-c42-validate="required email" />
    <span data-c42-field-error hidden></span>
  </div>
  <div data-c42-field>
    <label for="pwd">Password</label>
    <input id="pwd" name="pwd" type="password" data-c42-validate="required" data-c42-minlength="8" />
    <span data-c42-field-error hidden></span>
  </div>
  <button type="submit">Sign up</button>
</form>
```

```ts
import { Form } from '@42/core/form';
const form = new Form(root, {
  mode: 'blur', // 'submit' (default) | 'blur' | 'change' | 'input'
  validators: { pwd: (v) => (v.length >= 8 ? null : 'Too short') },
  messages: { required: 'Required field' },
});
form.on('form:submit', (e) => console.log(e.detail.values));
form.on('form:invalid', (e) => console.log(e.detail.errors));
```

Discovers fields via `[data-c42-field]` (control = `[data-c42-field-control]` or the
first native `input/select/textarea`; field name = the control's `name`). Built-in
rules in `data-c42-validate`: `required email url number integer`, plus constraint
attrs `data-c42-minlength|maxlength|min|max|pattern` (native `minlength`/`pattern`/…
also read). Per-field message override: `data-c42-error-<rule>`. Radio groups and
checkboxes are supported (unchecked = empty → fails `required`).

Wiring: sets `aria-invalid`, links `[data-c42-field-error]` via `aria-describedby`,
toggles the error's `hidden`, and `data-state="valid|invalid"` on the wrapper. On a
`<form>` root sets `noValidate` and `preventDefault`s submit. After the first submit
attempt, fields re-validate on `input` to clear errors live.

Options: `mode`, `validators`, `messages`
Methods: `validate()`, `validateField(name)`, `submit()`, `getValues()`, `setValues(map)`, `setError(name, msg)`, `clearErrors()`, `reset()`, `getState()`
Events: `form:submit` → `{ values }`, `form:invalid` → `{ errors }`, `form:change` → `{ name, value, values }`, `form:reset`

---

## nested-select

> Deep dive: [`docs/llm/nested-select.md`](../../../docs/llm/nested-select.md)

Hierarchical multi-select with groups, tri-state checkboxes, search filtering.

---

## rich-text-editor

Toolbar of `execCommand` formatting actions wired to a `contenteditable` region.

```html
<div data-c42-rich-text-editor>
  <div data-c42-rte-toolbar>
    <button data-c42-rte-command="bold" aria-label="Bold"><b>B</b></button>
    <button data-c42-rte-command="italic" aria-label="Italic"><i>I</i></button>
    <button data-c42-rte-command="insertUnorderedList" aria-label="List">• List</button>
    <button data-c42-rte-command="createLink" aria-label="Link">Link</button>
  </div>
  <div data-c42-rte-content data-placeholder="Write something…"></div>
</div>
```

```ts
import { RichTextEditor } from '@42/core/rich-text-editor';
const editor = new RichTextEditor(root, { value: '<p>Hi</p>' });
editor.format('bold');
editor.on('richtexteditor:change', (e) => console.log(e.detail.html));
```

Toolbar buttons carry `data-c42-rte-command` (any `document.execCommand` name);
pass a static argument with `data-value` (e.g. `formatBlock` → `data-value="h2"`).
`createLink` resolves its URL from the `getLinkUrl` option (defaults to
`window.prompt`). Toggle commands reflect their state on the button via
`aria-pressed` + `data-active`. An empty editor gets `data-empty` so the theme
can show `data-placeholder`.

> `document.execCommand` is deprecated but still the most broadly supported
> inline-formatting API (with native undo). It is feature-detected and guarded.

Options: `value` (initial HTML), `getLinkUrl` (() => string | null)
Methods: `format(command, value?)`, `getHTML()`, `setHTML(html)`, `focus()`
Events: `richtexteditor:command` → `{ command, value? }`, `richtexteditor:change` → `{ html }`

---

## select

Single-select dropdown (no search). Listbox ARIA, keyboard nav, `aria-activedescendant`.

```html
<div data-c42-select>
  <button data-c42-select-trigger><span data-c42-select-value>Choose…</span></button>
  <div data-c42-select-listbox>
    <div data-c42-select-option data-value="a">Apple</div>
    <div data-c42-select-option data-value="b">Banana</div>
  </div>
</div>
```

```ts
import { Select } from '@42/core/select';
new Select(root, { defaultValue: null, placeholder: 'Choose…' });
```

Options: `defaultValue`, `placeholder`
Events: `select:change` → `{ value: string, label: string }`, `select:open`, `select:close`

---

## slider

Single-value or two-thumb range slider with full keyboard control and pointer
dragging. Single vs range is inferred from the number of thumbs. Positions are
exposed as `--c42-slider-start-percent` / `--c42-slider-end-percent` custom
properties for CSS.

```html
<div data-c42-slider>
  <div data-c42-slider-track>
    <div data-c42-slider-range></div>
    <div data-c42-slider-thumb aria-label="Value"></div>
  </div>
</div>
```

For a range, add two thumbs with `data-thumb="start"` / `data-thumb="end"`.

```ts
import { Slider } from '@42/core/slider';
const s = new Slider(root, { min: 0, max: 100, step: 5, value: 40 });
s.setValue(60);
```

Keyboard: arrows (±step), PageUp/PageDown (±10×step), Home/End (min/max).
Options: `min` (0), `max` (100), `step` (1), `value` (number | [start, end]), `orientation` ('horizontal'|'vertical'), `label`
Methods: `setValue(number | [start, end])`, `getValue()`
Events: `slider:change` → `{ value, values }`

---

## sms-editor

Enhances a plain-text `<textarea>` and reports the things that affect SMS cost:
encoding (GSM-7 vs UCS-2), weighted character count and billable segment count.
No formatting toolbar (SMS is plain text). Optional emoji picker via the same
`renderPicker` plugin as `textarea-emoji` (emoji force UCS-2). No styling applied.

```html
<div data-c42-sms-editor>
  <textarea data-c42-sms-input placeholder="Write an SMS…"></textarea>
  <div class="c42-sms-editor-status">
    <span data-c42-sms-counter></span>
    <span data-c42-sms-segments></span>
    <span data-c42-sms-encoding></span>
    <!-- emoji affordance bottom-right (picker drops below) -->
    <span class="c42-sms-editor-emoji">
      <button data-c42-sms-trigger aria-label="Emoji">🙂</button>
      <div data-c42-sms-picker hidden></div>
    </span>
  </div>
  <!-- optional live preview (filled with the escaped message text) -->
  <div data-c42-sms-preview></div>
</div>
```

```ts
import { SmsEditor } from '@42/core/sms-editor';
const sms = new SmsEditor(root, { maxSegments: 1 });
sms.on('smseditor:change', (e) => {
  const { encoding, length, segments, overLimit } = e.detail;
});
```

Segmentation (3GPP TS 23.038): GSM-7 = 160 chars single / 153 per part;
UCS-2 = 70 / 67. GSM-7 extension chars (`^ { } \ [ ] ~ | €`) count as two.
The root reflects `data-encoding="GSM-7|UCS-2"`; when `maxSegments` is exceeded
it gets `data-over-limit`. By default there is no `maxlength` (multi-segment is
allowed); set `maxLength` to enforce a hard cap.

Options: `value`, `maxLength`, `maxSegments`, `emojis`, `renderPicker`
Methods: `insertText(text)`, `getText()`, `getSegmentInfo()`, `focus()`, `value` get/set
Events: `smseditor:change` → `{ text, encoding, length, segments, remaining, overLimit }`
Pure helpers (no DOM): `segment(text)`, `detectEncoding(text)`, `countChars(text, encoding)`

Optional preview: add a `[data-c42-sms-preview]` element and the controller
fills it with the live, escaped message text (toggling `data-empty` when blank).
Device preview (`@42/styles`): wrap that element in `.c42-sms-editor-device` to
render it on an iPhone header background (image ships with `@42/styles`). Purely
opt-in — omit the wrapper for a plain bubble, or omit the element entirely to
keep the editor counters-only. Tune with `--c42-sms-device-pad-top`,
`--c42-sms-device-pad-x`.

```html
<div class="c42-sms-editor-device">
  <div data-c42-sms-preview class="c42-sms-editor-preview"></div>
</div>
```

---

## tags-input

Token/tag entry with validation, max, deduplication.

```html
<div data-c42-tags>
  <div data-c42-tags-list></div>
  <input data-c42-tags-input placeholder="Add tag..." />
</div>
```

```ts
import { TagsInput } from '@42/core/tags-input';
new TagsInput(root, { max: 5, allowDuplicates: false, defaultTags: ['one'] });
```

Options: `max`, `allowDuplicates`, `delimiters`, `defaultTags`, `validate`
Events: `tags:change` → `{ tags: string[] }`

---

## textarea-emoji

Textarea with character counter and pluggable emoji picker.

```html
<div data-c42-textarea-emoji>
  <textarea data-c42-textarea-emoji-input rows="3" placeholder="Write..."></textarea>
  <div>
    <button data-c42-textarea-emoji-trigger>😀</button>
    <span data-c42-textarea-emoji-counter></span>
  </div>
  <div data-c42-textarea-emoji-picker></div>
</div>
```

```ts
import { TextareaEmoji } from '@42/core/textarea-emoji';
new TextareaEmoji(root, {
  maxLength: 200,
  emojis: true,
  renderPicker: (container, insert) => {
    // render your emoji grid, call insert(emoji) on selection
  },
});
```

Options: `maxLength`, `emojis`, `renderPicker`
Events: `textarea-emoji:input` → `{ value, length }`, `textarea-emoji:emoji` → `{ emoji, value }`

---

## time-picker

> Deep dive: [`docs/llm/time-picker.md`](../../../docs/llm/time-picker.md)

Time selection with 12h/24h format, minute stepping, confirm/cancel flow.

---

## whatsapp-editor

> Deep dive: [`docs/llm/whatsapp-editor.md`](../../../docs/llm/whatsapp-editor.md)

Enhances a `<textarea>` whose value IS the WhatsApp markup you send (`*bold*`,
`_italic_`, `~strike~`, `` `mono` ``, `>` quotes, `-`/`1.` lists). Toolbar +
keyboard shortcuts wrap/unwrap the selection; an optional emoji picker uses the
same `renderPicker` plugin as `textarea-emoji`; a live, HTML-escaped preview
shows how WhatsApp will render it. No styling applied.

```html
<div data-c42-whatsapp-editor>
  <!-- the editor card: toolbar + input + footer (styled via .c42-whatsapp-editor-surface) -->
  <div class="c42-whatsapp-editor-surface">
    <div data-c42-wa-toolbar>
      <button data-c42-wa-command="bold" aria-label="Bold"><b>B</b></button>
      <button data-c42-wa-command="italic" aria-label="Italic"><i>I</i></button>
      <button data-c42-wa-command="strikethrough" aria-label="Strikethrough">S</button>
      <button data-c42-wa-command="monospace" aria-label="Monospace">&lt;/&gt;</button>
      <button data-c42-wa-command="blockquote" aria-label="Quote">&gt;</button>
      <button data-c42-wa-command="bullet" aria-label="Bulleted list">•</button>
      <button data-c42-wa-command="ordered" aria-label="Numbered list">1.</button>
      <button data-c42-wa-command="clear" aria-label="Clear formatting">⌫</button>
    </div>
    <textarea data-c42-wa-input placeholder="Write a message…"></textarea>
    <!-- footer: counter left, emoji affordance bottom-right (picker drops below) -->
    <div class="c42-whatsapp-editor-footer">
      <span data-c42-wa-counter></span>
      <span class="c42-whatsapp-editor-emoji">
        <button data-c42-wa-trigger aria-label="Emoji">🙂</button>
        <div data-c42-wa-picker hidden></div>
      </span>
    </div>
  </div>
  <!-- optional bubble toolbar shown above the selection -->
  <div data-c42-wa-floating class="c42-whatsapp-editor-floating" hidden>
    <button data-c42-wa-command="bold" aria-label="Bold"><b>B</b></button>
    <button data-c42-wa-command="italic" aria-label="Italic"><i>I</i></button>
    <button data-c42-wa-command="strikethrough" aria-label="Strikethrough">S</button>
    <span class="c42-whatsapp-editor-separator" aria-hidden="true"></span>
    <button data-c42-wa-command="monospace" aria-label="Monospace">&lt;/&gt;</button>
  </div>
  <!-- preview lives OUTSIDE the surface, as a separate block below the editor -->
  <div data-c42-wa-preview aria-live="polite"></div>
</div>
```

```ts
import { WhatsappEditor } from '@42/core/whatsapp-editor';
const editor = new WhatsappEditor(root, { maxLength: 4096 });
editor.on('whatsappeditor:change', (e) => sendToApi(e.detail.text)); // text = markup
editor.format('bold'); // toggle markers around the selection
```

Toolbar buttons carry `data-c42-wa-command`. Inline markers
(`bold|italic|strikethrough|monospace`) wrap/unwrap the selection; block kinds
(`blockquote|bullet|ordered`) toggle a line prefix across the selected lines
(numbered lists renumber from 1); `clear` strips all formatting from the
selection (or the whole message if nothing is selected). Keyboard: Cmd/Ctrl+B
(bold), Cmd/Ctrl+I (italic). The picker reflects `data-picker-open` on the root
and closes on outside-click / Escape. The preview escapes all input before
interpreting markers (XSS-safe).

Optional floating selection toolbar: add a `[data-c42-wa-floating]` element with
`[data-c42-wa-command]` buttons anywhere inside the root, **or** pass
`floating: true` to have the controller inject a default themed bubble menu
(bold/italic/strikethrough/monospace) when you don't supply one. Authored markup
always wins; the flag only decides who provides the UI. Either way the controller
shows it centered above the selection (positioned via a mirror-div caret
measurement), hides it on collapse/blur/Escape/scroll/resize, and reflects each
inline marker's active state on every command button (toolbar + floating) via
`aria-pressed` and `data-active`.

Themed layout (`@42/styles`): wrap the toolbar, textarea and footer in
`.c42-whatsapp-editor-surface` to get the editor "card" (white surface, rounded,
subtle shadow, fixed toolbar separated by a divider, seamless borderless input).
The preview is intentionally **outside** that surface — a separate block below
the editor — so it never looks like part of the compose box. Structure is
layout-only; the controller finds its parts by `data-*` regardless.

Options: `value`, `maxLength` (default 4096), `emojis`, `floating`, `renderPicker`
Methods: `format(marker)`, `applyBlock(kind)`, `clearFormatting()`, `insertText(text)`, `getText()`, `getHTML()`, `focus()`, `isFloatingOpen()`, `value` get/set
Events: `whatsappeditor:change` → `{ text, html, length }`, `whatsappeditor:format` → `{ marker, text }`
Pure helpers (no DOM): `toPreviewHTML(text)`, `toggleMarker(value, start, end, marker)`, `toggleLinePrefix(value, start, end, kind)`, `stripFormatting(text)`, `isMarkerActive(value, start, end, marker)`, `WHATSAPP_MARKERS`

Optional device preview (`@42/styles`): wrap the preview in
`.c42-whatsapp-editor-device` to render it on a WhatsApp phone background (image
ships with `@42/styles`). Purely opt-in — omit the wrapper for the plain bubble.
Tune with `--c42-wa-device-pad-top`, `--c42-wa-device-pad-x`, `--c42-wa-device-height`.

```html
<div class="c42-whatsapp-editor-device">
  <div data-c42-wa-preview class="c42-whatsapp-editor-preview"></div>
</div>
```
