entasis 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,7 +33,7 @@ export declare const componentMcpRegistry: {
33
33
  readonly 'toggle-button-group': "\n# ToggleButtonGroup Component\n\nToggleButtonGroup renders an ordered list of ToggleButton items and exposes the pressed values as one bindable value.\n\n## Basic Usage\n\n```svelte\n<script lang=\"ts\">\n\tlet formatting = $state(['bold']);\n</script>\n\n<ToggleButtonGroup\n\tbind:value={formatting}\n\tlabel=\"Text formatting\"\n\tcolor=\"neutral\"\n\titems={[\n\t\t{ value: 'bold', children: 'Bold' },\n\t\t{ value: 'italic', children: 'Italic' },\n\t\t{ value: 'underline', children: 'Underline' }\n\t]}\n/>\n```\n\n## Props\n\n- **items**: ToggleButtonGroupItem[] (required) - Ordered button configurations. Each item carries a unique `value` plus ToggleButton props (no `variant`, `color`, or `size`; pressed state lives on the group value).\n- **label**: string (required) - Accessible name for the group.\n- **value**: string[] | string | undefined (bindable) - The only pressed-state source. `type=\"multiple\"` holds every pressed value as an array; `type=\"single\"` holds the checked value.\n- **defaultValue**: same shape as value - Initial pressed state when value is omitted.\n- **onValueChange**: (value) => void - Called once with the updated group value after a toggle.\n- **size**: 'small' | 'normal' | 'large' - Applied to every item.\n- **color**: Colors - Applied to every item.\n- **variant**: 'outline' | 'ghost' - Applied to every item. Defaults to 'ghost'.\n- **disabled**: boolean - Disables every item.\n- **joined**: boolean (default: false) - Renders the buttons as contiguous segments.\n- **type**: 'single' | 'multiple' (default: 'multiple') - `'multiple'` lets any number of buttons be pressed (`role=\"group\"`, `aria-pressed`) and its value is a `string[]`. `'single'` makes the selection exclusive: the value is a single string, the root becomes `role=\"radiogroup\"`, each button `role=\"radio\"` with `aria-checked`, pressing one clears the others, and the checked radio cannot be unpressed.\n- **class**: string - Additional CSS classes for the root.\n- **theme**: ToggleButtonGroupThemeProps - Theme overrides.\n\n## Examples\n\n### Icon Toolbar\n\n```svelte\n<ToggleButtonGroup\n\tbind:value={formatting}\n\tlabel=\"Text formatting\"\n\titems={[\n\t\t{ value: 'bold', prefix: textBIcon, label: 'Bold' },\n\t\t{ value: 'italic', prefix: textItalicIcon, label: 'Italic' },\n\t\t{ value: 'underline', prefix: textUnderlineIcon, label: 'Underline' }\n\t]}\n/>\n```\n\n### Joined Segments\n\n```svelte\n<ToggleButtonGroup\n\tjoined\n\tvariant=\"outline\"\n\tcolor=\"neutral\"\n\tlabel=\"Text formatting\"\n\tvalue={['bold']}\n\titems={[\n\t\t{ value: 'bold', prefix: textBIcon, label: 'Bold' },\n\t\t{ value: 'italic', prefix: textItalicIcon, label: 'Italic' },\n\t\t{ value: 'underline', prefix: textUnderlineIcon, label: 'Underline' }\n\t]}\n/>\n```\n\n### Single Selection (radio group)\n\n```svelte\n<script lang=\"ts\">\n\tlet alignment = $state('left');\n</script>\n\n<ToggleButtonGroup\n\ttype=\"single\"\n\tlabel=\"Alignment\"\n\tbind:value={alignment}\n\titems={[\n\t\t{ value: 'left', children: 'Left' },\n\t\t{ value: 'center', children: 'Center' },\n\t\t{ value: 'right', children: 'Right' }\n\t]}\n/>\n```\n\n### Change Handler\n\n```svelte\n<ToggleButtonGroup\n\tlabel=\"Density options\"\n\titems={[\n\t\t{ value: 'compact', children: 'Compact' },\n\t\t{ value: 'comfortable', children: 'Comfortable' }\n\t]}\n\tonValueChange={(value) => {\n\t\tconsole.log(value);\n\t}}\n/>\n```\n\n## Theme\n\n- **root**: Main button group container styles.\n\n## Accessibility\n\n- `type=\"multiple\"`: the root is `role=\"group\"` named by `label`; each ToggleButton exposes its independent state through `aria-pressed`.\n- `type=\"single\"`: the root is `role=\"radiogroup\"`; buttons are `role=\"radio\"` with `aria-checked`.\n- The group is a single tab stop (roving tabindex): Tab enters it, ArrowLeft / ArrowRight move between buttons (looping, mirrored in RTL), Home / End jump to the first / last, Space or Enter toggles the focused button.\n- Prefer SegmentedControl for a visually segmented exclusive choice; use `type=\"single\"` when you want toggle-button styling with exclusive semantics.\n\n## Theme example\n\n```svelte\n<ToggleButtonGroup\n\tlabel=\"Options\"\n\titems={items}\n\ttheme={{\n\t\troot: {\n\t\t\tbase: 'flex items-center gap-1'\n\t\t}\n\t}}\n/>\n```\n";
34
34
  readonly 'toggle-menu': "\n# ToggleMenu Component\n\nToggleMenu renders standalone toggles, independent toggle groups, exclusive radio groups, menu buttons, and custom snippet controls as one accessible, responsive toolbar.\n\n## Basic Usage\n\n```svelte\n<script lang=\"ts\">\n\timport { ToggleMenu, type ToggleMenuItem } from 'entasis/toggle-menu';\n\timport type { MenuItem } from 'entasis/menu';\n\timport { eyeIcon } from 'entasis/icons/eye';\n\timport { textBIcon } from 'entasis/icons/textB';\n\timport { textItalicIcon } from 'entasis/icons/textItalic';\n\timport { paletteIcon } from 'entasis/icons/palette';\n\n\tconst getTextColorOptions = (): MenuItem[] => [\n\t\t{ type: 'option', title: 'Red', color: 'danger' },\n\t\t{ type: 'option', title: 'Blue', color: 'info' }\n\t];\n\n\tlet items = $state<ToggleMenuItem[]>([\n\t\t{ type: 'toggle', prefix: eyeIcon, label: 'Preview', value: true },\n\t\t{\n\t\t\ttype: 'group',\n\t\t\tlabel: 'Text formatting',\n\t\t\tvalue: ['bold'],\n\t\t\titems: [\n\t\t\t\t{ value: 'bold', prefix: textBIcon, label: 'Bold' },\n\t\t\t\t{ value: 'italic', prefix: textItalicIcon, label: 'Italic' }\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\ttype: 'menu',\n\t\t\tlabel: 'Text color',\n\t\t\tprefix: paletteIcon,\n\t\t\tmenu: getTextColorOptions\n\t\t}\n\t]);\n</script>\n\n<ToggleMenu bind:items label=\"Editor tools\" />\n```\n\n## Item Types\n\n- **toggle**: A standalone ToggleButton configuration with `type: 'toggle'`. Its boolean `value` stores the pressed state.\n- **group**: A labeled collection of independent toggles. Its `items` is an array of button configurations, each with its own `value` string, and the group `value` is the `string[]` of pressed values.\n- **radio-group**: A labeled collection of mutually exclusive toolbar choices. Its `items` is an array of radio configurations, each with its own `value` string, and the group `value` is the checked string.\n- **menu**: A toolbar menu button. Its `menu` is a `MenuItem[]` or reactive factory and automatically becomes a submenu inside More.\n- **custom**: A snippet control with `type: 'custom'`. Its `children` snippet receives the resolved toolbar state and a `reference` attachment for the primary focusable element. `overflowItems` is required so the control has an explicit representation inside More.\n\nGroups are joined by default. Set `joined: false` when their buttons should remain visually separate.\n\n## Props\n\n- **items**: ToggleMenuItem[] (required, bindable) - Ordered toggles, groups, menu buttons, and custom controls. Pressed state lives on the items, so bind them to keep it.\n- **label**: string (required) - Accessible name for the toolbar.\n- **size**: 'small' | 'normal' | 'large' - Default size inherited by items.\n- **color**: Colors - Default color inherited by items.\n- **variant**: 'outline' | 'ghost' - Default variant inherited by items.\n- **disabled**: boolean - Disables every item.\n- **onItemsChange**: (items) => void - Called once with the complete updated item list.\n- **class**: string - Additional classes for the toolbar root.\n- **theme**: ToggleMenuThemeProps - Theme overrides for root, rail, units, and overflow trigger.\n\nItem-level size, color, variant, and disabled values override toolbar defaults. Toggle, group, and radio-group callbacks receive their native checked boolean, pressed-value array, or selected value.\n\n## Menu Buttons\n\nA `menu` item renders a normal button with menu-button ARIA state and inherited toolbar styling. Pass a function when checked rows or other menu state must be recalculated reactively. `closeOnItemClick` defaults to `true`. ToggleMenu owns the PopupMenu attachments, tooltip, and overflow submenu representation.\n\n## Custom Controls\n\n`ToggleMenuCustomPayload` contains:\n\n- **reference**: Attachment for the custom control's primary focusable element. Applying it includes the control in roving keyboard navigation.\n- **size**, **color**, **variant**, **disabled**: Resolved toolbar or item values.\n- **overflowed**: Whether the logical unit currently lives in More.\n\n`overflowItems` accepts either a `MenuItem[]` or a function returning one. Use the function form when the overflow representation depends on reactive state. Popup triggers should attach both the toolbar `reference` and their own popover reference to the same button.\n\n## Overflow\n\nToggleMenu stays on one row. When complete logical units no longer fit, it moves them into a More popup rather than wrapping or splitting a group. Toggle overflow rows use `menuitemcheckbox`; radio rows use `menuitemradio`. Both expose `aria-checked` and remain open while changing state. Menu buttons become submenus from the same menu data. Custom controls contribute their declared `overflowItems` as one logical unit.\n\n## Keyboard And Tooltips\n\n- The toolbar uses one roving tab stop.\n- ArrowLeft and ArrowRight move between visible controls; Home and End move to the edges.\n- Arrow navigation through a radio group does not change its value. Space, Enter, or click selects the focused radio.\n- Disabled and overflowed controls are excluded from navigation.\n- Icon-only toggle and menu controls automatically use their `label` as tooltip content. Custom snippets own their tooltip and accessible label.\n- Use `radio-group` for exclusive choices inside ToggleMenu and SegmentedControl for standalone exclusive controls.\n";
35
35
  readonly 'selection-menu': "\n# SelectionMenu Component\n\nSelectionMenu renders ToggleMenu controls anchored to a non-collapsed document selection. It accepts the same toolbar item props directly, filters the selection to a target container, preserves the range while controls are pressed, and positions the toolbar through Popover.\n\n## Basic Usage\n\n```svelte\n<script lang=\"ts\">\n\timport { SelectionMenu } from 'entasis/selection-menu';\n\timport type { ToggleMenuItem } from 'entasis/toggle-menu';\n\n\tlet items = $state<ToggleMenuItem[]>([\n\t\t{ type: 'toggle', label: 'Bold', prefix: boldIcon },\n\t\t{ type: 'toggle', label: 'Comment', prefix: commentIcon }\n\t]);\n</script>\n\n<div>\n\t<article>Select text in this article.</article>\n\t<SelectionMenu bind:items label=\"Selection tools\" />\n</div>\n```\n\nWith no target prop, SelectionMenu watches its parent. Pass a selector or an HTMLElement when the selection container is elsewhere:\n\n```svelte\n<SelectionMenu target=\"#editor\" bind:items label=\"Editor tools\" />\n<SelectionMenu target={editorElement} bind:items label=\"Editor tools\" />\n```\n\n## Props\n\n- **target**: HTMLElement | string | null - Parent by default; selectors resolve in the same Document or ShadowRoot. Null disables tracking.\n- **items**: ToggleMenuItem[] (bindable) - Canonical toolbar item list passed directly to ToggleMenu. Pressed state lives on the items.\n- **label**: string - Accessible name passed directly to ToggleMenu.\n- **color / variant / disabled**: ToggleMenu defaults inherited by every item.\n- **onItemsChange**: (items) => void - Called with the complete updated item list after any control changes.\n- **class / theme**: ToggleMenu root class and theme overrides.\n- **children**: Optional temporary replacement for the ToggleMenu body while retaining the same selection tracker. Toolbar props remain required.\n- **enabled**: boolean = true - Temporarily suppresses the menu without changing the target.\n- **position**: Popover placement = 'top' - Preferred placement around the selected range.\n- **offset**: number = 8 - Gap from the selected range.\n- **size**: size token = 'normal' - Shared ToggleMenu item and Popover size.\n- **transition**: Popover transition options - Enter and exit transition overrides.\n- **directedTransition**: boolean = true - Enters from the resolved placement.\n- **closeOnEscape**: boolean = true - Escape dismisses the current selection.\n- **closeOnClickOutside**: boolean = true - Outside clicks dismiss the current selection.\n- **onSelect**: (payload: SelectionMenuSelection | null) => void - The pick event: receives cloned valid ranges and clear\n events. Callback-naming decision: the document owns the text selection, so SelectionMenu has no\n controlled `selection` prop to change and cannot use the `onSelectionChange` state family.\n- **onAfterOpen / onAfterClose**: post-transition lifecycle callbacks receiving SelectionMenuPayload.\n- **popover**: Props forwarded to the floating Popover panel as one object - `{ class, theme }`.\n- **contentClass**: string - Additional classes for the advanced custom-content wrapper.\n- **selectionTheme**: SelectionMenuThemeProps - Popover panel and custom-content theme overrides.\n\n## Target Resolution\n\nBoth selection endpoints must be inside the resolved target. Whitespace-only, collapsed, or non-rendered ranges do not open the menu. String selectors use standard querySelector semantics, so use a selector that uniquely identifies the intended container.\n\n## Accessibility\n\nDirect mode inherits ToggleMenu's toolbar semantics, roving keyboard focus, disabled-item handling, tooltips, and responsive More menu. Escape and click-outside dismissal are provided by Popover. Custom-content mode is responsible for its own accessible role and keyboard model.\n";
36
- readonly ask: "\n# Ask\n\nImport from `entasis/ask`.\n\nMount Ask to host programmatic requests created by ask. Requests use the existing Form inputs and validation boundary inside Dialog. AskResult reports submitted and value; cancellation preserves the current form value. Submission failures reject the request. Mount the host before requesting a form.\n";
36
+ readonly ask: "\n# Ask\n\nImport from `entasis/ask`.\n\nMount Ask to host programmatic requests created by ask. Requests use the existing Form inputs and validation boundary inside Dialog, including input types the app registered with registerFormInputs (see Form), so a request can arrive as plain data. AskResult reports submitted and value; cancellation preserves the current form value. Submission failures reject the request. Mount the host before requesting a form.\n";
37
37
  readonly field: "\n# Field\n\nImport Field, createFieldState, and the FieldState type from entasis/field. Field is the shared\nvalidation and accessibility owner used by Entasis form controls. It renders field structure\naround a custom control and does not create another form or validation system.\n\n## Controller\n\ncreateFieldState receives id, type, value, errors, and focused. Use getter/setter properties\nfor values bound to component props; plain initial values also work for standalone controls.\nOptional name, required, disabled, visible, size, density, onValidate, and onValueChange\nconfigure the same owner used by built-in controls.\n\nThe controller exposes value, setValue(value), validate(), checkSchema(value), isValid,\nerrors, errorMessages, hasError, focused, disabled, required, size, and density.\nUse bind:value on field.value or call field.setValue for user edits. Accepted changes publish\none onValueChange callback each. Unchanged values and parent state synchronization do not\npublish callbacks. Disabled controllers reject user edits.\n\nSpread field.controlAttrs on the native control and attach field.control to register its node.\nThe attributes provide id, name, disabled, required, aria-invalid, and aria-describedby.\nThe controller exposes id, labelId, and errorId for more specialized controls. Field connects\nits label and error messages to this control, preserving unrelated accessible descriptions.\n\nfieldSchemas exports the built-in optional and required schemas. onValidate supplements that\nschema: a message, message array, or true means invalid; false, null, or undefined means valid.\nOptional empty values skip custom validation. validate returns a tuple containing the error\nflag and schema-normalized value.\n\n## Wrapper props\n\n- field: the shared FieldState controller.\n- children: custom control content.\n- as: native wrapper element; defaults to div. Use fieldset for grouped controls.\n- labelFor: associated control ID, or false for aria-labelledby; defaults to field.id.\n- size: small, normal, or large control geometry and label typography.\n- density: compact, normal, or comfortable internal spacing, independent of size.\n- labelPosition: top or left; left switches to side labels once the field itself is at least 32rem wide (a container query on the field, not the viewport).\n- fieldAttrs: native attributes for the wrapper. Put native control attributes on the control.\n- class and theme: wrapper classes and Field part overrides.\n\nSlots include header, label, actions, prefix, suffix, footer, description, helper,\nerrorsContainer, and error. Field renders required indicators and validation errors.\n\n## Form participation\n\nA Form input with type: 'field' supplies fieldType and a snippet accepting FieldState<fieldType>.\nThe entry inherits InputProps, including defaultValue, required, disabled, size, density,\nonValidate, and onValueChange. The controller receives the resolved Form size and density.\nIts value participates in inferred values, validation, submission, and Form bind:value.\nThe existing type: 'custom' entry remains display-only and contributes no value key.\n";
38
38
  readonly calendar: "\n# Calendar\n\nDate-based calendar primitives for single, range, and multiple selection. CalendarPrimitive is the visual primitive; CalendarInput wraps single and range modes in the Form Field contract.\n\n## Import\n\n```svelte\n<script lang=\"ts\">\n import { CalendarPrimitive, CalendarInput } from '../components/Form/Calendar/index.ts';\n</script>\n```\n\n## Single date\n\n```svelte\n<script lang=\"ts\">\n let value = $state<Date | null>(null);\n</script>\n\n<CalendarPrimitive type=\"calendar\" bind:value />\n```\n\n## Range\n\n```svelte\n<script lang=\"ts\">\n let value = $state<[Date | null, Date | null] | null>(null);\n</script>\n\n<CalendarPrimitive type=\"calendar-range\" bind:value />\n```\n\n## Multiple dates\n\n```svelte\n<script lang=\"ts\">\n let value = $state<Date[]>([]);\n</script>\n\n<CalendarPrimitive type=\"calendar-multiple\" bind:value />\n```\n\n## Main props\n\n- `type`: `'calendar' | 'calendar-range' | 'calendar-multiple'`.\n- `value`: bindable value matching the selected type.\n- `view`: `'single' | 'double'`. Double view falls back to one month when the calendar container is narrower than 576px.\n- `disabledDates`: individual dates or inclusive `[start, end]` ranges.\n- `minDate` / `maxDate`: selection bounds.\n- `events`: values with `start` and `end`; matching events are exposed to the custom cell snippet.\n- `cell`: custom day renderer receiving the complete Calendar cell payload.\n- `onViewChange`: receives the first and last visible month after pointer, keyboard, or controlled-value navigation.\n\n## Keyboard\n\nThe visible month title opens an in-place month and year chooser composed from two scroll areas. Each list uses roving focus with ArrowUp/ArrowDown and Home/End; Escape returns to the day grid.\n\nDay focus is roving. Arrow keys move by day or week, Home/End move to week boundaries, PageUp/PageDown move by month, and Shift+PageUp/PageDown move by year. Enter or Space uses the native day button activation.\n\nMonth navigation slides in chronological direction while the viewport transitions to its new intrinsic height. Double view responds to the calendar container rather than the browser viewport, so it safely falls back to one month in narrow layouts. Both animations respect reduced-motion preferences.\n\n## State contract\n\n- **value**: current bindable editable value.\n- **defaultValue**: initial value used only when `value` is omitted.\n- **onValueChange**: called with the new value when the component changes it.\n\n";
39
39
  readonly checkbox: "\n# Checkbox\n\nImport from `entasis/checkbox`.\n\nCheckbox is a boolean Form control. value is bindable, defaultValue initializes an uncontrolled value, and onValueChange reports changes. required, disabled, errors, label, and fieldAttrs use the shared Field contract. indeterminate controls the mixed presentation. Checkbox shares the CheckboxesInput theme owner.\n";
@@ -44,7 +44,7 @@ export declare const componentMcpRegistry: {
44
44
  readonly 'date-input': "\n# DateInput\n\nMasked native text entry composed with Field and DateSelector. The bindable value is a native `Date | null`.\n\n## Import\n\n```svelte\n<script lang=\"ts\">\n import { DateInput } from '../components/Form/DateInput/index.ts';\n</script>\n```\n\n## Usage\n\n```svelte\n<script lang=\"ts\">\n let value = $state<Date | null>(null);\n</script>\n\n<DateInput label=\"Start date\" bind:value />\n```\n\n## Date formatting\n\nUse `format` with `dd`, `mm`, `yy`, and `yyyy` segments. Supported values include `dd/mm/yyyy`, `mm/dd/yyyy`, `mm/yyyy`, and `yyyy/mm/dd`. The `separator` prop replaces the slash in the rendered mask.\n\n## Selector options\n\n- `presets`: date shortcuts shaped as `{ label, value }`.\n- `disabledDates`: individual dates or inclusive date ranges.\n- `minDate` / `maxDate`: calendar selection bounds.\n- `calendarView`: `'single' | 'double'`.\n- `mobileSheet`: renders the DateSelector as a bottom sheet below 768px.\n- `closeOnSelect`: closes after choosing a date; defaults to `false`.\n- `onSelect`: the pick event -- fires only when a date is chosen from the selector or its\n presets; `onValueChange` is the value state change and reports both typed and picked dates.\n\n```svelte\n<DateInput\n label=\"Delivery date\"\n bind:value\n minDate={new Date()}\n presets={[\n { label: 'Tomorrow', value: new Date(2026, 6, 16) },\n { label: 'Next week', value: new Date(2026, 6, 22) }\n ]}\n/>\n```\n\nThe selector never opens on focus, so tabbing through the field does not pop it. Open it with ArrowDown while the input has focus, or from the full-height calendar button (which carries `aria-haspopup=\"dialog\"`, `aria-expanded`, and `aria-controls`). It stays open after selection unless `closeOnSelect` is enabled. Calendar days support arrow, Home/End, and PageUp/PageDown navigation.\n\n## State contract\n\n- **value**: current bindable editable value.\n- **defaultValue**: initial value used only when `value` is omitted.\n- **onValueChange**: called with the new value when the component changes it.\n\n";
45
45
  readonly 'date-selector': "\n# DateSelector\n\nPopover calendar composition for selecting one date, a range, or multiple dates. It uses Entasis's Popover, CalendarPrimitive, Button, and transitionSize attachment.\n`DateSelector` is the raw popover primitive; `DateSelectorInput` wraps it in the standard field\nchrome (label, description, errors, form registration) and extends InputProps — field type 'date'\nfor `mode=\"date\"`, 'calendar-range' for `mode=\"range\"` (`mode=\"multiple\"` has no field type and\nstays primitive-only).\n\n## Import\n\n```svelte\n<script lang=\"ts\">\n import { DateSelector, DateSelectorInput } from '../components/Form/DateSelector/index.ts';\n</script>\n```\n\n## Basic usage\n\n```svelte\n<script lang=\"ts\">\n let value = $state<Date | null>(null);\n</script>\n\n<!-- Field variant: label/description/errors like every other form input -->\n<DateSelectorInput\n\tlabel=\"Due date\"\n\tdescription=\"We'll remind you the day before\"\n\tbind:value\n/>\n\n<!-- Raw popover primitive -->\n<DateSelector bind:value />\n```\n\n## DateSelectorInput (field variant)\n\nAccepts every field prop from InputProps — `label`, `description`, `name`, `required`, `errors`,\n`onValidate`, `helper`, snippets, … — plus the selector props (`mode`, `presets`, `trigger`,\n`closeOnSelect`, `position`, `minDate`/`maxDate`, …). `theme` is split: `theme.selector` for the\npopover parts, `theme.field` for the field wrapper.\n\n## Selection modes\n\n- `mode=\"date\"` binds `Date | null`.\n- `mode=\"range\"` binds `[Date | null, Date | null] | null`.\n- `mode=\"multiple\"` binds `Date[]`.\n\nThe selector stays open after selection by default. Set `closeOnSelect` to close after a date, a complete range, or a multiple-date change.\n\n## Presets\n\n```svelte\n<DateSelector\n bind:value\n presets={[\n { label: 'Today', value: new Date() },\n { label: 'Tomorrow', value: new Date(2026, 6, 16) }\n ]}\n/>\n```\n\nEach preset value must match the active mode. Presets follow the same `closeOnSelect` policy as calendar selection.\n\n## Custom trigger\n\nThe `trigger` prop follows Popover's trigger contract. A trigger snippet receives `PopoverState`; attach `popover.reference` to the trigger element and call `popover.toggle()`.\n\n## Calendar configuration\n\nUse `view`, `weekStartsOnMonday`, `weekdayLength`, `locale`, `minDate`, `maxDate`, and `disabledDates` to configure the embedded calendar. Set `mobileSheet` to render the selector as a bottom sheet below 768px.\n\nThe content container animates intrinsic height changes. Month navigation uses a direction-aware slide and respects reduced-motion preferences.\n\n## State contract\n\n- **value**: current bindable editable value.\n- **defaultValue**: initial value used only when `value` is omitted.\n- **onValueChange**: called with the new value when the component changes it.\n- **open**: bindable popover visibility.\n- **defaultOpen**: initializes an uncontrolled selector once.\n- **onOpenChange**: receives the new boolean once for each opening or closing interaction.\n\nSelecting the current date or preset does not emit another value callback. With closeOnSelect,\nthat selection can still close the popover and emit one onOpenChange callback.\n\n";
46
46
  readonly 'file-input': "\n# FileInput Component\n\nThe FileInput component provides file upload functionality with drag-and-drop support and an inline list of the selected files (image files get a thumbnail preview). It supports a single-file mode and a multiple-file mode.\n\n## Basic Usage\n\n```svelte\n<script lang=\"ts\">\n\tlet file = $state<File | null>(null);\n</script>\n\n<FileInput label=\"Upload File\" bind:value={file} />\n```\n\n## Props\n\nExtends all Field component props plus:\n\n### Core Props\n- **mode**: 'single' | 'multiple' (default: 'single') - Single-file or multiple-file mode\n- **value**: File | null in single mode, File[] | null in multiple mode (bindable) - Selected file(s)\n- **defaultValue**: same type as `value` - Initial selection when `value` is omitted\n- **types**: string[] (default: ['image/*']) - Accepted MIME types or extensions (e.g. ['image/*'], ['.pdf', '.docx']); used for the native `accept` attribute and drag validation\n- **maxSize**: number (default: 50 MB) - Maximum file size in bytes; larger files are rejected\n- **maxFiles**: number (default: 1) - Maximum number of files (multiple mode caps the combined selection)\n- **clickable**: boolean (default: true) - Whether clicking the dropzone opens the native file picker\n\n### Display Props\n- **placeholder**: string (default: 'Click or drag files here') - Text shown in the empty dropzone\n- **fileList**: Snippet - Custom rendering of the selected files list\n- **file**: Snippet - Custom rendering of each selected file row\n- **fileListClass** / **fileClass** / **placeholderClass**: string - Extra classes for those parts\n\n### Field Props (inherited)\n- **label**: string | Snippet - Field label\n- **description**: string | Snippet - Helper text\n- **required**: boolean - Mark as required\n- **disabled**: boolean - Disable input\n- **size**: 'small' | 'normal' | 'large'\n\n### Event Props\n- **onValueChange**: (value: File | null) => void in single mode, (value: File[] | null) => void in multiple mode - Called when the selection changes\n- **onReject**: (rejections: { file: File; reason: 'type' | 'size' | 'duplicate' | 'limit' }[]) => void - Called with the files rejected by type, size, duplicate or count validation\n\n### Styling Props\n- **class**: string - Additional CSS classes\n- **theme**: ComponentTheme - Custom theme overrides\n\n## Structure\n\n```\n<Field>\n\t<Label />\n\t<Description />\n\t<Dropzone> <!-- inputContainer -->\n\t\t<Placeholder /> <!-- shown while no file is selected -->\n\t\t<FileList> <!-- shown once files are selected -->\n\t\t\t<File /> <!-- thumbnail, name, size, remove button -->\n\t\t\t<AddMoreButton /> <!-- multiple mode, while below maxFiles -->\n\t\t</FileList>\n\t</Dropzone>\n\t<Error />\n</Field>\n```\n\n## Examples\n\n### Basic File Input\n```svelte\n<script lang=\"ts\">\n\tlet file = $state<File | null>(null);\n</script>\n\n<FileInput\n\tlabel=\"Upload Document\"\n\ttypes={['.pdf', '.doc', '.docx']}\n\tbind:value={file}\n/>\n```\n\n### Multiple Files\n```svelte\n<script lang=\"ts\">\n\tlet files = $state<File[]>([]);\n</script>\n\n<FileInput\n\tmode=\"multiple\"\n\tmaxFiles={5}\n\tlabel=\"Upload Documents\"\n\ttypes={['.pdf', '.doc', '.docx']}\n\tbind:value={files}\n/>\n```\n\n### Image Upload with Preview\n```svelte\n<script lang=\"ts\">\n\tlet avatar = $state<File | null>(null);\n</script>\n\n<FileInput\n\tlabel=\"Profile Picture\"\n\tbind:value={avatar}\n\ttypes={['image/*']}\n/>\n```\n\n### Restrict File Types\n```svelte\n<script lang=\"ts\">\n\tlet document = $state<File | null>(null);\n</script>\n\n<FileInput\n\tlabel=\"Upload PDF\"\n\tbind:value={document}\n\ttypes={['.pdf']}\n\tdescription=\"PDF files only\"\n/>\n```\n\n### With File Size Limit\n```svelte\n<script lang=\"ts\">\n\tlet image = $state<File | null>(null);\n</script>\n\n<FileInput\n\tlabel=\"Upload Image\"\n\tbind:value={image}\n\ttypes={['image/*']}\n\tmaxSize={5 * 1024 * 1024}\n\tdescription=\"Maximum 5MB\"\n/>\n```\n\n### Required File\n```svelte\n<script lang=\"ts\">\n\tlet resume = $state<File | null>(null);\n</script>\n\n<FileInput\n\tlabel=\"Resume\"\n\tbind:value={resume}\n\ttypes={['.pdf', '.doc', '.docx']}\n\trequired\n/>\n```\n\n### Disabled State\n```svelte\n<FileInput\n\tlabel=\"Uploaded File\"\n\tvalue={existingFile}\n\tdisabled\n/>\n```\n\n### Avatar Upload\n```svelte\n<script lang=\"ts\">\n\tlet avatar = $state<File | null>(null);\n\n\tfunction handleUpload(file: File | null) {\n\t\tif (!file) return;\n\t\tconsole.log('Uploading:', file.name);\n\t\t// Upload to server\n\t}\n</script>\n\n<FileInput\n\tlabel=\"Profile Picture\"\n\tbind:value={avatar}\n\ttypes={['image/png', 'image/jpeg']}\n\tmaxSize={2 * 1024 * 1024}\n\tdescription=\"PNG or JPEG, max 2MB\"\n\tonValueChange={handleUpload}\n/>\n```\n\n### Handling Rejected Files\n```svelte\n<script lang=\"ts\">\n\tlet image = $state<File | null>(null);\n\tlet rejectionMessage = $state('');\n</script>\n\n<FileInput\n\tlabel=\"Upload Image\"\n\tbind:value={image}\n\ttypes={['image/*']}\n\tmaxSize={1024 * 1024}\n\tonReject={(rejections) => {\n\t\trejectionMessage = rejections\n\t\t\t.map((rejection) => `${rejection.file.name}: ${rejection.reason}`)\n\t\t\t.join(', ');\n\t}}\n/>\n\n{#if rejectionMessage}\n\t<p class=\"text-danger-readable text-sm\">{rejectionMessage}</p>\n{/if}\n```\n\n### Document Upload Form\n```svelte\n<script lang=\"ts\">\n\tlet documents = $state<File[]>([]);\n\n\tasync function handleSubmit(event: SubmitEvent) {\n\t\tevent.preventDefault();\n\t\tconst formData = new FormData();\n\t\tdocuments.forEach((file, i) => {\n\t\t\tformData.append(`document${i}`, file);\n\t\t});\n\n\t\tawait fetch('/api/upload', {\n\t\t\tmethod: 'POST',\n\t\t\tbody: formData\n\t\t});\n\t}\n</script>\n\n<form onsubmit={handleSubmit}>\n\t<FileInput\n\t\tmode=\"multiple\"\n\t\tmaxFiles={10}\n\t\tlabel=\"Supporting Documents\"\n\t\tbind:value={documents}\n\t\ttypes={['.pdf', '.doc', '.docx']}\n\t\tdescription=\"Upload relevant documents\"\n\t\trequired\n\t/>\n\n\t<Button type=\"submit\" disabled={documents.length === 0}>\n\t\tSubmit Application\n\t</Button>\n</form>\n```\n\n### Image Gallery Upload\n```svelte\n<script lang=\"ts\">\n\tlet images = $state<File[]>([]);\n</script>\n\n<FileInput\n\tmode=\"multiple\"\n\tmaxFiles={20}\n\tlabel=\"Upload Photos\"\n\tbind:value={images}\n\ttypes={['image/*']}\n\tmaxSize={10 * 1024 * 1024}\n\tdescription=\"Select multiple images (max 10MB each)\"\n/>\n\n<p>Selected: {images.length} images</p>\n```\n\n### With Upload Progress\n```svelte\n<script lang=\"ts\">\n\tlet file = $state<File | null>(null);\n\tlet progress = $state(0);\n\tlet uploading = $state(false);\n\n\tasync function handleUpload(selectedFile: File | null) {\n\t\tif (!selectedFile) return;\n\t\tuploading = true;\n\t\tprogress = 0;\n\n\t\tconst formData = new FormData();\n\t\tformData.append('file', selectedFile);\n\n\t\t// Simulate upload progress\n\t\tconst interval = setInterval(() => {\n\t\t\tprogress += 10;\n\t\t\tif (progress >= 100) {\n\t\t\t\tclearInterval(interval);\n\t\t\t\tuploading = false;\n\t\t\t}\n\t\t}, 200);\n\t}\n</script>\n\n<FileInput\n\tlabel=\"Upload File\"\n\tbind:value={file}\n\tonValueChange={handleUpload}\n\tdisabled={uploading}\n/>\n\n{#if uploading}\n\t<Meter value={progress} />\n{/if}\n```\n\n### CV Upload\n```svelte\n<script lang=\"ts\">\n\tlet cv = $state<File | null>(null);\n</script>\n\n<FileInput\n\tlabel=\"Upload CV/Resume\"\n\tbind:value={cv}\n\ttypes={['.pdf', '.doc', '.docx']}\n\tmaxSize={5 * 1024 * 1024}\n\tdescription=\"PDF or Word document, max 5MB\"\n\trequired\n/>\n```\n\n### Attachment Field\n```svelte\n<script lang=\"ts\">\n\tlet attachments = $state<File[]>([]);\n\n\tfunction removeFile(index: number) {\n\t\tattachments = attachments.filter((_, i) => i !== index);\n\t}\n</script>\n\n<FileInput\n\tmode=\"multiple\"\n\tmaxFiles={Infinity}\n\tlabel=\"Attachments\"\n\ttypes={['*/*']}\n\tbind:value={attachments}\n\tdescription=\"Add any supporting files\"\n/>\n\n{#if attachments.length > 0}\n\t<div class=\"file-list\">\n\t\t{#each attachments as file, i}\n\t\t\t<div class=\"file-item\">\n\t\t\t\t<span>{file.name}</span>\n\t\t\t\t<Button\n\t\t\t\t\tsize=\"small\"\n\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\tonclick={() => removeFile(i)}\n\t\t\t\t>\n\t\t\t\t\tRemove\n\t\t\t\t</Button>\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n{/if}\n```\n\n## Drag and Drop\n\nFileInput supports drag-and-drop:\n- Drag files over the drop zone\n- Visual feedback while dragging (the dropzone reflects an idle / potential / valid / invalid state)\n- Drop to select files\n- Works alongside click-to-browse (disable with `clickable={false}`)\n\n## File List\n\nOnce files are selected the dropzone shows them inline:\n- Images show a thumbnail preview\n- Other files show their name\n- File size displayed\n- Remove button for each file\n- In multiple mode, an \"Add more files\" button appears while below `maxFiles`\n\n## Validation\n\nFileInput validates:\n- **required**: At least one file selected\n- **types**: File type matches the accepted types (rejected with reason 'type')\n- **maxSize**: File size within limit (rejected with reason 'size')\n- **maxFiles**: Number of files in multiple mode (rejected with reason 'limit')\n- Duplicate files (same name and size) are rejected with reason 'duplicate'\n\nRejected files never enter `value`; listen to `onReject` to surface them.\n\n## File Type Patterns\n\nEntries accepted in `types`:\n- `'image/*'` - All images\n- `'image/png'`, `'image/jpeg'` - Specific image types\n- `'.pdf'` - PDF files\n- `'.pdf'`, `'.doc'`, `'.docx'` - Documents\n- `'video/*'` - Videos\n- `'audio/*'` - Audio files\n- `'*/*'` - Any file\n\n## Accessibility\n\n- Proper label association\n- Keyboard accessible (Enter/Space to open file picker)\n- ARIA attributes for drop zone\n- File selection announced to screen readers\n- Error states communicated\n\n## Notes\n\n- Supports drag-and-drop file upload\n- Preview images before upload\n- Validates file types and sizes\n- Works with FormData for upload\n- Can handle single or multiple files\n- File objects are returned for processing\n\n## Theme Customization\n\nThe FileInput component uses a theme object that can be customized using the `theme` prop or by setting a global theme.\n\n### Theme Structure\n\nThe theme object contains the following parts:\n- **inputContainer**: The dropzone container\n- **placeholder**: The empty-state placeholder (icon and text)\n- **fileList**: The list wrapping the selected files\n- **file**: Each selected file row\n\nField theme parts (label, description, error, ...) are also accepted on the same `theme` prop.\n\n### Available Variants\n\n**inputContainer**:\n- base: Base classes for the dropzone\n- Variants:\n - size: 'small' | 'normal' | 'large' - Minimum height\n - state: 'idle' | 'potential' | 'valid' | 'invalid' - Drag state styling\n - disabled: boolean - Disabled state styling\n\n**placeholder**:\n- base: Base classes for the placeholder\n- Variants:\n - size: 'small' | 'normal' | 'large' - Text size and gap\n\n**fileList**:\n- base: Base classes for the file list\n- Variants:\n - size: 'small' | 'normal' | 'large' - Gap between rows\n\n**file**:\n- base: Base classes for each file row\n- Variants:\n - size: 'small' | 'normal' | 'large' - Padding, text size and gap\n\n### Usage Examples\n\n**Basic Theme Override**:\n```svelte\n<FileInput\n label=\"Upload Files\"\n bind:value={files}\n theme={{\n inputContainer: {\n base: 'border-2 border-dashed rounded-lg',\n state: {\n valid: 'border-primary bg-primary/10'\n }\n }\n }}\n/>\n```\n\n**Custom Dropzone Styling**:\n```svelte\n<FileInput\n label=\"Custom Upload\"\n bind:value={files}\n theme={{\n inputContainer: {\n base: 'border-2 border-dashed border-neutral-muted rounded-xl p-8 hover:border-primary transition-colors',\n state: {\n potential: 'border-primary',\n valid: 'border-primary bg-primary/5',\n invalid: 'border-danger bg-danger/5'\n }\n },\n fileList: {\n base: 'mt-4 grid grid-cols-2 gap-4'\n },\n file: {\n base: 'rounded-md border border-neutral-muted'\n }\n }}\n/>\n```\n\n**Global Theme Setting**:\n```svelte\n<script>\n import { setFileInputTheme } from '../components/Form/File/index.ts';\n\n setFileInputTheme({\n inputContainer: {\n base: 'border-2 border-dashed rounded-lg transition-all',\n state: {\n valid: 'border-primary bg-primary/5'\n }\n },\n fileList: {\n base: 'mt-4'\n }\n });\n</script>\n```\n\n## State contract\n\n- **value**: current bindable editable value.\n- **defaultValue**: initial value used only when `value` is omitted.\n- **onValueChange**: called with the new value when the component changes it.\n\n";
47
- readonly form: "\n# Form\n\nForm renders configured entasis fields inside a div, owns their state and validation, and submits programmatically. It provides managed Enter-key navigation and submission, but not native form events, browser constraint validation, or FormData behavior.\n\n## Basic usage\n\n```svelte\n<script lang=\"ts\">\n\timport {\n\t\tForm,\n\t\ttype FormState,\n\t\ttype InferFormValue,\n\t\ttype LiveFormValue\n\t} from 'entasis/form';\n\n\tconst inputs = {\n\t\tidentity: {\n\t\t\ttype: 'group',\n\t\t\tlabel: 'Identity',\n\t\t\tdescription: 'Core account details',\n\t\t\tcolumns: 2,\n\t\t\tinputs: {\n\t\t\t\tname: { type: 'text', label: 'Name', required: true },\n\t\t\t\temail: { type: 'email', label: 'Email', required: true }\n\t\t\t}\n\t\t}\n\t} as const;\n\n\tlet value = $state<LiveFormValue<typeof inputs>>({ name: 'Ada' });\n\tlet form = $state<FormState<typeof inputs>>();\n\tlet submitted = $state<InferFormValue<typeof inputs> | null>(null);\n</script>\n\n<Form\n\t{inputs}\n\tbind:value\n\tbind:form\n\tvariant=\"card\"\n\tlayout=\"horizontal\"\n\tsize=\"small\"\n\tdensity=\"normal\"\n\tactions={[\n\t\t{\n\t\t\tchildren: 'Save',\n\t\t\tonAction: (payload) => payload.submit()\n\t\t}\n\t]}\n\tonSubmit={(validatedValue) => {\n\t\tsubmitted = validatedValue;\n\t}}\n/>\n```\n\n## Props\n\n- **inputs** (required): ordered object keyed by entry name. Entries may be value-bearing fields, visual groups, action-button rows, or custom snippets. Only fields contribute keys to the live and validated values.\n- **value** (bindable): controlled partial live value. Visible mounted fields are published; hidden fields are omitted. A supplied top-level value takes precedence over an input's configured value.\n- **defaultValue**: initial partial live value when value is omitted.\n- **onValueChange**: called once when a user changes the visible live value. Parent value updates do not emit it.\n- **form** (bindable): the FormState instance.\n- **onSubmit**: called with the validated visible payload. Its return value is ignored; rejected promises propagate.\n- **size**: small, normal (default), or large. Controls Form typography and becomes the fallback size for fields and actions. An explicit field or action size takes precedence.\n- **density**: compact, normal (default), or comfortable. Controls gaps between the Form header, fields, footer, and actions independently from size.\n- **variant**: plain (default), sectioned, or card. All variants consume the shared Card typography. Plain remains transparent without separators. Sectioned stays transparent and adds edge-to-edge separators between the header, visible top-level fields or groups, and footer. Card adds the same separators inside the Card surface and inset; Card header/footer padding applies only to that variant. Separators do not appear between fields inside a group.\n- **layout**: vertical (default) or horizontal. Horizontal keeps one field per row and places labels to the left once each field is at least 32rem wide; fields remain stacked in narrower forms.\n- **actions**: optional array of Button props. Each action's onAction receives the live FormState, so a submit action calls form.submit(). Actions are disabled while the form is submitting; their own loading and disabled props are composed with that protection.\n- **class**: additional classes on the root div.\n- **theme**: Form theme overrides for root, header, title, description, group, group label/description/fields, top-level items, custom entries, footer, and actions. Global Card typography and section-rhythm changes flow into all Form variants before Form-specific overrides are composed; Card surface changes apply only to the card variant.\n\nThe **header**, **title**, **description**, **children**, and **footer** snippets each receive the FormState instance directly. No-argument snippets remain valid.\n\nField wrapper attributes and native control attributes have separate owners. Use fieldAttrs for the shared Field wrapper. Controls expose a typed bag for their actual native element, such as inputAttrs on TextInput, textareaAttrs on TextArea, and triggerAttrs on the custom Select's combobox button.\n\n## FormState\n\n- **value**: current visible partial value.\n- **loading**: true while the active submission handler is pending.\n- **hasError**: true after validation fails; reset after successful validation.\n- **validate()**: returns the validated visible payload, or false.\n- **submit()**: returns a promise of the validated visible payload, or false. Concurrent calls share the active promise.\n\n## Enter-key behavior\n\nOn an enabled, editable single-line input, Enter validates the current field. A valid field moves focus to the next visible enabled field, while an invalid field remains focused and displays its errors. Enter on a valid final navigable field calls form.submit(). Multiline editors and controls that already own Enter, such as selects and combobox menus, keep their native component behavior. Modified, repeated, and IME-composition Enter events are ignored.\n\n## Programmatic Dialog forms\n\nMount one Ask host near the application root, then call ask() from any client-side event. Both exports come from entasis/form:\n\n```svelte\n<script lang=\"ts\">\n\timport { Ask, ask } from 'entasis/form';\n\n\tconst inputs = {\n\t\tname: { type: 'text', label: 'Name', required: true },\n\t\temail: { type: 'email', label: 'Email', required: true }\n\t} as const;\n\n\tasync function editProfile() {\n\t\tconst outcome = await ask({\n\t\t\ttitle: 'Edit profile',\n\t\t\tdescription: 'Review the account details.',\n\t\t\tinputs,\n\t\t\tvalue: { name: 'Ada' },\n\t\t\tconfirm: 'Save',\n\t\t\tcancel: 'Cancel'\n\t\t});\n\n\t\tif (outcome.submitted) {\n\t\t\t// outcome.value is InferFormValue<typeof inputs>\n\t\t} else {\n\t\t\t// outcome.value is the partial, visible live value at cancellation\n\t\t}\n\t}\n</script>\n\n<Ask />\n```\n\nAskOptions accepts inputs, value, onSubmit, class, size, density, variant, layout, and theme with the same contracts as Form. title is required for the Dialog's accessible name. confirm and cancel accept a string or Button presentation props plus text. Their click, loading, link, and native type behavior is owned by Ask. Set type on the mounted Ask host for its default Dialog type; a request's optional dialog object may override type and also accepts responsive, size, and scroll.\n\nValidation failures keep the Dialog open and focus the first invalid field. A successful submission resolves with the validated visible payload. Cancel, the close button, and Escape resolve with submitted: false and the current partial visible value. Outside-click and swipe dismissal are disabled. While submission is active, all close paths and actions are protected. Rejected onSubmit promises reject ask() after the Dialog closes. Concurrent ask() calls render as independent stacked Dialogs. Calling ask() during SSR or without one mounted host rejects clearly.\n\n## Visibility and values\n\nThe visible prop may be a boolean or a predicate receiving the complete private value cache:\n\n```svelte\n<Form\n\tinputs={{\n\t\tcontactMethod: {\n\t\t\ttype: 'select',\n\t\t\tlabel: 'Contact method',\n\t\t\titems: [\n\t\t\t\t{ label: 'Email', value: 'email' },\n\t\t\t\t{ label: 'Phone', value: 'phone' }\n\t\t\t]\n\t\t},\n\t\tphone: {\n\t\t\ttype: 'phone',\n\t\t\tlabel: 'Phone',\n\t\t\tvisible: (value) => value.contactMethod === 'phone'\n\t\t}\n\t}}\n/>\n```\n\nHidden field values are preserved privately and restored when shown again. Hidden fields are excluded from value, validation, and submission. Removing an input definition removes its cached value.\n\n## Field label position\n\nAll configured inputs inherit the Form layout through the shared Field wrapper. Set labelPosition: 'top' or 'left' on an individual input to override the Form layout without affecting value inference:\n\n```ts\nexport const inputs = {\n\tname: { type: 'text', label: 'Name' },\n\tnotes: { type: 'textarea', label: 'Notes', labelPosition: 'top' }\n} as const;\n```\n\nThe left position becomes a two-column label/control layout once the field is at least 32rem wide and remains stacked in narrower forms. The breakpoint is a container query against the field, so a form in a narrow drawer stacks on any screen. Form uses layout rather than orientation so controls such as Slider retain their own orientation prop.\n\n## Visual groups\n\nA group renders a transparent semantic fieldset with a required legend label and an optional description. Its label and description use the same size scale as ordinary fields; grouping does not add another card or padded container:\n\n```ts\nimport type { InferFormValue } from 'entasis/form';\n\nexport const inputs = {\n\tcontact: {\n\t\ttype: 'group',\n\t\tlabel: 'Contact details',\n\t\tdescription: 'How we can reach you',\n\t\tcolumns: 1,\n\t\tinputs: {\n\t\t\temail: { type: 'email', label: 'Email', required: true },\n\t\t\tphone: { type: 'phone', label: 'Phone' }\n\t\t}\n\t}\n} as const;\n\ntype Value = InferFormValue<typeof inputs>;\n// { email: string; phone: string | null }\n```\n\nThe group key is visual only and never appears in value or submission output. Descendant field names remain top-level, so duplicate names across groups are rejected. Group visibility hides all descendants, preserves field values in the private cache, and makes inferred field keys optional. In a vertical form layout, set columns to 1, 2, 3, or 4 to control the equal-width grid; the count is a container query against the form width (2 columns from 42rem, 3 from 56rem, 4 from 72rem), and groups collapse to one column in narrower forms. Horizontal form layouts ignore columns and always render group entries in one column. A child class may span tracks when an entry needs more room. Groups may contain fields, action rows, and custom snippets; nested groups are intentionally unsupported.\n\n## Custom value fields\n\nUse type: 'field' when Form must own a value but a custom control must render it. fieldType selects the existing FieldState schema and inferred value type. The snippet receives that registered FieldState controller:\n\n```svelte\n{#snippet handleControl(field)}\n\t<input\n\t\t{...field.controlAttrs}\n\t\t{@attach field.control}\n\t\tbind:value={field.value}\n\t/>\n{/snippet}\n\n<Form\n\tinputs={{\n\t\thandle: {\n\t\t\ttype: 'field',\n\t\t\tfieldType: 'text',\n\t\t\tlabel: 'Handle',\n\t\t\trequired: true,\n\t\t\tsnippet: handleControl\n\t\t}\n\t}}\n/>\n```\n\nThe controller exposes value, disabled, required, validation state, accessible ids, controlAttrs, and the control attachment. The entry participates in bind:value, visibility, validation, submission, and InferFormValue through the same FieldState owner as built-in controls.\n\n## Non-value entries\n\nUse type: 'action' to place a labelled row of buttons in the ordered inputs flow. It accepts optional label and description content plus the same Form-aware action objects as the top-level actions prop. Its label position follows the Form layout unless labelPosition overrides it. Form size is the default button size, action density follows the Form, and loading or disabled protection cannot be overridden while the Form is submitting.\n\nUse type: 'custom' to place a snippet in the flow. The snippet receives the live public FormState and owns its own surface and padding; Form adds neither. Both entry types support class and visible, may appear at the top level or inside a group, and are omitted from bind:value, validation, and inferred submission output:\n\n```svelte\n{#snippet accountStatus(form)}\n\t<p>{Object.keys(form.value).length} visible values</p>\n{/snippet}\n\n<Form\n\tinputs={{\n\t\tname: { type: 'text', label: 'Name', required: true },\n\t\tstatus: { type: 'custom', snippet: accountStatus },\n\t\tcontrols: {\n\t\t\ttype: 'action',\n\t\t\tlabel: 'Account actions',\n\t\t\tdescription: 'Validate or save this account.',\n\t\t\tactions: [\n\t\t\t\t{ children: 'Validate', variant: 'soft', onAction: (payload) => payload.validate() },\n\t\t\t\t{ children: 'Save', onAction: (payload) => payload.submit() }\n\t\t\t]\n\t\t}\n\t}}\n/>\n```\n\nFor this configuration, InferFormValue contains name only; status and controls are presentation keys.\n\n## Validation\n\nBuilt-in validation uses the field schema and preserves its issue messages. onValidate may return:\n\n- false, null, undefined, an empty string, or an empty array for valid input;\n- true for a generic error;\n- a string for one error;\n- a string array for multiple errors.\n\nValidation runs on submission. After a field has an error, it revalidates while edited. Failed submission scrolls to and focuses the first invalid control.\n\n## Supported input types\n\ntext, email, url, password, number, rating, voice, slider, slider-range, textarea, rich-text, select, combobox, radio, checkboxes, checkbox, switch, phone, calendar, calendar-range, date, datetime, color, file, files, tag-group, tag, keyvalue, pin, and time.\n\nAlternative displays are display: 'selector' for date and calendar-range, and display: 'picker' for color.\n\nfield is the custom value-bearing entry. group, action, and custom are structural visual nodes rather than value-bearing input types.\n";
47
+ readonly form: "\n# Form\n\nForm renders configured entasis fields inside a div, owns their state and validation, and submits programmatically. It provides managed Enter-key navigation and submission, but not native form events, browser constraint validation, or FormData behavior.\n\n## Basic usage\n\n```svelte\n<script lang=\"ts\">\n\timport {\n\t\tForm,\n\t\ttype FormState,\n\t\ttype InferFormValue,\n\t\ttype LiveFormValue\n\t} from 'entasis/form';\n\n\tconst inputs = {\n\t\tidentity: {\n\t\t\ttype: 'group',\n\t\t\tlabel: 'Identity',\n\t\t\tdescription: 'Core account details',\n\t\t\tcolumns: 2,\n\t\t\tinputs: {\n\t\t\t\tname: { type: 'text', label: 'Name', required: true },\n\t\t\t\temail: { type: 'email', label: 'Email', required: true }\n\t\t\t}\n\t\t}\n\t} as const;\n\n\tlet value = $state<LiveFormValue<typeof inputs>>({ name: 'Ada' });\n\tlet form = $state<FormState<typeof inputs>>();\n\tlet submitted = $state<InferFormValue<typeof inputs> | null>(null);\n</script>\n\n<Form\n\t{inputs}\n\tbind:value\n\tbind:form\n\tvariant=\"card\"\n\tlayout=\"horizontal\"\n\tsize=\"small\"\n\tdensity=\"normal\"\n\tactions={[\n\t\t{\n\t\t\tchildren: 'Save',\n\t\t\tonAction: (payload) => payload.submit()\n\t\t}\n\t]}\n\tonSubmit={(validatedValue) => {\n\t\tsubmitted = validatedValue;\n\t}}\n/>\n```\n\n## Props\n\n- **inputs** (required): ordered object keyed by entry name. Entries may be value-bearing fields, visual groups, action-button rows, or custom snippets. Only fields contribute keys to the live and validated values.\n- **value** (bindable): controlled partial live value. Visible mounted fields are published; hidden fields are omitted. A supplied top-level value takes precedence over an input's configured value.\n- **defaultValue**: initial partial live value when value is omitted.\n- **onValueChange**: called once when a user changes the visible live value. Parent value updates do not emit it.\n- **form** (bindable): the FormState instance.\n- **onSubmit**: called with the validated visible payload. Its return value is ignored; rejected promises propagate.\n- **size**: small, normal (default), or large. Controls Form typography and becomes the fallback size for fields and actions. An explicit field or action size takes precedence.\n- **density**: compact, normal (default), or comfortable. Controls gaps between the Form header, fields, footer, and actions independently from size.\n- **variant**: plain (default), sectioned, or card. All variants consume the shared Card typography. Plain remains transparent without separators. Sectioned stays transparent and adds edge-to-edge separators between the header, visible top-level fields or groups, and footer. Card adds the same separators inside the Card surface and inset; Card header/footer padding applies only to that variant. Separators do not appear between fields inside a group.\n- **layout**: vertical (default) or horizontal. Horizontal keeps one field per row and places labels to the left once each field is at least 32rem wide; fields remain stacked in narrower forms.\n- **actions**: optional array of Button props. Each action's onAction receives the live FormState, so a submit action calls form.submit(). Actions are disabled while the form is submitting; their own loading and disabled props are composed with that protection.\n- **class**: additional classes on the root div.\n- **theme**: Form theme overrides for root, header, title, description, group, group label/description/fields, top-level items, custom entries, footer, and actions. Global Card typography and section-rhythm changes flow into all Form variants before Form-specific overrides are composed; Card surface changes apply only to the card variant.\n\nThe **header**, **title**, **description**, **children**, and **footer** snippets each receive the FormState instance directly. No-argument snippets remain valid.\n\nField wrapper attributes and native control attributes have separate owners. Use fieldAttrs for the shared Field wrapper. Controls expose a typed bag for their actual native element, such as inputAttrs on TextInput, textareaAttrs on TextArea, and triggerAttrs on the custom Select's combobox button.\n\n## FormState\n\n- **value**: current visible partial value.\n- **loading**: true while the active submission handler is pending.\n- **hasError**: true after validation fails; reset after successful validation.\n- **validate()**: returns the validated visible payload, or false.\n- **submit()**: returns a promise of the validated visible payload, or false. Concurrent calls share the active promise.\n\n## Enter-key behavior\n\nOn an enabled, editable single-line input, Enter validates the current field. A valid field moves focus to the next visible enabled field, while an invalid field remains focused and displays its errors. Enter on a valid final navigable field calls form.submit(). Multiline editors and controls that already own Enter, such as selects and combobox menus, keep their native component behavior. Modified, repeated, and IME-composition Enter events are ignored.\n\n## Programmatic Dialog forms\n\nMount one Ask host near the application root, then call ask() from any client-side event. Both exports come from entasis/form:\n\n```svelte\n<script lang=\"ts\">\n\timport { Ask, ask } from 'entasis/form';\n\n\tconst inputs = {\n\t\tname: { type: 'text', label: 'Name', required: true },\n\t\temail: { type: 'email', label: 'Email', required: true }\n\t} as const;\n\n\tasync function editProfile() {\n\t\tconst outcome = await ask({\n\t\t\ttitle: 'Edit profile',\n\t\t\tdescription: 'Review the account details.',\n\t\t\tinputs,\n\t\t\tvalue: { name: 'Ada' },\n\t\t\tconfirm: 'Save',\n\t\t\tcancel: 'Cancel'\n\t\t});\n\n\t\tif (outcome.submitted) {\n\t\t\t// outcome.value is InferFormValue<typeof inputs>\n\t\t} else {\n\t\t\t// outcome.value is the partial, visible live value at cancellation\n\t\t}\n\t}\n</script>\n\n<Ask />\n```\n\nAskOptions accepts inputs, value, onSubmit, class, size, density, variant, layout, and theme with the same contracts as Form. title is required for the Dialog's accessible name. confirm and cancel accept a string or Button presentation props plus text. Their click, loading, link, and native type behavior is owned by Ask. Set type on the mounted Ask host for its default Dialog type; a request's optional dialog object may override type and also accepts responsive, size, and scroll.\n\nValidation failures keep the Dialog open and focus the first invalid field. A successful submission resolves with the validated visible payload. Cancel, the close button, and Escape resolve with submitted: false and the current partial visible value. Outside-click and swipe dismissal are disabled. While submission is active, all close paths and actions are protected. Rejected onSubmit promises reject ask() after the Dialog closes. Concurrent ask() calls render as independent stacked Dialogs. Calling ask() during SSR or without one mounted host rejects clearly.\n\n## Visibility and values\n\nThe visible prop may be a boolean or a predicate receiving the complete private value cache:\n\n```svelte\n<Form\n\tinputs={{\n\t\tcontactMethod: {\n\t\t\ttype: 'select',\n\t\t\tlabel: 'Contact method',\n\t\t\titems: [\n\t\t\t\t{ label: 'Email', value: 'email' },\n\t\t\t\t{ label: 'Phone', value: 'phone' }\n\t\t\t]\n\t\t},\n\t\tphone: {\n\t\t\ttype: 'phone',\n\t\t\tlabel: 'Phone',\n\t\t\tvisible: (value) => value.contactMethod === 'phone'\n\t\t}\n\t}}\n/>\n```\n\nHidden field values are preserved privately and restored when shown again. Hidden fields are excluded from value, validation, and submission. Removing an input definition removes its cached value.\n\n## Field label position\n\nAll configured inputs inherit the Form layout through the shared Field wrapper. Set labelPosition: 'top' or 'left' on an individual input to override the Form layout without affecting value inference:\n\n```ts\nexport const inputs = {\n\tname: { type: 'text', label: 'Name' },\n\tnotes: { type: 'textarea', label: 'Notes', labelPosition: 'top' }\n} as const;\n```\n\nThe left position becomes a two-column label/control layout once the field is at least 32rem wide and remains stacked in narrower forms. The breakpoint is a container query against the field, so a form in a narrow drawer stacks on any screen. Form uses layout rather than orientation so controls such as Slider retain their own orientation prop.\n\n## Visual groups\n\nA group renders a transparent semantic fieldset with a required legend label and an optional description. Its label and description use the same size scale as ordinary fields; grouping does not add another card or padded container:\n\n```ts\nimport type { InferFormValue } from 'entasis/form';\n\nexport const inputs = {\n\tcontact: {\n\t\ttype: 'group',\n\t\tlabel: 'Contact details',\n\t\tdescription: 'How we can reach you',\n\t\tcolumns: 1,\n\t\tinputs: {\n\t\t\temail: { type: 'email', label: 'Email', required: true },\n\t\t\tphone: { type: 'phone', label: 'Phone' }\n\t\t}\n\t}\n} as const;\n\ntype Value = InferFormValue<typeof inputs>;\n// { email: string; phone: string | null }\n```\n\nThe group key is visual only and never appears in value or submission output. Descendant field names remain top-level, so duplicate names across groups are rejected. Group visibility hides all descendants, preserves field values in the private cache, and makes inferred field keys optional. In a vertical form layout, set columns to 1, 2, 3, or 4 to control the equal-width grid; the count is a container query against the form width (2 columns from 42rem, 3 from 56rem, 4 from 72rem), and groups collapse to one column in narrower forms. Horizontal form layouts ignore columns and always render group entries in one column. A child class may span tracks when an entry needs more room. Groups may contain fields, action rows, and custom snippets; nested groups are intentionally unsupported.\n\n## Custom value fields\n\nUse type: 'field' when Form must own a value but a custom control must render it. fieldType selects the existing FieldState schema and inferred value type. The snippet receives that registered FieldState controller:\n\n```svelte\n{#snippet handleControl(field)}\n\t<input\n\t\t{...field.controlAttrs}\n\t\t{@attach field.control}\n\t\tbind:value={field.value}\n\t/>\n{/snippet}\n\n<Form\n\tinputs={{\n\t\thandle: {\n\t\t\ttype: 'field',\n\t\t\tfieldType: 'text',\n\t\t\tlabel: 'Handle',\n\t\t\trequired: true,\n\t\t\tsnippet: handleControl\n\t\t}\n\t}}\n/>\n```\n\nThe controller exposes value, disabled, required, validation state, accessible ids, controlAttrs, and the control attachment. The entry participates in bind:value, visibility, validation, submission, and InferFormValue through the same FieldState owner as built-in controls.\n\n## App-defined input types\n\nAn app can add its own input types to every Form and ask() dialog, by name, so entries stay plain data that serializes (JSON from a server, a tool call, a database). Two steps, once per app.\n\nType it: augment `FormInputRegistry` with the value the input holds and the props its entries take.\n\n```ts\n// doc-fence: skip (imports a sibling component; tooling/component-contract/consumer-form-input.ts checks this exact setup against the package)\nimport { registerFormInputs } from 'entasis/form';\nimport MoneyInput from './MoneyInput.svelte';\n\nexport type Money = { amount: number; currency: string };\n\ndeclare module 'entasis/form' {\n\tinterface FormInputRegistry {\n\t\tmoney: { value: Money; props: { currencies?: string[] } };\n\t}\n}\n\nregisterFormInputs({\n\tmoney: {\n\t\tcomponent: MoneyInput,\n\t\tisEmpty: (value) => value?.amount === undefined,\n\t\tvalidate: (value) => (value.amount < 0 ? 'Must not be negative' : null)\n\t}\n});\n```\n\nRegister it: call registerFormInputs at the top level of a module the server and the browser both load before a form renders, such as one the root layout imports. Its keys are checked against the registry; built-in type names are refused, and registering a type again replaces it.\n\nThe component renders the control alone. Form wraps it in Field (label, description, errors) and hands it the field it registered, typed from the registry, plus the entry's own props:\n\n```svelte\n<!-- doc-fence: skip -->\n<!-- Needs the money registration above; tooling/component-contract/consumer-money-input.svelte is the checked copy. -->\n<script lang=\"ts\">\n\timport type { FormInputComponentProps } from 'entasis/form';\n\n\tlet { field, currencies = ['EUR'] }: FormInputComponentProps<'money'> = $props();\n\tconst currency = $derived(field.value?.currency ?? currencies[0] ?? 'EUR');\n</script>\n\n<input\n\ttype=\"number\"\n\t{...field.controlAttrs}\n\t{@attach field.control}\n\tvalue={field.value?.amount ?? ''}\n\toninput={(event) => field.setValue({ amount: Number(event.currentTarget.value), currency })}\n/>\n```\n\nEntries then use the type like a built-in one, in Form inputs, groups, and ask():\n\n```ts\n// doc-fence: skip (needs the money registration above)\nconst result = await ask({\n\ttitle: 'Set a price',\n\tconfirm: 'Save',\n\tcancel: 'Cancel',\n\tinputs: { price: { type: 'money', label: 'Price', required: true, currencies: ['EUR', 'USD'] } }\n});\nif (result.submitted) result.value.price; // Money\n```\n\nEvery Field entry option applies (label, description, required, disabled, size, density, visible, defaultValue, onValidate, onValueChange, slots); any other key goes to the component. required rejects what isEmpty reports empty (by default null, undefined, an empty string, or an empty array); validate runs for every entry of the type, before the entry's own onValidate. The value takes part in bind:value, visibility, validation, submission, and InferFormValue like a built-in input's. An entry whose type is neither built-in nor registered throws when the form reads it.\n\n## Non-value entries\n\nUse type: 'action' to place a labelled row of buttons in the ordered inputs flow. It accepts optional label and description content plus the same Form-aware action objects as the top-level actions prop. Its label position follows the Form layout unless labelPosition overrides it. Form size is the default button size, action density follows the Form, and loading or disabled protection cannot be overridden while the Form is submitting.\n\nUse type: 'custom' to place a snippet in the flow. The snippet receives the live public FormState and owns its own surface and padding; Form adds neither. Both entry types support class and visible, may appear at the top level or inside a group, and are omitted from bind:value, validation, and inferred submission output:\n\n```svelte\n{#snippet accountStatus(form)}\n\t<p>{Object.keys(form.value).length} visible values</p>\n{/snippet}\n\n<Form\n\tinputs={{\n\t\tname: { type: 'text', label: 'Name', required: true },\n\t\tstatus: { type: 'custom', snippet: accountStatus },\n\t\tcontrols: {\n\t\t\ttype: 'action',\n\t\t\tlabel: 'Account actions',\n\t\t\tdescription: 'Validate or save this account.',\n\t\t\tactions: [\n\t\t\t\t{ children: 'Validate', variant: 'soft', onAction: (payload) => payload.validate() },\n\t\t\t\t{ children: 'Save', onAction: (payload) => payload.submit() }\n\t\t\t]\n\t\t}\n\t}}\n/>\n```\n\nFor this configuration, InferFormValue contains name only; status and controls are presentation keys.\n\n## Validation\n\nBuilt-in validation uses the field schema and preserves its issue messages. onValidate may return:\n\n- false, null, undefined, an empty string, or an empty array for valid input;\n- true for a generic error;\n- a string for one error;\n- a string array for multiple errors.\n\nValidation runs on submission. After a field has an error, it revalidates while edited. Failed submission scrolls to and focuses the first invalid control.\n\n## Supported input types\n\ntext, email, url, password, number, rating, voice, slider, slider-range, textarea, rich-text, select, combobox, radio, checkboxes, checkbox, switch, phone, calendar, calendar-range, date, datetime, color, file, files, tag-group, tag, keyvalue, pin, and time.\n\nAlternative displays are display: 'selector' for date and calendar-range, and display: 'picker' for color.\n\nfield is the custom value-bearing entry. group, action, and custom are structural visual nodes rather than value-bearing input types.\n";
48
48
  readonly 'key-value-input': "\n# KeyValueInput Component\n\nAn editable list of key/value string pairs. Each row renders a key text input, a value text input, and a remove button; a full-width Add button below the rows appends a new empty pair. There is no dropdown, no async, and no option list — just plain text entry.\n\n## Basic Usage\n\n```svelte\n<script>\n\timport { KeyValueInput } from 'entasis/key-value-input';\n\tlet value = $state(null);\n</script>\n\n<KeyValueInput bind:value={value} />\n```\n\n## Props\n\n### Core Props\n\n- **value**: `KeyValuePair[] | null` (default: `null`, bindable)\n - The current list of `{ key, value }` pairs. `null` is treated as an empty list.\n - The array is never mutated in place — every add/remove assigns a fresh array.\n - Replacing `value` updates the editor rows without emitting `onValueChange`. Use `defaultValue` to initialize an uncontrolled editor once.\n\n### Content Props\n\n- **keyPlaceholder**: `string` (optional)\n - Placeholder for the key input. Defaults to the localized \"Key\" label.\n\n- **valuePlaceholder**: `string` (optional)\n - Placeholder for the value input. Defaults to the localized \"Value\" label.\n\n- **addLabel**: `string` (optional)\n - Label for the Add button. Defaults to the localized \"Add\" label.\n\n### Behavior Props\n\n- **maxRows**: `number` (optional)\n - Maximum number of rows. Once reached, the Add button is disabled. There is no minimum.\n\n### Field Props\n\n- **required**: `boolean` (default: `false`)\n- **disabled**: `boolean`\n- **size**: `'small' | 'normal' | 'large'` (default: `'normal'`)\n- **name**: `string`\n- **errors**: `string[] | boolean` (bindable)\n- **focused**: `boolean` (bindable)\n- **description**: `string`\n\n### Event Props\n\n- **onValueChange**: `(value: KeyValuePair[]) => void`\n - Called when the pairs change.\n\n- **onValidate**: `(value: KeyValuePair[]) => string[] | boolean`\n - Custom validation function.\n\n### i18n / Theme Props\n\n- **i18n**: `Partial<Messages>`\n - Per-instance message overrides merged over the global catalog (`add`, `keyLabel`, `valueLabel`, `remove`).\n\n- **theme**: `KeyValueInputThemeProps`\n - Customize styling for: inputContainer, row, input, removeButton, addButton.\n\n## Examples\n\n### Empty (Start With Add)\n\n```svelte\n<script>\n\tlet value = $state(null);\n</script>\n\n<KeyValueInput bind:value={value} />\n```\n\n### Pre-filled Value\n\n```svelte\n<script>\n\tlet value = $state([\n\t\t{ key: 'Content-Type', value: 'application/json' },\n\t\t{ key: 'Accept', value: '*/*' }\n\t]);\n</script>\n\n<KeyValueInput bind:value={value} />\n```\n\n### Limited Number of Rows\n\n```svelte\n<KeyValueInput maxRows={3} bind:value={value} />\n```\n\n### Custom Placeholders\n\n```svelte\n<KeyValueInput keyPlaceholder=\"Header\" valuePlaceholder=\"Value\" addLabel=\"Add header\" bind:value={value} />\n```\n\n### Object Output\n\nThe value is an array of pairs; convert it to a plain object with `Object.fromEntries`:\n\n```svelte\n<script>\n\tlet value = $state([{ key: 'a', value: '1' }]);\n\tconst object = $derived(Object.fromEntries((value ?? []).map((p) => [p.key, p.value])));\n</script>\n\n<KeyValueInput bind:value={value} />\n<pre>{JSON.stringify(object)}</pre>\n```\n\n## Keyboard Interactions\n\n- **Enter** (in a non-empty value input): Prevents the enclosing form from submitting, appends a new empty row, and focuses its key input.\n- **Enter** (in a key or empty value input): Prevents the enclosing form from submitting without adding a row.\n- **Arrow Right** (at the end of an input): Moves to the next input and places the caret at its start.\n- **Arrow Left** (at the start of an input): Moves to the previous input and places the caret at its end.\n- **Arrow Up / Arrow Down**: Moves to the same field in the previous or next row while preserving the caret offset when possible.\n- **Backspace** (in an empty input): Moves to the previous input. When the entire row is empty, removes the row and focuses the previous row's value input when available.\n\n## Accessibility\n\n- The rows collection is a real list (`role=\"list\"` / `role=\"listitem\"`), not a listbox.\n- Each row's remove control is a real `<button type=\"button\">` with an `aria-label` of \"Remove <key>\" (falling back to the \"Key\" label when the key is empty).\n- The Add button is a real `<button type=\"button\">`.\n- Inputs use `autocomplete=\"off\"` and `data-1p-ignore`.\n\n## Notes\n\n- **Stable-id keying**: Rows are keyed by a stable per-instance uid, not by the key string. Keys can be empty or duplicated while typing, and duplicate/empty keys in a keyed `{#each}` would crash the renderer.\n- **Controlled values**: Parent value replacements update the editor. User edits publish one change callback; unchanged pairs do not publish another callback.\n- **Fresh arrays**: Add and remove always assign a new array so binding, reactivity, and form updates fire correctly.\n- **Animations**: Rows animate on reorder (`animate:flip`) and on enter/leave (`transition:scale`).\n- **maxRows**: Disables the Add button once the row count reaches `maxRows`.\n- **Object output**: Use `Object.fromEntries((value ?? []).map((p) => [p.key, p.value]))` to get a plain object.\n\n## Theme Customization\n\nThe KeyValueInput uses a theme object customizable via the `theme` prop or a global theme.\n\n### Theme Structure\n\n- **inputContainer**: The vertical stack holding the rows and Add button (no border/background of its own).\n- **row**: The animated flex wrapper around each key/value/remove trio.\n- **input**: The bordered key and value text inputs.\n- **removeButton**: The square remove (X) button.\n- **addButton**: The full-width Add button.\n\n### Global Theme Setting\n\n```svelte\n<script>\n\timport { setKeyValueInputTheme } from 'entasis/key-value-input';\n\n\tsetKeyValueInputTheme({\n\t\taddButton: {\n\t\t\tbase: 'rounded-lg border-2'\n\t\t}\n\t});\n</script>\n```\n\n## State contract\n\n- **value**: current bindable editable value.\n- **defaultValue**: initial value used only when `value` is omitted.\n- **onValueChange**: called with the new value when the component changes it.\n\n";
49
49
  readonly 'multi-step-form': "\n# MultiStepForm\n\nMultiStepForm owns one Form per step, displays them through Stepper, validates visited steps before navigation, and validates every step before final submission. Field names must be unique across steps.\n\n## Basic usage\n\n```svelte\n<script lang=\"ts\">\n\timport {\n\t\tMultiStepForm,\n\t\ttype FormStep,\n\t\ttype MergedMultiStepFormInputs\n\t} from 'entasis/multi-step-form';\n\timport type { FormInputs, InferFormValue, LiveFormValue } from 'entasis/form';\n\n\tconst accountInputs = {\n\t\tname: { type: 'text', label: 'Name', required: true },\n\t\temail: { type: 'email', label: 'Email', required: true }\n\t} satisfies FormInputs;\n\tconst preferenceInputs = {\n\t\tnewsletter: { type: 'switch', label: 'Newsletter' }\n\t} satisfies FormInputs;\n\n\tconst items: [\n\t\tFormStep<typeof accountInputs>,\n\t\tFormStep<typeof preferenceInputs>\n\t] = [\n\t\t{\n\t\t\ttitle: 'Account',\n\t\t\tinputs: accountInputs\n\t\t},\n\t\t{\n\t\t\ttitle: 'Preferences',\n\t\t\tinputs: preferenceInputs\n\t\t}\n\t];\n\n\ttype Inputs = MergedMultiStepFormInputs<typeof items>;\n\tlet value = $state<LiveFormValue<Inputs>>({});\n\tlet submitted = $state<InferFormValue<Inputs> | null>(null);\n</script>\n\n<MultiStepForm\n\t{items}\n\tbind:value\n\tonSubmitForm={(validatedValue) => {\n\t\tsubmitted = validatedValue;\n\t}}\n/>\n```\n\n## Props\n\n- **items** (required): ordered FormStep array. Duplicate field names across steps throw.\n- **value** (bindable): controlled partial values merged across all steps, synchronized in both directions.\n- **defaultValue**: initial merged values when value is omitted.\n- **onValueChange**: called when the merged live value changes.\n- **onSubmitForm**: called with the validated merged payload on the final step only.\n- **onSubmitStep**: receives one `{ value, step, index }` payload after the step's onBeforeChange. `value` contains validated values from visited steps; unvisited keys remain optional. Return false to block advancing.\n- **showMeter**: whether to render the progress meter. Defaults to true.\n- **meterColor**: meter color token. Defaults to neutral.\n- **previousText**, **nextText**, **submitText**: navigation labels.\n- **variant**: visual presentation of the multi-step shell. Card renders one shared Card surface around the fixed meter, transitioning step content, and navigation footer. Header, meter, step fields, and footer share one horizontal inset in every variant. Each step reuses Form's card spacing and edge-to-edge separator rules without creating another Card surface. Supports plain, sectioned, and card; defaults to plain.\n- **previousButtonProps**, **nextButtonProps**, **submitButtonProps**: Button props. Custom click handlers are composed with internal behavior; disabled and loading protection cannot be overridden.\n- **class**: additional classes on the root.\n- **theme**: MultiStepForm theme overrides plus optional nested form theme overrides.\n\nThe **header**, **children**, and **footer** snippets receive the MultiStepFormState instance directly. Supplying a header or footer replaces that region's default meter or controls. In the card variant, header and footer content remains inside the shared card while only the active step content transitions.\n\n## FormStep\n\n```ts\ntype FormStep<I extends FormInputs> = {\n\ttitle?: string;\n\tdescription?: string;\n\tinputs: I;\n\tonBeforeChange?: (context: {\n\t\tvalue: InferFormValue<I>;\n\t\tform: FormState<I>;\n\t\tnext: () => void;\n\t}) => void | Promise<void>;\n};\n```\n\nWhen onBeforeChange is present, navigation remains blocked unless the hook calls next(). After permission is granted, onSubmitStep may still block by returning false.\n\n## Submission behavior\n\n- Next validates every visited step. Hooks do not run and the active step does not change when validation fails.\n- Final submission validates every step before calling onSubmitForm; partial invalid data is never submitted.\n- Enter follows Form's field-navigation behavior. A terminal Enter validates the current and visited steps, advances when permitted, and submits from the final step.\n- Rejected hooks and submit handlers propagate their errors, while loading always resets.\n- Concurrent calls to submit() share one active promise.\n\n## MultiStepFormState\n\n- **steps**: current step definitions.\n- **stepper**: bound Stepper state, including value, next(), and previous().\n- **value**: merged live partial value.\n- **loading**: true while a navigation or final-submit hook is pending.\n- **isLastStep**: whether the current step is final.\n- **progress** and **meterSteps**: values used by the default meter.\n- **submit()**: validates and advances or submits, returning the validated visited-step payload or false. The final-step result is the complete validated payload.\n";
50
50
  readonly 'number-input': "\n# NumberInput Component\n\nThe NumberInput component is a numeric input field with +/- controls, min/max validation, and formatting options.\n\n## Basic Usage\n\n```svelte\n<script>\n\tlet quantity = $state(1);\n</script>\n\n<NumberInput label=\"Quantity\" bind:value={quantity} />\n```\n\n## Props\n\nExtends all Field component props plus:\n\n### Core Props\n- **value**: number (bindable) - Input value\n- **min**: number - Minimum allowed value\n- **max**: number - Maximum allowed value\n- **step**: number - Native number input step for keyboard and browser interactions\n- **increment**: number (default: step ?? 1) - Amount changed by the +/- buttons\n- **showControls**: boolean (default: true) - Whether to render the +/- buttons\n\n### Display Props\n- **placeholder**: string - Placeholder text\n\n### Field Props (inherited)\n- **label**: string | Snippet - Field label\n- **description**: string | Snippet - Helper text\n- **error**: string - Error message\n- **required**: boolean - Mark as required\n- **disabled**: boolean - Disable input\n- **size**: 'small' | 'normal' | 'large' - Input size\n\n### Content Slots\n- **prefix**: Snippet - Content before input (e.g., currency symbol)\n- **suffix**: Snippet - Content after input (e.g., unit)\n\n### Styling Props\n- **class**: string - Additional CSS classes\n- **theme**: ComponentTheme - Custom theme overrides\n\n## Structure\n\n```\n<Field>\n\t<Label />\n\t<Description />\n\t<InputContainer>\n\t\t<Prefix />\n\t\t<Input type=\"number\" />\n\t\t<Controls>\n\t\t\t<Button label=\"Decrease value\" />\n\t\t\t<Button label=\"Increase value\" />\n\t\t</Controls>\n\t\t<Suffix />\n\t</InputContainer>\n\t<Error />\n</Field>\n```\n\n## Examples\n\n### Basic Number Input\n```svelte\n<script>\n\tlet age = $state(25);\n</script>\n\n<NumberInput label=\"Age\" bind:value={age} />\n```\n\n### With Min and Max\n```svelte\n<NumberInput \n\tlabel=\"Quantity\"\n\tbind:value={quantity}\n\tmin={1}\n\tmax={100}\n/>\n```\n\n### With Step and Button Increment\n```svelte\n<NumberInput \n\tlabel=\"Price\"\n\tbind:value={price}\n\tstep={0.01}\n\tincrement={1}\n\tmin={0}\n/>\n```\n\n### Custom Button Increment\n```svelte\n<NumberInput \n\tlabel=\"Quantity\"\n\tbind:value={quantity}\n\tincrement={5}\n/>\n```\n\n### With Prefix (Currency)\n```svelte\n<NumberInput \n\tlabel=\"Amount\"\n\tbind:value={amount}\n\tmin={0}\n\tstep={0.01}\n>\n\t{#snippet prefix()}\n\t\t<span>$</span>\n\t{/snippet}\n</NumberInput>\n```\n\n### With Suffix (Unit)\n```svelte\n<NumberInput \n\tlabel=\"Weight\"\n\tbind:value={weight}\n\tmin={0}\n>\n\t{#snippet suffix()}\n\t\t<span>kg</span>\n\t{/snippet}\n</NumberInput>\n```\n\n### Required Field\n```svelte\n<NumberInput \n\tlabel=\"Number of Guests\"\n\tbind:value={guests}\n\tmin={1}\n\trequired\n/>\n```\n\n### Disabled State\n```svelte\n<NumberInput \n\tlabel=\"Score\"\n\tvalue={100}\n\tdisabled\n/>\n```\n\n### Product Quantity Selector\n```svelte\n<script>\n\tlet quantity = $state(1);\n\tlet price = 29.99;\n\t\n\tconst total = $derived(quantity * price);\n</script>\n\n<div>\n\t<NumberInput \n\t\tlabel=\"Quantity\"\n\t\tbind:value={quantity}\n\t\tmin={1}\n\t\tmax={10}\n\t\tdescription={`Total: $${total.toFixed(2)}`}\n\t/>\n</div>\n```\n\n### Age Input\n```svelte\n<NumberInput \n\tlabel=\"Age\"\n\tbind:value={age}\n\tmin={18}\n\tmax={120}\n\tdescription=\"You must be 18 or older\"\n\trequired\n/>\n```\n\n### Temperature Control\n```svelte\n<NumberInput \n\tlabel=\"Temperature\"\n\tbind:value={temperature}\n\tmin={-10}\n\tmax={40}\n\tstep={0.5}\n>\n\t{#snippet suffix()}\n\t\t<span>°C</span>\n\t{/snippet}\n</NumberInput>\n```\n\n### Percentage Input\n```svelte\n<NumberInput \n\tlabel=\"Discount\"\n\tbind:value={discount}\n\tmin={0}\n\tmax={100}\n\tstep={5}\n>\n\t{#snippet suffix()}\n\t\t<span>%</span>\n\t{/snippet}\n</NumberInput>\n```\n\n### Rating Input\n```svelte\n<NumberInput \n\tlabel=\"Rating\"\n\tbind:value={rating}\n\tmin={1}\n\tmax={5}\n\tstep={0.5}\n\tdescription=\"Rate from 1 to 5 stars\"\n/>\n```\n\n## Validation\n\nNumberInput automatically validates:\n- **min**: Value must be >= min\n- **max**: Value must be <= max\n- **step**: Value must align with step increments\n- **required**: Non-null value\n\n## Keyboard Controls\n\n- **Arrow Up**: Increment by step\n- **Arrow Down**: Decrement by step\n- **Page Up**: Increment by step * 10\n- **Page Down**: Decrement by step * 10\n\n## Accessibility\n\n- Proper label association\n- ARIA attributes for min, max, and current value\n- Keyboard navigation support\n- Screen reader announcements for value changes\n- Control buttons are focusable and keyboard accessible\n\n## Notes\n\n- Native HTML number input for best compatibility\n- +/- controls provide visual increment/decrement\n- Automatically respects min/max bounds\n- Supports decimal values with step\n- Can be styled with prefix/suffix for units\n\n## Theme Customization\n\nThe NumberInput component uses a theme object that can be customized using the `theme` prop or by setting a global theme.\n\n### Theme Structure\n\nThe theme object contains the following parts:\n- **input**: Input element styles\n- **inputContainer**: Input container wrapper styles\n\n### Available Variants\n\n**input**:\n- base: Base classes applied to the input element\n- Variants:\n - size: 'small' | 'normal' | 'large' - Text size\n - disabled: boolean - Disabled state styling\n\n**inputContainer**:\n- base: Base classes for the input container (handles focus states, borders, padding)\n- Variants:\n - size: 'small' | 'normal' | 'large' - Container height and size-based styling\n - disabled: boolean - Disabled state styling\n\n### Usage Examples\n\n**Basic Theme Override**:\n```svelte\n<NumberInput \n label=\"Quantity\"\n bind:value={quantity}\n theme={{\n inputContainer: {\n base: 'border-2 rounded-lg',\n size: {\n normal: 'h-8 px-4'\n }\n },\n input: {\n size: {\n normal: 'text-base'\n }\n }\n }}\n/>\n```\n\n**Focus State Customization**:\n\nTo recolor every focus ring in the app at once, set `designTokens.focusColor` on `Theme`\ninstead of overriding per component. `ring-focus` is the focus state role and falls back to the\ncurrent role, so it never hard-pins a color.\n\n```svelte\n<NumberInput \n label=\"Custom Number\"\n bind:value={value}\n theme={{\n inputContainer: {\n base: 'focus-within:ring-2 focus-within:ring-focus/50 focus-within:border-focus'\n }\n }}\n/>\n```\n\n**Global Theme Setting**:\n```svelte\n<script>\n import { setNumberInputTheme } from '../components/Form/NumberInput/index.ts';\n \n setNumberInputTheme({\n inputContainer: {\n base: 'rounded-lg border-2 transition-all',\n size: {\n normal: 'h-8 px-4'\n }\n },\n input: {\n base: 'text-right'\n }\n });\n</script>\n```\n\n## State contract\n\n- **value**: current bindable editable value.\n- **defaultValue**: initial value used only when `value` is omitted.\n- **onValueChange**: called with the new value when the component changes it.\n\n";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entasis",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "dev": "vite dev",