{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://poetryui.com/a2ui/v1_0/catalog.json","protocolVersion":"1.0","title":"Poetry UI Catalog","description":"Poetry's component library as an A2UI catalog, projected from its registry.","catalogId":"https://poetryui.com/a2ui/v1_0/catalog.json","instructions":"Poetry components, rendered on the server. Compose a surface as a flat list of components referenced by id:\nexactly one component has the id \"root\" (a layout or container such as Card, Box, or Stack). Put visible\ntext in a component's `text` property; put child components in `children` (an array of component ids) or a\nslot property (one component id). Style axes are enums - pick by intent, never by color. Each component's\ndescription carries its rules; they are binding. Functions: `formatString` interpolates `${/path}` and\n`${name(arg: value)}` blocks; `formatNumber`, `formatCurrency`, `formatDate`, and `pluralize` format values;\n`required`, `regex`, `length`, `numeric`, and `email` are checks; `and`, `or`, `not` combine them; `openUrl`\nis a Button's local action.","components":{"Accordion":{"type":"object","description":"A vertically stacked set of interactive headings that each reveal a section of content. Rules: Items via with_item(value:, title:) { panel content } - value is the open-state key. type: :single (default) opens one at a time; pass collapsible: true to allow closing it. Server-render the open item(s) via open: %w[value] - never toggle data-open/data-closed by hand. heading_level: fits the page outline (h3 default) - the trigger button lives inside it. The chevron is built in - never add another indicator icon to the trigger. disabled: true on with_item locks that item (native disabled on the trigger; roving focus skips it).","properties":{"component":{"const":"Accordion"},"collapsible":{"type":"boolean","description":"With type: :single, allows the open section to be closed again.","default":false},"heading_level":{"type":"string","enum":["h2","h3","h4","h5","h6"],"default":"h3","description":"The heading element wrapping each trigger; pick it to fit the page outline."},"open":{"type":"array","items":{"type":"string"},"description":"Value keys of the sections rendered expanded on load.","default":"dynamic"},"type":{"type":"string","enum":["single","multiple"],"default":"single","description":"Whether one section (:single) or several (:multiple) may be open at once."},"items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"The accordion sections. Each takes value: (its open-state key), title:, and a block of panel content; disabled: true locks the section closed."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Alert":{"type":"object","description":"A callout that highlights an important inline message. Rules: Use poetry_alert for inline callouts - it carries role/aria-live; never a hand-rolled div. destructive announces assertively (role=alert) - reserve it for errors, not emphasis.","properties":{"component":{"const":"Alert"},"variant":{"type":"string","enum":["default","destructive"],"default":"default","description":"The intent axis; :destructive marks errors and announces assertively."},"icon":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Optional leading icon; pass icon props (e.g. name: :\"triangle-alert\"), not a block."},"title":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The heading line of the callout."},"action":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Optional corner action (a dismiss button or link), pinned to the top-right."}},"required":["component"]},"AlertDialog":{"type":"object","description":"A modal dialog that interrupts the user and expects a response. Rules: with_trigger(compose: true) { |wiring| ... } composes YOUR control as the trigger: the block is yielded the trigger wiring (the Stimulus behavior the overlay needs; poppers add id/aria and their trigger slot, modals hand only the open action) - splat it onto a wiring-free control (poetry_sidebar_menu_button, a plain tag); without compose: the classic composed Button renders. Destructive confirmations use AlertDialog with with_action(variant: :destructive) - never a bare Dialog, never data-turbo-confirm. with_title AND with_description are REQUIRED (both raise). The action must be an explicit user activation - agents NEVER auto-submit the action. No extra form fields inside an AlertDialog - if input is needed, use a Dialog. Cancel keeps variant: :outline; do not make cancel visually primary.","properties":{"component":{"const":"AlertDialog"},"size":{"type":"string","enum":["default","sm"],"default":"default","description":"The panel size; :sm compacts the layout and switches the footer to a two-column grid."},"trigger":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The button that opens the dialog; keywords are forwarded as Button props."},"title":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The heading - the dialog's accessible name (required)."},"description":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The explanation read alongside the title by assistive tech (required)."},"media":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Optional icon/illustration well above the title."},"action":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The confirming choice (required) - a Button; pass variant: :destructive for deletes. Activating it also closes the dialog (a caller-supplied data-action opts out)."},"cancel":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The safe way out (required) - an outline Button that takes initial focus and closes the dialog on activation."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"AspectRatio":{"type":"object","description":"Locks its content to a fixed width-to-height ratio. Rules: Pass ratio: as a string fraction ('16/9', '1/1') - Ruby's 16/9 is integer division (1). The child fills the box itself (size-full object-cover on an image).","properties":{"component":{"const":"AspectRatio"},"ratio":{"type":"string","description":"A CSS <ratio>: \"16/9\", \"1\", \"1.5\" - kept a string so the fraction survives verbatim into the --ratio custom property."}},"required":["component"]},"Attachment":{"type":"object","description":"A file or image chip showing its name, type, and size. Rules: State is server-owned: render data-upload-state and flip it by Turbo Stream replace - never toggle it in JS. with_media(variant: :image) wraps the caller's <img>; file names and URLs are user content - never render them html_safe. Actions are with_action(...) poetry Buttons (ghost/icon-xs defaults) - each needs label: (icon-only). with_trigger makes the whole chip the control (a stretched overlay UNDER the actions) - don't also wrap the chip in a link. error state needs a with_description explaining the failure - the tint alone is not the message.","properties":{"component":{"const":"Attachment"},"orientation":{"type":"string","enum":["horizontal","vertical"],"default":"horizontal","description":"Row (:horizontal) or stacked thumbnail-card (:vertical) layout."},"size":{"type":"string","enum":["default","sm","xs"],"default":"default","description":"The chip density axis."},"state":{"type":"string","enum":["idle","uploading","processing","error","done"],"default":"done","description":"The upload lifecycle state; flip it by re-render or Turbo Stream replace, never in JS."},"media":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Leading visual: :icon (default) boxes an icon tile, :image wraps the caller's <img>."},"title":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The file name line. User content - never mark it html_safe."},"description":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Muted metadata under the title (size, type); in the error state, the failure explanation."},"actions":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Trailing icon actions - each renders a Button (ghost, icon-xs defaults) and requires label:."},"trigger":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Makes the whole chip the control - a stretched button (or anchor via tag: :a, href:) layered under the actions. Don't also wrap the chip in a link."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Autocomplete":{"type":"object","description":"An input that suggests options as you type - the text itself is the value. Rules: The input IS the value: name: is the param key and free text submits as-is - suggestions are conveniences, not constraints (constrained pick = Combobox). Items via with_item(label:) - label is what filtering matches and what commit writes; value: overrides the committed text when it differs from the label. empty_text: renders the no-matches state (hidden while anything matches). open_on_focus: false waits for typing before suggesting. Server-side filtering stays yours: render fewer items on re-render - the client filter only narrows what the server sent.","properties":{"component":{"const":"Autocomplete"},"empty_text":{"type":"string","description":"The no-matches message; hidden while anything matches.","default":"No results."},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The accessible name (or wire aria-labelledby via html attrs)."},"name":{"type":"string","description":"The form param key; the input's text submits under it as-is."},"open":{"type":"boolean","description":"Server-renders the suggestion popup open.","default":false},"open_on_focus":{"type":"boolean","description":"Opens the suggestions on focus; false waits for typing.","default":true},"placeholder":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Placeholder text shown while the input is empty."},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The initial input text."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Avatar":{"type":"object","description":"A user's image with an initials fallback. Rules: label: (the person's name) is REQUIRED - it is the avatar's accessible name (role=img). The content block is the fallback (initials) and is also required - it is what shows while the image loads or when it fails. The badge slot is decorative (a presence dot); put the status meaning in label:, not in the badge. Stack avatars with poetry_avatar_group; the overflow count is poetry_avatar_group_count.","properties":{"component":{"const":"Avatar"},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The person's name - the avatar's accessible name (blank raises). The required flag also carries the fact to the registry so static checks see it."},"size":{"type":"string","enum":["default","sm","lg"],"default":"default","description":"The diameter axis."},"src":{"type":"string","description":"The image URL; without it only the initials fallback shows."},"badge":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Decorative presence dot, bottom-right; keep the status meaning in label:."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"],"anyOf":[{"required":["text"]},{"required":["children"]}]},"Badge":{"type":"object","description":"A small count or status descriptor. Rules: Badges are non-interactive status labels - never attach click handlers; use Button for actions. The one interactive form is href:, which renders the badge AS a real link (a navigational chip - the themes' [a&]:hover treatments activate). The visible text is the content block: render ... { \"beta\" } - there is no label: option. Pick the variant by intent (destructive = error states; success/warning/info = record status, e.g. Fulfilled/Processing/Syncing), never by color preference. Status badges on one surface read as a SET: keep one treatment family per table/list - the soft trio (+ outline for neutral) together, or the solid pair together; never a solid destructive pill inside a soft status column (design lint flags the mix).","properties":{"component":{"const":"Badge"},"variant":{"type":"string","enum":["default","secondary","destructive","outline","ghost","link","success","warning","info"],"default":"default","description":"The intent axis; success/warning/info are the soft record-status treatments."},"href":{"type":"string","description":"Renders the pill as a real <a> - a navigational chip; the theme's link hover treatments activate on exactly this element."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"],"anyOf":[{"required":["text"]},{"required":["children"]}]},"Breadcrumb":{"type":"object","description":"Shows the path to the current page as a trail of links. Rules: Declare the trail with with_item(label, href:) - never hand-build the nav/ol/li chain. The current page is the item WITHOUT href: (it renders aria-current=page, not a link). Collapse a long middle with with_ellipsis - it announces 'More' to screen readers. A BLOCK item (with_item { ... }) renders your content inside the <li> - the seat for a dropdown crumb or a custom-rendered link; you own its semantics (aria-current only applies to label items). with_separator(icon: :dot) - or a block - replaces the chevron in EVERY gap; the default chevron RTL-flips, a custom glyph is used as given.","properties":{"component":{"const":"Breadcrumb"},"items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"The crumbs, in declaration order. A label with href: renders a link; without one, the current page. A block makes the <li>'s content caller-owned (a dropdown crumb, a custom-rendered link)."},"separator":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Replaces the separator glyph in EVERY gap: an icon name, or a block for arbitrary content. Absent, the default chevron renders (with its RTL flip - a custom glyph is used as given)."}},"required":["component"]},"Bubble":{"type":"object","description":"A chat message bubble aligned to its sender. Rules: One Bubble per message; stack a sender's run inside poetry_bubble_group. Quick replies are tag: :button (with the caller's data-action) or tag: :a + href: - never a click handler on a div. ghost is for tool output / system text flowing full-width - not a visual preference. Reactions REQUIRE label: (the accessible name for the cluster). Inside a Message, alignment follows the Message's align - do not set both.","properties":{"component":{"const":"Bubble"},"variant":{"type":"string","enum":["default","secondary","muted","tinted","outline","ghost","destructive"],"default":"default","description":"The intent axis; :ghost is for tool output / system text flowing full-width."},"align":{"type":"string","enum":["start","end"],"default":"start","description":"Which side the bubble hugs; inside a Message, set the Message's align instead."},"href":{"type":"string","description":"Renders the content as a real anchor; implies tag: :a."},"tag":{"type":"string","enum":["div","button","a"],"default":"div","description":"The content element: :div (default), or :button/:a for a quick reply."},"reactions":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The reactions pill overlaid on an edge; label: names the cluster for assistive tech, side:/align: place it (default bottom end)."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"],"anyOf":[{"required":["text"]},{"required":["children"]}]},"Button":{"type":"object","description":"Triggers an action or event, such as submitting a form or opening a dialog. Rules: Use poetry_button - never a raw <button> with hand-written Tailwind. The visible text is the content block: poetry_button { \"Save\" }. label: is ONLY the accessible name. Icon-only buttons (size: :icon*) MUST pass label: (the accessible name). Link-styled actions use variant: :link - not <a> with button classes. Navigation wearing button styling: pass href: (renders a real <a>; tag: :a is implied) - never onclick navigation. Loading via loading: - never a manual disabled + spinner. Never nest an interactive element inside a Button. Pick the variant by intent; one primary (default) action per view.","properties":{"component":{"const":"Button"},"size":{"type":"string","enum":["default","xs","sm","lg","icon","icon-xs","icon-sm","icon-lg"],"default":"default","description":"The size axis; the icon* sizes are square icon-only forms (label: required)."},"variant":{"type":"string","enum":["default","destructive","outline","secondary","ghost","link"],"default":"default","description":"The visual intent axis; :destructive marks irreversible actions."},"disabled":{"type":"boolean","description":"Disables the control (native disabled; aria-disabled on the anchor form).","default":false},"href":{"type":"string","description":"The link target; implies the anchor form."},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The accessible name for icon-only usage - not visible text."},"loading":{"type":"boolean","description":"The no-JS loading state: aria-busy, a spinner, and the control disabled.","default":false},"tag":{"type":"string","enum":["button","a"],"default":"button","description":"Renders the same styling on an <a> when :a - navigation wearing button clothes."},"type":{"type":"string","enum":["button","submit","reset"],"default":"button","description":"The native button type; ignored when the button renders as an anchor."},"leading":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Optional leading visual, rendered inside the icon span."},"trailing":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Optional trailing visual, rendered inside the icon span."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."},"action":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/Action","description":"What the press does."}},"required":["component"]},"ButtonGroup":{"type":"object","description":"Visually joins adjacent buttons and controls into one group. Rules: Members go in the content block - the group's selectors join ANY data-slot children (buttons, inputs, select triggers); never hand-round the inner corners. Give the group an aria-label when the page has more than one (role=group is unnamed by default). A visual divider between members is poetry_button_group_separator, not a styled border.","properties":{"component":{"const":"ButtonGroup"},"orientation":{"type":"string","enum":["horizontal","vertical"],"default":"horizontal","description":"The join axis - a horizontal row or a vertical stack."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"],"anyOf":[{"required":["text"]},{"required":["children"]}]},"Calendar":{"type":"object","description":"A month grid for selecting single dates or ranges. Rules: name: makes it a form control (the chosen date posts as an ISO string in a hidden input; range mode posts name[start] + name[end]). month:/selected:/today accept a Date or an ISO string; min:/max: bound the selectable range. mode: :range selects a span - selected: takes a Date..Date Range, [start, end], or {start:, end:}; the second click completes, click-before-start swaps, re-click clears. The grid is server-rendered - it shows a valid month with no JS; the controller adds navigation + selection. For a text-field + popover, use DatePicker (it composes this) - a bare Calendar is the always-visible grid. caption_layout: :dropdown swaps the month label for month + year selects (jump navigation); the year list derives from min:/max: when both are set, else ten years around the initial month. week_numbers: true adds the ISO week column (each row's Thursday decides the number).","properties":{"component":{"const":"Calendar"},"caption_layout":{"type":"string","description":":label shows the month text; :dropdown swaps it for month + year selects (jump navigation).","default":"label"},"mode":{"type":"string","description":"Picks one date (:single) or a span (:range). Range selection completes on the second click; a click before the start swaps, a re-click clears.","default":"single"},"name":{"type":"string","description":"Makes the calendar a form control: the pick posts as an ISO string in a hidden input; range mode posts name[start] + name[end]."},"week_numbers":{"type":"boolean","description":"Adds the ISO week-number column (each row's Thursday decides the number).","default":false},"week_start":{"type":"integer","description":"The first weekday column (0 = Sunday .. 6 = Saturday).","default":0}},"required":["component"]},"Card":{"type":"object","description":"A container that groups related content and actions. Rules: Compose with the slots (title/description/action/footer) - never rebuild the header grid by hand. The card body is the content block; use CardAction for the header-corner control. The title renders as a real heading (h3 default) - set title_tag: to fit the page outline.","properties":{"component":{"const":"Card"},"title_tag":{"type":"string","enum":["h1","h2","h3","h4","h5","h6"],"default":"h3","description":"The heading element for the title - pick it to fit the page outline."},"title":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The heading line, rendered as a real heading element (title_tag:)."},"description":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Muted one-liner under the title."},"action":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The header's trailing corner control (a button, menu, or link)."},"footer":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The bottom row (actions/meta). class: merges into the footer div (a border-t divider is the canonical use); every other option (id:, data:, ...) rides onto the footer div verbatim."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Carousel":{"type":"object","description":"A slideshow for cycling through content, built on native scroll-snap. Rules: label: is REQUIRED - the carousel region's accessible name. Declare slides with with_item - the component stamps the slide roles (role=group + aria-roledescription=slide). Slides are REAL scroll content: they stay reachable by swipe, wheel, and keyboard even before JS - never gate content behind the buttons alone. Size slides with item classes (basis-full default; basis-1/2 lg:basis-1/3 for a gallery). Change slide spacing as a TRIO: track_classes: \"-ml-1\" plus item classes \"pl-1 -scroll-ml-1\" - the gutter padding and its snap scroll-margin move together.","properties":{"component":{"const":"Carousel"},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The carousel region's accessible name - required; rendering without it raises."},"orientation":{"type":"string","enum":["horizontal","vertical"],"default":"horizontal","description":"The scroll axis; snapping, controls, and arrow keys follow it."},"show_controls":{"type":"boolean","description":"Renders the prev/next buttons; slides stay reachable by swipe, wheel, and keyboard without them.","default":true},"track_classes":{"type":"string","description":"Utility classes for the slide track - change spacing as a trio: track_classes: \"-ml-1\" pairs with item classes \"pl-1 -scroll-ml-1\"."},"items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Declares one slide. The content block is required; classes: sizes the slide (basis-full default)."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Checkbox":{"type":"object","description":"A control for toggling a single value on or off. Rules: A select-all run rides poetry_checkbox_group (wrapper) + poetry_checkbox_group_all (the mixed-state parent) + poetry_checkbox_group_item per member - toggles fan out and re-derive automatically. Use poetry_checkbox (or f.check_box) - never a raw input[type=checkbox] with hand-written Tailwind, and never a hand-rolled button[role=checkbox]. Always give it a name: in forms - a checkbox without one submits nothing (visual-only mode is for controlled UI like DataTable row selection ONLY). Every checkbox needs an accessible name: a Label/Field for= association (preferred) or label:. Indeterminate is set programmatically/server-side only - no user gesture produces it; use it for select-all parents. Select-all recipe: wrap parent + rows in data-controller=\"poetry--core--checkbox-group\" with data-action=\"poetry:checkbox:change->poetry--core--checkbox-group#changed\"; mark the parent box data: {\"poetry--core--checkbox-group-target\": \"all\"} and each row box target \"item\" - the parent fans out, rows re-derive checked/unchecked/indeterminate (DataTable's selectable: already does this for its own rows). Instant-effect settings use Switch; pressed UI tools use Toggle; one-of-N uses RadioGroup. NEVER write the checked attributes (data-checked/data-unchecked/data-indeterminate) without aria-checked and the input's checked property (the controller writes all three; agents patching DOM must too). Don't suppress unchecked_value unless us...","properties":{"component":{"const":"Checkbox"},"checked":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicBoolean","description":"The state as ONE tri-valued option (true, false, or :indeterminate) - there is no separate indeterminate: flag."},"disabled":{"type":"boolean","description":"Disables the visual button and the hidden input together.","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"aria-label fallback when no <label for>/Field association exists."},"name":{"type":"string","description":"Form participation: present renders the hidden native input pair; absent leaves the checkbox visual-only (controlled UI)."},"required":{"type":"boolean","description":"aria-required ONLY, never native required - native required on the hidden input would make an unfocusable control invalid.","default":false},"unchecked_value":{"type":"string","description":"The paired hidden input's value submitted when unchecked; nil suppresses the pair (the checkbox-array idiom).","default":"0"},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The value submitted when checked (the Rails check_box \"1\")."}},"required":["component"]},"ClipboardText":{"type":"object","description":"A read-only value with a button to copy it to the clipboard. Rules: A read-only value with one copy affordance (poetry_clipboard_text) - API keys, install commands, IDs. Editable text is an Input; a secret that needs masking is a SensitiveInput. value: is what SHOWS; text_to_copy: overrides what lands on the clipboard when the display truncates - never truncate the copied text itself. Give it label: (or compose under a Field/Label) - the readonly input still needs its accessible name.","properties":{"component":{"const":"ClipboardText"},"described_by":{"type":"string","description":"Ids for the input's aria-describedby (hint or error text)."},"disabled":{"type":"boolean","description":"Disables the input and the copy button together.","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The readonly input's accessible name when no Label/Field association exists."},"text_to_copy":{"type":"string","description":"Overrides what lands on the clipboard when the displayed value truncates: display short, copy full."},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The displayed text - also what copies, unless text_to_copy: overrides it."}},"required":["component"]},"CodeBlock":{"type":"object","description":"A syntax-highlighted code panel with a copy button and optional line numbers. Rules: Blocks of code are a CodeBlock (poetry_code_block) - never a hand-rolled pre/code with utility classes; the syntax palette, line counters, and copy affordance ride it. Highlighting needs `gem \"rouge\"` in the host Gemfile - without it the block renders plain (same markup, no colors). Inline code stays plain <code> typography. highlight_lines: takes 1-based line numbers; line numbers are CSS counters and never pollute copied text.","properties":{"component":{"const":"CodeBlock"},"code":{"type":"string","description":"The source text to highlight - required."},"copy":{"type":"boolean","description":"Renders the copy button in the panel's corner.","default":true},"highlight_lines":{"type":"string","description":"1-based line numbers to tint via the theme's highlight hook."},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The scroll region's accessible name; defaults to the localized \"Code\" (a focusable scrollable region must be named - axe)."},"language":{"type":"string","description":"The lexer name (\"ruby\", \"js\", ...); unknown languages fall back to plain text.","default":"text"},"line_numbers":{"type":"boolean","description":"Renders CSS-counter line numbers - never part of selection or copied text.","default":false}},"required":["component"]},"Collapsible":{"type":"object","description":"An interactive element that expands and collapses a section of content. Rules: with_trigger(compose: true) { |wiring| ... } composes YOUR control as the trigger: the block is yielded the trigger wiring (the Stimulus behavior the overlay needs; poppers add id/aria and their trigger slot, modals hand only the open action) - splat it onto a wiring-free control (poetry_sidebar_menu_button, a plain tag); without compose: the classic composed Button renders. The trigger is with_trigger { \"label\" } - a real button, wired for you (aria-expanded/controls). Server-render the initial state via open: - never toggle data-open/data-closed by hand. Content stays in the DOM when closed (hidden) - do not conditionally render it. For URL-controlled disclosure without JS, render open: from params - the same markup serves both.","properties":{"component":{"const":"Collapsible"},"open":{"type":"boolean","description":"The server-rendered initial state; the trigger toggles it client-side.","default":false},"trigger":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The disclosure control - a real button, wired for you (aria-expanded, aria-controls); options merge onto it."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"],"anyOf":[{"required":["text"]},{"required":["children"]}]},"Combobox":{"type":"object","description":"A text input with an autocomplete popover for picking from a list. Rules: Use poetry_combobox (f.poetry_combobox in forms) - never hand-wire Popover+Command+hidden-input; this component IS that wiring, with the form story done right. Combobox picks VALUES. Filter-then-ACT is bare Command; short known lists are Select; free text is Input. Every Combobox MUST be named (Field label via id: or aria-label) - a nameless bare combobox fails at render. NEVER write aria-selected from highlight logic (position is data-highlighted + aria-activedescendant); NEVER write the display without the native select first - the commit pipeline does all of it; agents patching DOM must too. Async options: filter: false + the Turbo-frame ?q= recipe - AND the frame must render the twin native <option> for every committable item (the recipe's one hard rule). multiple: true is the multi-select/chips mode: value: takes an ARRAY, the native <select multiple> posts name[] (the [] is appended for you), selection TOGGLES with the popup staying open, and chips replace the trigger - never fake multi with hidden inputs. Do not put interactive elements inside options (an option IS the interactive unit). Deselection in single mode is include_blank (a visible blank option) or show_clear: (the trigger-side X - single mode only), never a re-click toggle - committing the already-selected value closes without change. In multiple, re-committing IS the deselect gesture (chip-remove is its pointer twin).","properties":{"component":{"const":"Combobox"},"align":{"type":"string","enum":["start","center","end"],"default":"start","description":"The popup's alignment along the trigger's edge."},"align_offset":{"type":"integer","description":"Skid in px along the aligned edge.","default":0},"avoid_collisions":{"type":"boolean","description":"Flips/shifts the popup to stay inside the viewport.","default":true},"dir":{"type":"string","enum":["ltr","rtl"],"description":"Writing-direction override (ltr/rtl) stamped on the root."},"disabled":{"type":"boolean","description":"Disables the trigger, the filter input, and the native select.","default":false},"filter":{"type":"boolean","description":"Forwarded to the embedded engine: false = server-driven options (the async Turbo-frame recipe).","default":true},"loop":{"type":"boolean","description":"Wraps arrow-key highlight movement past either end of the list.","default":false},"modal":{"type":"boolean","description":"DEFAULT FALSE - popover semantics (Tab-out closes, no scrim). true restores the focus-scope trap for dialog-critical pickers.","default":false},"multiple":{"type":"boolean","description":"Multi-select mode: value: becomes LIST-capable (single stays the scalar), the trigger is replaced by the chips field, the native <select multiple> posts name[], selection toggles without closing.","default":false},"name":{"type":"string","description":"The form field name on the native <select>; multiple: appends [] for you."},"open":{"type":"boolean","description":"Server-renders the popup open.","default":false},"placeholder":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Shown in the value display (multiple: in the inline input) while nothing is committed."},"required":{"type":"boolean","description":"Forwards to the native <select> for constraint validation.","default":false},"search_placeholder":{"type":"string","description":"Placeholder for the popup's filter input (single mode)."},"show_clear":{"type":"boolean","description":"Single mode only: the trigger-side deselection X - swaps in over the chevrons while a value is committed and commits the blank value, so the cleared state serializes as \"\".","default":false},"side":{"type":"string","enum":["top","right","bottom","left"],"default":"bottom","description":"The popup's preferred side of the trigger; collisions may flip it."},"side_offset":{"type":"integer","description":"Gap in px between the trigger and the popup.","default":4},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The committed value; with multiple:, an array of values."},"width":{"type":"string","description":"The trigger width utility class; the popup ALWAYS tracks the trigger's measured width, so one knob sizes both surfaces. nil resolves to the dictionary's default (w-50)."},"trigger":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Optional custom trigger content rendered BEFORE the value span (rare); the component owns role=combobox + the aria wiring + the chevrons regardless, so composition cannot drop the contract."},"empty":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Custom zero-results content (defaults to t('poetry.combobox.empty'))."},"loading":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Custom pending content (a spinner); the HOST toggles visibility (Turbo frame events) - the part renders hidden (Command parity)."},"items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"The option UNION forwarded to the embedded command list: item | group (heading + items) | separator - one ordered collection (interleaving preserved; items and groups are part COMPONENTS so option registration follows render/DOM order)."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Command":{"type":"object","description":"A command palette for fast, keyboard-driven search and actions. Rules: Use poetry_command - never hand-roll a filterable listbox with an input + a list and ad-hoc JS. Command items DO things; they carry no form value. Picking a value for a form is Combobox (which wraps this) - never bind a hidden input to a bare Command. Every item needs a unique value: (ArgumentError) and gets a server id - never strip item ids (aria-activedescendant depends on them). Never put tabindex or focus on options; never write aria-selected in a bare Command - highlight is data-highlighted + activedescendant only. Filtering is hide-only: never reorder, remove, or re-append items to 'sort' results - DOM order is the contract. The poetry:command:select event is the ONLY activation surface - act in a listener (or item data-action); don't patch the controller to navigate. Long/async data: filter: false + a Turbo frame (the recipe) - don't render 5,000 items and hope. Icon-rich labels: set filter_value:/keywords: rather than stuffing hidden text into items.","properties":{"component":{"const":"Command"},"disabled":{"type":"boolean","description":"Disables the filter input.","default":false},"filter":{"type":"boolean","description":"Client-side filtering; false leaves the list server-driven.","default":true},"list_label":{"type":"string","description":"The listbox's accessible name.","default":"dynamic"},"loop":{"type":"boolean","description":"Wraps arrow-key highlight movement past either end of the list.","default":false},"placeholder":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The filter input's placeholder text."},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Seats the initial highlight on the item with this value."},"empty":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Custom zero-results content (defaults to t('poetry.command.empty'))."},"loading":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Custom pending content (a spinner); the HOST toggles visibility (Turbo frame events) - Command renders the part, never sets it."},"items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"The item UNION: item | group (heading + items) | separator - one ordered collection (interleaving preserved; items and groups are part COMPONENTS so id assignment follows render/DOM order)."}},"required":["component"]},"ContextMenu":{"type":"object","description":"A menu of actions revealed by right-clicking an element. Rules: NEVER make a context menu the only path to an action - it is an invisible affordance; every item needs a visible equivalent (a '...' DropdownMenu button, a toolbar, a detail page). Choose ContextMenu only for right-click-on-an-object semantics; a visible button opening a menu is DropdownMenu. Do not add aria-haspopup or a role to the trigger surface; do not make it focusable except via focusable_surface: true. side: picks which side of the pointer the menu opens toward (top/right/bottom/left, default :right); align and offsets are not API - collisions still flip the side. Wrap the whole logical object (row/card) as the trigger surface, not a fragment. Destructive items use variant: :destructive AND still confirm irreversible actions via a dialog. shortcut: is a visual hint only - it does NOT bind the key. Do not nest a ContextMenu trigger surface inside another ContextMenu trigger surface.","properties":{"component":{"const":"ContextMenu"},"dir":{"type":"string","enum":["ltr","rtl"],"description":"Writing-direction override (ltr/rtl) stamped on the root."},"disabled":{"type":"boolean","description":"Inerts the surface - no gesture opens the menu.","default":false},"focusable_surface":{"type":"boolean","description":"Puts the surface in the tab order and advertises Shift+F10.","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The menu's accessible name (localized fallback when omitted)."},"long_press_delay":{"type":"integer","description":"Touch long-press duration in ms before the menu opens.","default":700},"loop":{"type":"boolean","description":"Wraps arrow-key movement past either end of the menu.","default":false},"modal":{"type":"boolean","description":"Traps focus in the open menu; false keeps the page interactive.","default":true},"open":{"type":"boolean","description":"Server-renders the menu open (rare - context menus normally open from the gesture).","default":false},"side":{"type":"string","enum":["top","right","bottom","left"],"default":"right","description":"Which side of the pointer the menu opens toward; collisions may still flip it."},"items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"The menu composition API: one ordered items collection accepting seven kinds, interleaved in call order - with_item an action row (href: renders it as a real link; submit: as a real submit button) with_checkbox_item a toggleable checked/unchecked row with_radio_group a single-select scope; add rows inside it via with_radio_item(value:) with_label a non-interactive heading for a run of items with_separator a horizontal rule between runs with_group semantic grouping around the same union, one level down with_sub a nested submenu: its own with_trigger plus the same union, recursively"},"trigger":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The right-click/long-press SURFACE: wraps arbitrary content (a card, a row, a region); polymorphic tag: (default :span, set tag: :div to wrap block content). NOT a button: no role, no aria-haspopup, no tabindex by default. The inline -webkit-touch-callout suppresses the iOS callout so long-press can run (iOS never fires contextmenu; the timer is the only touch path there)."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"DataTable":{"type":"object","description":"A table with sorting, row selection, and sticky headers. Rules: Build State.from_params(params, sortable: [...]) in the controller - NEVER order by raw params; the whitelist is what makes state.order_clause injection-safe. Column cell blocks RETURN the cell content ({ |row| row.title }) - they must not write to the template buffer. Sort/filter/page are URL state over GET links and a GET form. Row mutations (inline edit, row actions) belong to poetry-reactive components rendered inside cells - never to this component. Give the table a caption: - it is the table's accessible purpose.","properties":{"component":{"const":"DataTable"},"caption":{"type":"string","description":"The table's accessible purpose, rendered as its <caption>."},"empty_text":{"type":"string","description":"Shown in a full-width row when rows are empty.","default":"No results."},"filter":{"type":"boolean","description":"Renders the filter form; false drops the toolbar row.","default":true},"filter_label":{"type":"string","description":"The filter input's accessible label.","default":"Filter"},"filter_name":{"type":"string","description":"The query-param key the filter submits under.","default":"q"},"filter_placeholder":{"type":"string","description":"The filter input's placeholder text.","default":"Filter…"},"frame":{"type":"string","description":"Wrap in a <turbo-frame data-turbo-action=\"advance\"> so hosts with Turbo scope the round trip to the table while the URL still advances. The host response must render the same frame id."},"scroll_label":{"type":"string","description":"Accessible name for the sticky scroll region; falls back to caption:."},"selectable":{"type":"string","description":"Row selection: a lambda mapping each row to its id turns the feature ON - a leading checkbox column (select-all with a real indeterminate middle state, shift ranges, count announcements) whose checkboxes ARE the form value (selection_name[], plain checkboxes with no JS). Pair with the action-bar block for bulk actions."},"selection_name":{"type":"string","description":"The checkbox field name; selected row ids post as selection_name[].","default":"selected_ids"},"sticky_header":{"type":"boolean","description":"Forwarded to the inner Table: sticky_header pins the thead while the table's scroll container scrolls; container_class caps that container's height (\"max-h-96\") - without a cap nothing sticks. The sticky scroll region needs an accessible name (the ScrollArea rule); scroll_label: falls back to caption:.","default":false},"columns":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Columns are DECLARED here and rendered per row by the template. A sortable column's key must be in the state's whitelist - catching drift between the view's columns and the controller's sortable: list at render, not as a silently unsortable header."}},"required":["component"]},"DateField":{"type":"object","description":"A segmented input for typing a date one part at a time. Rules: Date entry is a DateField (poetry_date_field / form.date_field) - never a masked Input, three selects, or a bare input type=date when the design system is in play. The native input is the form value: params[<name>] is ISO (yyyy-mm-dd) with or without JS; min:/max: take Date or ISO strings and ride native validation. Pair with a Label/Field for the accessible name (label for= the input id); standalone use takes label: - segments announce it themselves. Locale drives segment order and numerals automatically; pass locale: only to pin a field to a different locale than the page.","properties":{"component":{"const":"DateField"},"described_by":{"type":"string","description":"Ids for aria-describedby (hint or error text)."},"disabled":{"type":"boolean","description":"Disables the field; the segment group dims and goes inert.","default":false},"invalid":{"type":"boolean","description":"Paints the destructive border/ring and sets aria-invalid.","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Standalone accessible name; inside a form the Field label wires ids instead. Segments announce it themselves."},"locale":{"type":"string","description":"Pins the field to a locale other than the page's."},"max":{"type":"string","description":"The latest allowed date (Date or ISO string) - rides native constraint validation."},"min":{"type":"string","description":"The earliest allowed date (Date or ISO string) - rides native constraint validation."},"name":{"type":"string","description":"The form field name - required; the value posts as ISO yyyy-mm-dd with or without JS."},"placeholder_value":{"type":"string","description":"What the first arrow press on an empty segment lands on; defaults to today."},"readonly":{"type":"boolean","description":"The value shows but cannot be edited.","default":false},"required":{"type":"boolean","description":"Marks the native input required.","default":false},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Date, or an ISO yyyy-mm-dd string; nil renders empty."}},"required":["component"]},"DatePicker":{"type":"object","description":"A date field that opens a calendar popover for selection. Rules: name: is REQUIRED - the chosen date posts as an ISO string (the Calendar's hidden input). value: preselects a date (a Date or ISO string) - the trigger shows it formatted, no JS needed. min:/max: bound the selectable range; the label + placeholder are the trigger's text. key:/id: forwards to the composed Popover - a keyed DatePicker renders cache-stable popover ids. variant: :input renders a text field with a calendar button - typed parseable dates re-select the calendar; single mode only. For an always-visible grid use Calendar directly - DatePicker is the field+popover form.","properties":{"component":{"const":"DatePicker"},"caption_layout":{"type":"string","description":"Forwarded to the wrapped Calendar: :dropdown swaps the caption for month + year selects (the date-of-birth recipe - min:/max: bound the year list).","default":"label"},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The trigger's accessible name (aria-label)."},"mode":{"type":"string","description":":single or :range (two-date selection; the trigger shows the joined pair).","default":"single"},"name":{"type":"string","description":"The form field name - required; the chosen date posts as ISO."},"placeholder":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Trigger text while nothing is chosen."},"variant":{"type":"string","description":":button (the default trigger) or :input - a text field that accepts a typed date (parseable text re-selects the calendar) with a calendar icon-button opening the popover. Single mode only.","default":"button"}},"required":["component"]},"DateTimeField":{"type":"object","description":"A segmented input for typing a date and a time one part at a time. Rules: Date-and-time entry is a DateTimeField (poetry_date_time_field / form.datetime_field) - never a DateField beside a TimeField, three selects, or a bare input type=datetime-local when the design system is in play; params[<name>] is YYYY-MM-DDTHH:MM (with :SS under seconds:), with or without JS. No zone rides the wire: the value is the wall time the user typed on their own clock - the app places it (Time.zone.parse in the controller, or the model's zone). 12- vs 24-hour follows the user's locale automatically (the dayPeriod segment appears only under twelve-hour cycles); hour_cycle: pins it when a product must.","properties":{"component":{"const":"DateTimeField"},"described_by":{"type":"string","description":"Ids for aria-describedby (hint or error text)."},"disabled":{"type":"boolean","description":"Disables the field; the segment group dims and goes inert.","default":false},"hour_cycle":{"type":"string","description":"Pins the hour cycle (h12/h23/h11/h24) instead of the locale's."},"invalid":{"type":"boolean","description":"Paints the destructive border/ring and sets aria-invalid.","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Standalone accessible name; inside a form the Field label wires ids instead. Segments announce it themselves."},"locale":{"type":"string","description":"Pins the field to a locale other than the page's."},"max":{"type":"string","description":"The latest allowed date (Date or ISO string) - rides native constraint validation."},"min":{"type":"string","description":"The earliest allowed date (Date or ISO string) - rides native constraint validation."},"name":{"type":"string","description":"The form field name - required; the value posts as ISO yyyy-mm-dd with or without JS."},"placeholder_value":{"type":"string","description":"What the first arrow press on an empty segment lands on; defaults to today."},"readonly":{"type":"boolean","description":"The value shows but cannot be edited.","default":false},"required":{"type":"boolean","description":"Marks the native input required.","default":false},"seconds":{"type":"boolean","description":"Adds the seconds segment; the wire format becomes YYYY-MM-DDTHH:MM:SS.","default":false},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Date, or an ISO yyyy-mm-dd string; nil renders empty."}},"required":["component"]},"Deferred":{"type":"object","description":"A region that lazily loads its content on visibility, with skeleton and error states. Rules: Use poetry_deferred(src:) for expensive regions - never a spinner div + a hand-rolled fetch. loading: :lazy (the default) fetches on visibility: a deferred region inside a hidden Tabs panel (with_tab defer:) or HoverCard (defer:) loads on first reveal for free. The block is the placeholder (a Skeleton renders when absent); failure shows a retryable error card automatically - never hand-wire loading or error states around it.","properties":{"component":{"const":"Deferred"},"loading":{"type":"string","description":":lazy fetches when the frame becomes visible; :eager fetches right after paint.","default":"lazy"},"src":{"type":"string","description":"The URL to fetch - required; rendering without it raises."}},"required":["component"]},"Dialog":{"type":"object","description":"A window overlaid on the page for content that requires attention. Rules: with_trigger(compose: true) { |wiring| ... } composes YOUR control as the trigger: the block is yielded the trigger wiring (the Stimulus behavior the overlay needs; poppers add id/aria and their trigger slot, modals hand only the open action) - splat it onto a wiring-free control (poetry_sidebar_menu_button, a plain tag); without compose: the classic composed Button renders. Open dialogs with with_trigger(...) - never a hand-wired button. with_title is REQUIRED (the accessible name); with_description when the purpose needs explaining. Confirmations that must not be lost use dismissible: false (backdrop clicks stop closing). show_close_button: false removes the corner X - keep a footer action (Esc still closes). Destructive confirmations pair a destructive Button in the footer - never auto-submit.","properties":{"component":{"const":"Dialog"},"dismissible":{"type":"boolean","description":"Backdrop clicks close the dialog; false keeps confirmations from being dismissed accidentally (Esc still closes).","default":true},"show_close_button":{"type":"boolean","description":"Renders the corner X; false forces a deliberate footer choice (footer actions and Esc remain). Sheet inherits this.","default":true},"trigger":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The trigger is a poetry Button wired to open the dialog - agents pass Button props: with_trigger(variant: :outline) { \"Open\" }."},"title":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The heading - the dialog's accessible name; required."},"description":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Muted copy under the title, wired to aria-describedby."},"footer":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The action row at the bottom of the panel."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Drawer":{"type":"object","description":"A gesture-driven panel that slides in from a screen edge. Rules: Open drawers with with_trigger(...) - never a hand-wired button. with_title is REQUIRED (the accessible name) - the inherited Dialog rule. direction: is the DISMISS direction: :down is the mobile bottom sheet (the default); left/right make an edge panel - prefer Sheet on desktop. show_swipe_handle: true renders the grab pill - use it on bottom sheets so the gesture is discoverable. Esc and the backdrop still dismiss (the platform trap) - the swipe is an addition, never the only way out. modal: false keeps the page interactive (no scrim, no focus trap) - pair a wired footer close; Esc while focus is inside still exits. snap_points: [\"31rem\", 1] snaps a bottom sheet between preset heights (ascending fractions or px/rem lengths; opens at the first) - direction: :down only.","properties":{"component":{"const":"Drawer"},"direction":{"type":"string","enum":["down","up","left","right"],"default":"down","description":"The dismiss direction - :down is the mobile bottom sheet; the edge chrome and swipe axis derive from it."},"dismissible":{"type":"boolean","description":"Backdrop clicks close the dialog; false keeps confirmations from being dismissed accidentally (Esc still closes).","default":true},"modal":{"type":"boolean","description":"Non-modal (false) opens with show() - no top layer, no scrim, no focus trap, no scroll lock; the page behind stays interactive. Esc (while focus is inside), the swipe, and any wired close button still exit; there is no backdrop to click, so pointer dismissal is off by nature.","default":true},"show_swipe_handle":{"type":"boolean","description":"Renders the grab pill so the swipe gesture is discoverable.","default":false},"snap_points":{"type":"string","description":"Preset resting heights for a bottom sheet, ascending: fractions of the full height (0..1] or CSS px/rem lengths ([\"31rem\", 1]). The popup runs full-height and opens at the first point; drags move between points, below the first dismisses. direction: :down only."},"trigger":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The trigger is a poetry Button wired to open the dialog - agents pass Button props: with_trigger(variant: :outline) { \"Open\" }."},"title":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The heading - the dialog's accessible name; required."},"description":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Muted copy under the title, wired to aria-describedby."},"footer":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The action row at the bottom of the panel."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"DropdownMenu":{"type":"object","description":"A menu of actions or options triggered by a button. Rules: with_trigger(compose: true) { |wiring| ... } composes YOUR control as the trigger: the block is yielded the trigger wiring (the Stimulus behavior the overlay needs; poppers add id/aria and their trigger slot, modals hand only the open action) - splat it onto a wiring-free control (poetry_sidebar_menu_button, a plain tag); without compose: the classic composed Button renders. Use poetry_dropdown_menu - never hand-roll role=menu popups with Tailwind. Items are ACTIONS. Choosing a form VALUE is a Select/Combobox - do not fake it with radio items. Navigation items pass with_item(href:) (external: for a new tab); a form action (sign-out, a DELETE) passes with_item(submit:, method:). The item renders AS the anchor / submit button (role=menuitem on the <a> or <button>) - one interactive element - so NEVER nest a link_to or button_to inside an item. Icon-only triggers MUST have an accessible name (the composed Button's label: rule). Never write the state attributes (data-popup-open / data-checked / data-unchecked) without their aria twin (aria-expanded / aria-checked) - the controller writes both; agents patching DOM must too. Destructive items use variant: :destructive AND still confirm irreversible actions via a dialog. shortcut: is a visual hint only - it does NOT bind the key; wire a real hotkey separately or omit it. Do not nest interactive elements inside items (a menuitem IS the interactive unit). Keep submenus <= 2...","properties":{"component":{"const":"DropdownMenu"},"align":{"type":"string","enum":["start","center","end"],"default":"center","description":"The menu's alignment against the trigger's edge."},"align_offset":{"type":"integer","description":"Pixel shift along the alignment edge.","default":0},"avoid_collisions":{"type":"boolean","description":"Flips/shifts placement to keep the menu inside the viewport.","default":true},"dir":{"type":"string","enum":["ltr","rtl"],"description":"Reading direction; :rtl flips submenu sides and indicators."},"disabled":{"type":"boolean","description":"Disables the menu trigger button.","default":false},"loop":{"type":"boolean","description":"Arrow-key navigation wraps from the last item back to the first.","default":false},"modal":{"type":"boolean","description":"While open, pointer interaction outside the menu is blocked; false keeps the rest of the page interactive.","default":true},"open":{"type":"boolean","description":"Renders the menu already open on page load.","default":false},"side":{"type":"string","enum":["top","right","bottom","left"],"default":"bottom","description":"Which side of the trigger the menu opens on (flips on collision)."},"side_offset":{"type":"integer","description":"Gap in pixels between the trigger and the menu.","default":4},"items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"The menu composition API: one ordered items collection accepting seven kinds, interleaved in call order - with_item an action row (href: renders it as a real link; submit: as a real submit button) with_checkbox_item a toggleable checked/unchecked row with_radio_group a single-select scope; add rows inside it via with_radio_item(value:) with_label a non-interactive heading for a run of items with_separator a horizontal rule between runs with_group semantic grouping around the same union, one level down with_sub a nested submenu: its own with_trigger plus the same union, recursively"},"trigger":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The menu button - a poetry Button (options forward to it, e.g. variant: :outline). The slot owns the aria-haspopup/expanded/ controls wiring regardless of the composed content, so composition cannot drop the aria."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Empty":{"type":"object","description":"An empty-state placeholder with an icon, message, and actions. Rules: An empty collection gets an Empty state with a next action - never a bare 'No results' div. Compose with the slots (media/title/description); the actions are the content block. The title renders as a real heading (h3 default) - set title_tag: to fit the page outline. media_variant: :icon gives the rounded muted icon tile; wrap a poetry_icon in with_media.","properties":{"component":{"const":"Empty"},"media_variant":{"type":"string","enum":["default","icon"],"default":"default","description":"The media slot's treatment; :icon gives the rounded muted icon tile."},"title_tag":{"type":"string","enum":["h1","h2","h3","h4","h5","h6"],"default":"h3","description":"The title's heading level - a real heading element, so set it to fit the page outline."},"media":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Optional leading visual above the title - an icon or illustration."},"title":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The headline, rendered as a real heading (see title_tag)."},"description":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Muted copy under the title."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Field":{"type":"object","description":"Wraps a form control with its label, hint, and validation message. Rules: Wire the control with field.control_attributes - never hand-write aria-describedby. Error text arrives via error: (from model errors upstream) - never a bare red <p>. hint: (escaped string) for pure data; with_hint { } for authored markup (links) - call it BEFORE the control so the hint id lands in aria-describedby. orientation: :horizontal is the boolean-control layout (checkbox/switch left, label + hint stacked right) - text inputs and groups stay vertical. orientation: :responsive stacks by default and flips label-left / control-right once its poetry_field_group container passes the md mark - the settings-page recipe (it needs that FieldGroup ancestor to measure against).","properties":{"component":{"const":"Field"},"orientation":{"type":"string","enum":["vertical","horizontal","setting","responsive"],"default":"vertical","description":"The layout axis. :horizontal is the boolean-control pattern: the control lands in the first grid column, label + hint/error stack in the second, and the control row-centers against the label line."},"error":{"type":"string","description":"The error line (typically from model errors) - presence flips the invalid skin and leads the control's aria-describedby."},"group":{"type":"boolean","description":"group: the control is a role-bearing <div> (RadioGroup, Slider) - label[for] would be inert (Chrome flags it), so the label drops for=, carries label_id, and control_attributes names the group via aria-labelledby (the visible label, i18n-proof).","default":false},"hint":{"type":"string","description":"Plain-text guidance under the control (escaped wholesale); use with_hint for authored markup."},"hint_position":{"type":"string","description":"Where the hint renders relative to the control - :above puts guidance before a tall control. aria-describedby is identical either way; this is visual order only.","default":"below"},"invalid":{"type":"boolean","description":"Flips the invalid skin (data-invalid + aria-invalid) WITHOUT an error line. error: implies it; use invalid: alone when the hint copy IS the requirement.","default":false},"label_text":{"type":"string","description":"The visible label text, associated with the control via for=."},"required":{"type":"boolean","description":"Marks the control required via aria-required only - never the native required attribute.","default":false}},"required":["component"]},"FieldGroup":{"type":"object","description":" Rules: Stack Fields (and Fieldsets) with poetry_field_group - the theme owns the rhythm; never hand-space a form column with gap utilities. variant: :choices packs a run of horizontal checkbox/switch fields tighter (the choice-group form). Field's orientation: :responsive is container-driven: it needs a FieldGroup ancestor to measure against - without one it stays stacked.","properties":{"component":{"const":"FieldGroup"},"variant":{"type":"string","enum":["default","choices"],"default":"default","description":":choices packs a run of horizontal checkbox/switch fields tighter."}},"required":["component"]},"FieldSeparator":{"type":"object","description":" Rules: Divides stacked fields inside a poetry_field_group - not a general-purpose rule (that is poetry_separator). Pass a block for the inline caption form (\"Or continue with\") - the caption sits on the line, backed by the page background.","properties":{"component":{"const":"FieldSeparator"},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Fieldset":{"type":"object","description":" Rules: A run of related fields gets poetry_fieldset with legend: - the group's accessible name (a bare <div> around fields tells AT nothing). legend_variant: :label renders the legend at label size - use it when the group is one setting explained by its rows (checkbox/switch runs). hint: is the muted description under the legend; per-field hints stay on the fields. Stack the fields inside with poetry_field_group - never hand-spaced flex columns.","properties":{"component":{"const":"Fieldset"},"hint":{"type":"string","description":"Muted description under the legend; per-field hints stay on the fields."},"legend":{"type":"string","description":"The group's accessible name - renders as the real <legend>."},"legend_variant":{"type":"string","enum":["legend","label"],"default":"legend","description":":label renders the legend at label size - for a group that is one setting explained by its rows (checkbox/switch runs)."}},"required":["component"]},"FileInput":{"type":"object","description":"A control for selecting, previewing, and removing files to upload. Rules: File selection is a FileInput: variant: :input for compact forms, :dropzone when dragging is expected (uploads as the page's point) - never a hand-rolled drop div. The native input is the form value: set name: (multiple: true wants a name ending in [] for Rails params); ActiveStorage direct upload attaches to it as usual. The dropzone's selected-file list and clear button are controller-rendered - compose prompt:/hint: copy instead of adding your own list markup. In a Field, prefer form.file_input (the builder wires id/label/errors); the bare component suits standalone dropzones.","properties":{"component":{"const":"FileInput"},"variant":{"type":"string","enum":["input","dropzone"],"default":"input","description":":input is the compact native control; :dropzone the drag-and-drop surface."},"accept":{"type":"string","description":"The native accept filter (e.g. \"image/*,.pdf\")."},"described_by":{"type":"string","description":"Ids for the native input's aria-describedby (Field wires this)."},"disabled":{"type":"boolean","description":"Disables the native input and dims the dropzone.","default":false},"hint":{"type":"string","description":"Muted constraints copy under the prompt (formats, size limits)."},"invalid":{"type":"boolean","description":"Marks the control aria-invalid (set by Field/FormBuilder from model errors).","default":false},"multiple":{"type":"boolean","description":"Allows selecting several files; forwarded to the native input.","default":false},"name":{"type":"string","description":"The native input's name - the submitted param (multiple: true wants a name ending in [] for Rails params)."},"prompt":{"type":"string","description":"The dropzone's instruction line - overrides the translated default."}},"required":["component"]},"HoverCard":{"type":"object","description":"A card that reveals preview content when its trigger is hovered. Rules: with_trigger(compose: true) { |wiring| ... } composes YOUR control as the trigger: the block is yielded the trigger wiring (the Stimulus behavior the overlay needs; poppers add id/aria and their trigger slot, modals hand only the open action) - splat it onto a wiring-free control (poetry_sidebar_menu_button, a plain tag); without compose: the classic composed Button renders. Use poetry_hover_card - never hand-roll hover-div previews. THE REACHABLE-ELSEWHERE RULE (non-negotiable): every piece of information in a hover card MUST exist at the trigger link's destination (or another keyboard/touch-reachable surface). The card is pointer-only enrichment - keyboard and touch users never see inside it. The trigger must be a REAL link with a real href - it is the fallback, the touch path, and the keyboard path all at once. For a button LOOK, pass variant:/size: (renders through Button, still an <a> via href:) - never swap the tag to :button. NO interactive elements inside the card - they get tabindex=-1 stripped and become pointer-only traps. Actions belong in a Popover or at the destination. Don't add aria-expanded/haspopup to the trigger - advertising an unreachable surface is worse than silence. Never use HoverCard for hints (Tooltip) or for content users act on (Popover). Prefer defer: for expensive previews - a lazy turbo-frame that fetches on first open.","properties":{"component":{"const":"HoverCard"},"align":{"type":"string","enum":["start","center","end"],"default":"center","description":"Panel alignment along the chosen side."},"align_offset":{"type":"integer","description":"Shift in pixels along the alignment axis.","default":0},"avoid_collisions":{"type":"boolean","description":"Flips and shifts the panel to stay inside the viewport.","default":true},"close_delay":{"type":"integer","description":"Close-grace window in ms over the trigger+content pair.","default":300},"defer":{"type":"string","description":"Defer the card body to a lazy turbo-frame. The panel is hidden until hover, so the fetch fires on first open for free; the component block (if any) becomes the frame's placeholder."},"open":{"type":"boolean","description":"Renders the card already open on page load.","default":false},"open_delay":{"type":"integer","description":"Hover-intent delay in ms before the card opens.","default":600},"side":{"type":"string","enum":["top","right","bottom","left"],"default":"bottom","description":"Which side of the anchor the panel opens on."},"side_offset":{"type":"integer","description":"Gap in pixels between the anchor and the panel.","default":4},"trigger":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The enriched LINK: a real navigable <a> - THE no-JS fallback. tag: passthrough exists but change it knowingly (an <a> is the contract's fallback story). NO aria-haspopup/expanded/describedby - the card is invisible to the accessibility tree on purpose. Built as a lazy anatomy part (rendered at render time, not at with_trigger time). variant:/size: route through Button::Component - Button's href-implies-anchor keeps the trigger a REAL <a> wearing button styling, so the reachable-elsewhere contract holds."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Icon":{"type":"object","description":"Renders an inline SVG icon from the icon set. Rules: Icons are decorative by default (aria-hidden); pass label: when the icon stands alone. Never inline raw <svg> markup where an icon exists - use poetry_icon.","properties":{"component":{"const":"Icon"},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The accessible name - given, the icon is standalone (role=img); absent, it is decorative (aria-hidden)."},"library":{"type":"string","description":"Per-render icon set override (defaults to config.icon_library)."},"name":{"type":"string","description":"The icon's name in the active set. format: :\"icon-name\" is the machine-readable value contract: the registry carries it, and literal names are validated against the icon set statically - a misspelled name is caught before it can crash a render."}},"required":["component"]},"Input":{"type":"object","description":"A form control for entering a single line of text. Rules: Inside a form, never render Input directly - use the FormBuilder's field (it wires ids, errors, and aria). Error styling comes from aria-invalid, set from model errors - never hand-toggle error classes. mask: formats as the user types ('(999) 999-9999'; 9=digit, a=letter, A=upper, *=alnum, #=sign/digit, \\\\ escapes, ? makes the rest optional) - the MASKED text submits; read data-raw for the bare value.","properties":{"component":{"const":"Input"},"disabled":{"type":"boolean","description":"Disables the native input.","default":false},"invalid":{"type":"boolean","description":"Marks the input aria-invalid - the error skin keys on the attribute.","default":false},"mask":{"type":"string","description":"Format-as-you-type mask descriptor ('(999) 999-9999'). Extra knobs (slot char, always-show, auto-clear) ride Stimulus values via data: - one declarative option covers the common case."},"name":{"type":"string","description":"The submitted param name."},"placeholder":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Native placeholder text - not a substitute for a Label."},"type":{"type":"string","description":"The native type attribute (text, email, password, file, ...).","default":"text"},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The current value."}},"required":["component"]},"InputGroup":{"type":"object","description":"One bordered surface combining an input with buttons, icons, or add-ons. Rules: The control INSIDE must be poetry_input_group_input/_textarea - a plain poetry_input keeps its own border+ring and double-chromes the group. Addons are poetry_input_group_addon(align:) wrapping icons/text/buttons; use poetry_input_group_text for muted captions and poetry_input_group_button for tiny actions. The group is a surface, not a label - the control still needs its Label/Field pairing.","properties":{"component":{"const":"InputGroup"},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"],"anyOf":[{"required":["text"]},{"required":["children"]}]},"InputOtp":{"type":"object","description":"A fixed-length, segmented input for one-time passcodes. Rules: Use poetry_input_otp / form.otp_field - NEVER build per-cell inputs (n Tab stops, broken paste, broken SMS autofill, unnameable cells). Label via Field always ('Verification code'); put the length in the hint. groups must sum to length (ArgumentError). Do NOT auto-submit on poetry:otp:complete without a visible confirm affordance - silent submit on the 6th keystroke strands users who mistyped char 3. Never pre-fill value: with a real code in previews/test fixtures beyond dummies; never log the value (it is a live credential). InputOTP is for CODES - passwords use Input type=password, longer identifiers use Input.","properties":{"component":{"const":"InputOtp"},"disabled":{"type":"boolean","description":"Disables the native input (the whole row dims).","default":false},"groups":{"type":"string","description":"Cell clustering, e.g. [3, 3] -> two groups with a separator."},"invalid":{"type":"boolean","description":"aria-invalid on the input; the cells mirror the destructive treatment (set by Field/FormBuilder from the failed verify).","default":false},"length":{"type":"integer","description":"Code length = slot count = maxlength."},"name":{"type":"string","description":"The ONE input serializes params[name] = the code string."},"pattern":{"type":"string","description":":digits (numeric keypad) | :alphanumeric | a custom Regexp - the per-char filter + the native pattern attribute + inputmode.","default":"digits"},"required":{"type":"boolean","description":"aria-required on the input - never native required (the Field rule: required rides server-side validation + aria).","default":false},"separator":{"type":"boolean","description":"role=separator dash between groups (meaningful with 2+ groups).","default":true},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Current code (server-rendered into the input AND the cells). The FormBuilder deliberately never round-trips it (a rejected code is dead)."}},"required":["component"]},"Item":{"type":"object","description":"A generic list row with media, content, and actions. Rules: Rows live inside poetry_item_group (role=list) and each row passes role: \"listitem\" - a role=list parent with roleless children fails aria-required-children. Separate grouped rows with poetry_item_separator. Compose with the slots (media/title/description/actions); loose content lands in the content column after the description. media_variant: :icon for a glyph, :image for a thumbnail (sized/rounded automatically). A clickable row is tag: :a with href: - never wrap an Item in a bare <a>.","properties":{"component":{"const":"Item"},"size":{"type":"string","enum":["default","sm","xs"],"default":"default","description":"The row's density."},"variant":{"type":"string","enum":["default","outline","muted"],"default":"default","description":"The row's visual treatment - :outline boxes it, :muted recedes."},"media_variant":{"type":"string","enum":["default","icon","image"],"default":"default","description":"The media treatment - :icon for a glyph, :image for a thumbnail."},"tag":{"type":"string","description":"The root element - tag: :a (href via passthrough) makes the whole row clickable.","default":"div"},"media":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The leading media cell - a glyph or thumbnail (see media_variant)."},"title":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The title row."},"description":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The muted description line (clamps to two lines)."},"actions":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The trailing actions cell - buttons, a menu, a switch."},"header":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Full-width row above the media/content columns."},"footer":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Full-width row below the media/content columns."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Kbd":{"type":"object","description":"Displays a keyboard key or shortcut. Rules: Kbd renders a real <kbd> - the key text is the content block (⌘, Esc, Ctrl). For a chord (⌘+K) render one Kbd per key inside an inline-flex row.","properties":{"component":{"const":"Kbd"},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"],"anyOf":[{"required":["text"]},{"required":["children"]}]},"Label":{"type":"object","description":"An accessible caption bound to a form control. Rules: Every control gets a Label wired via for_id - placeholder text is never the label.","properties":{"component":{"const":"Label"},"for_id":{"type":"string","description":"The id of the control this label names; omit it for a group label (the group then points at this label via aria-labelledby)."}},"required":["component"]},"Link":{"type":"object","description":"A styled navigational hyperlink. Rules: Use poetry_link for navigation - poetry_button for actions (never an <a> styled by hand). Mark the active nav item with current: true (aria-current), never with a bespoke class. external: true handles target/rel safely - never hand-write target=_blank.","properties":{"component":{"const":"Link"},"underline":{"type":"string","enum":["hover","always","none"],"default":"hover","description":"When the underline appears; :none suits links styled by their container."},"current":{"type":"boolean","description":"Marks this link as the current page via aria-current=page.","default":false},"external":{"type":"boolean","description":"Opens in a new tab with rel=\"noopener noreferrer\" - never hand-write target=_blank.","default":false},"href":{"type":"string","description":"The destination URL."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"],"anyOf":[{"required":["text"]},{"required":["children"]}]},"Marker":{"type":"object","description":"A transcript divider or inline status marker for chat UIs. Rules: The marker text is real announced content - never mark it aria-hidden or role=separator. announce: :status is for the ONE in-flight marker (streaming status); static dividers never announce. Icons ride the icon slot (decorative always): with_icon(name:) for a lucide glyph, with_icon { } for other media (a Spinner mid-run). Use variant: :separator for date/section breaks; :border under pinned headers.","properties":{"component":{"const":"Marker"},"variant":{"type":"string","enum":["default","separator","border"],"default":"default","description":"The divider treatment - :separator for date/section breaks, :border for a full-width rule under pinned headers."},"announce":{"type":"string","enum":["none","status"],"default":"none","description":"Makes the marker a live status region (role=status) for the one in-flight marker; static dividers never announce."},"tag":{"type":"string","description":"The root element's tag.","default":"div"},"icon":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Optional leading visual: name: renders an icon glyph; a block carries other media (a Spinner mid-run). Either way it sits in an aria-hidden cell and stays decorative - the marker root does the announcing."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"],"anyOf":[{"required":["text"]},{"required":["children"]}]},"Menubar":{"type":"object","description":"A horizontal bar of menus, like a desktop application menu. Rules: Use poetry_menubar for app-chrome command menus ONLY - site navigation is NavigationMenu (untrapped), a single actions menu is DropdownMenu. label: is REQUIRED (the bar's accessible name). Never put non-menuitem interactive elements directly in the bar (breaks roving focus + APG roles) - a Toolbar is the component for mixed controls. shortcut: is a visual hint ONLY - it does not register a keybinding; wire real shortcuts separately. Do not hand-wire hover-open-from-cold; hover only slides between menus once one is open (the gated-hover rule). In-menu item rules (destructive variant, inset, checkbox/radio) follow the DropdownMenu family contract.","properties":{"component":{"const":"Menubar"},"dir":{"type":"string","enum":["ltr","rtl"],"description":"The reading direction; :rtl flips arrow-key movement and submenu sides."},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The bar's accessible name - a page may hold more than one menubar."},"loop":{"type":"boolean","description":"Wraps arrow-key movement past either end of the bar.","default":false},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Server-renders the menu with this value open (values default to \"menu-<position>\")."},"menus":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"The top-level menus. Each takes with_trigger (the menu button) plus the family item slots (with_item, with_checkbox_item, with_radio_group, with_sub, with_separator, ...); value: defaults to the menu's position."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Message":{"type":"object","description":"A chat row pairing an author and avatar with message content. Rules: One Message per turn: avatar slot + header/footer slots; the body block holds the Bubbles. align: :end is the local user's side - set it on the Message, never on the Bubbles inside. The avatar slot is decorative context by default - pass meaningful sender identity in the header. Timestamps and delivery state belong in the footer slot (it lifts the avatar automatically).","properties":{"component":{"const":"Message"},"align":{"type":"string","enum":["start","end"],"default":"start","description":"Which side the row sits on; :end mirrors it for the local user's side."},"avatar":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The sender's avatar, kept beside the content column - decorative context; put meaningful sender identity in the header."},"header":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The sender identity line above the bubbles."},"footer":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Timestamps / delivery state below the bubbles."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"MessageScroller":{"type":"object","description":"A streaming-aware transcript that keeps the latest message in view. Rules: Stream by UPDATING a row's text (morph/replace) - appending nodes per token re-announces the row to AT. Rows are poetry_message_scroller_item(id: message.id) - the id is how anchoring and Streams find them. Append new turns with a Turbo Stream targeting the content element's dom id. History loads PREPEND into the content element - the controller preserves the reading position. Never nest a second scroll container inside the viewport.","properties":{"component":{"const":"MessageScroller"},"auto_scroll":{"type":"boolean","description":"Follows the newest message while the reader sits at the bottom; scrolling up releases the follow.","default":true},"default_scroll_position":{"type":"string","enum":["start","end","last-anchor"],"default":"end","description":"Where the viewport lands on connect: the newest message (:end), the oldest (:start), or the last anchor: true row (:\"last-anchor\")."},"jump_button":{"type":"boolean","description":"Renders the floating jump-to-latest button (shown once the reader leaves the bottom).","default":true},"preserve_scroll_on_prepend":{"type":"boolean","description":"Keeps the reading position stable when history prepends into the content element.","default":true},"track_visibility":{"type":"boolean","description":"Opt-in observation of which rows are on screen - emits a visibility event as the visible set changes.","default":false},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"MetadataList":{"type":"object","description":"A key-value list for labeled attributes on detail pages. Rules: Record facts on a detail page belong in a MetadataList - never a hand-rolled grid of label/value divs (this is the <dl> the page owes its readers). Each with_item takes label: and the value as its block - values compose freely (text, a Badge, a Link, a Timestamp). columns: :two / :three spread the facts on wide viewports; orientation: :horizontal puts labels beside values (the classic key/value sheet) - pick one per surface. For editable facts pair each value with its edit affordance inside the item block; the list itself stays read-only vocabulary.","properties":{"component":{"const":"MetadataList"},"columns":{"type":"string","enum":["one","two","three"],"default":"one","description":"How many columns the facts spread across on wide viewports."},"orientation":{"type":"string","enum":["vertical","horizontal"],"default":"vertical","description":"Label placement - above the value, or beside it for the classic key/value sheet."},"items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"The facts. Each takes label: (the fact's name, the <dt>) and the value as its block (the <dd>)."}},"required":["component"]},"Meter":{"type":"object","description":"A gauge that shows a quantity within a known range. Rules: A quantity within a range is a Meter (disk, seats, strength); an operation's completion over time is Progress. There is NO indeterminate meter - unknown duration means Spinner. label: is REQUIRED - the meter's accessible name and visible caption. value_text: replaces the visible readout verbatim (\"3 of 4 seats\"); without it the readout shows the percentage of the RANGE. No aria-valuetext - ARIA 1.2 deprecated it on role=meter; aria-valuenow carries the value.","properties":{"component":{"const":"Meter"},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The meter's accessible name and visible caption."},"max":{"type":"integer","description":"The range's upper bound - must exceed min:.","default":100},"min":{"type":"integer","description":"The range's lower bound.","default":0},"show_value":{"type":"boolean","description":"Set false to hide the visible readout.","default":true},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The measured quantity, clamped into min:..max:."},"value_text":{"type":"string","description":"Verbatim human-readable value (\"3 of 4 seats\") replacing the percentage readout. Visible readout only - ARIA 1.2 deprecated aria-valuetext on role=meter, so aria-valuenow carries the value."}},"required":["component"]},"NativeSelect":{"type":"object","description":"A styled wrapper around the real native select control. Rules: This is a REAL <select> - use it for plain picking; the JS Select is for styled options. Pair it with a Label (for_id: its id) or a Field - a bare select has no accessible name. The fast path is options: [[label, value], ...] + selected:; a content block overrides it.","properties":{"component":{"const":"NativeSelect"},"described_by":{"type":"string","description":"Space-separated hint/error ids wired to the SELECT itself - a raw aria-describedby in html_attributes would land on the wrapper div, unassociated for assistive technology."},"disabled":{"type":"boolean","description":"Disables the native select; the wrapper dims the whole pair.","default":false},"invalid":{"type":"boolean","description":"Marks the select invalid (aria-invalid on the element itself).","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The accessible name for label-less placements (a visible Label paired via id:/for_id: is still the default pattern)."},"name":{"type":"string","description":"The submitted field name, forwarded to the native select."},"size":{"type":"string","enum":["default","sm"],"default":"default","description":"The control size axis; :sm suits dense toolbars and table rows."}},"required":["component"]},"NavigationMenu":{"type":"object","description":"A site-navigation bar with links and optional dropdown panels. Rules: label: is REQUIRED (the nav landmark's accessible name). with_item(title, value:) declares a trigger + panel; with_link(title, href:) is a top-level destination - use links for pages, panels for groups of links. Panel content is poetry_navigation_menu_link entries (active: marks the current page) - never buttons; navigation navigates. This is a DISCLOSURE bar: Tab moves through it normally and nothing traps - do not wire menu/menuitem roles. Rich panels (title + description grids) want viewport: true - the shared morphing card contains and sizes them; the default per-item mode suits simple link lists (the top-nav block shows the viewport pattern).","properties":{"component":{"const":"NavigationMenu"},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The nav landmark's accessible name - a page may hold more than one nav."},"viewport":{"type":"boolean","description":"Opts into the shared morphing viewport: panels adopt into one positioned card that morphs size and position between triggers. Off, each panel opens under its own item (also the no-JS shape).","default":false},"items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"The bar entries. with_item(title, value:) { panel } declares a trigger + panel; with_item(title, href:) a top-level link (with_link is the shorthand)."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"NumberField":{"type":"object","description":"A numeric input with increment and decrement steppers. Rules: Use poetry_number_field / form.number_field - never a hand-rolled spinner or a bare input type=number. The server reads params[<name>] as the raw number string - display formatting (format:) never changes what submits. Steppers are mouse/touch affordances (tabindex -1); keyboard users step with ArrowUp/Down (Shift = large_step, Alt = small_step) on the input itself. Pair it with a Label/Field for the accessible name - the component ships none. format: takes Intl.NumberFormatOptions as a Hash ({ style: \"currency\", currency: \"USD\" }); pick locale: to pin parsing separators.","properties":{"component":{"const":"NumberField"},"described_by":{"type":"string","description":"aria-describedby wiring for Field hint/error pairing."},"disabled":{"type":"boolean","description":"Disables both inputs and the steppers; the group chrome dims.","default":false},"format":{"type":"string","description":"Intl.NumberFormatOptions for the DISPLAY (submission stays raw)."},"invalid":{"type":"boolean","description":"Marks the field invalid (aria-invalid on the visible input; the group wears the destructive ring).","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Standalone accessible name -> aria-label on the visible input. Inside a form, the Field label wires ids instead - pass neither and pair with poetry_label/form."},"large_step":{"type":"number","description":"The Shift-arrow step size (the coarse jump).","default":10.0},"locale":{"type":"string","description":"Locale tag pinning the display and parsing separators; the page locale otherwise."},"max":{"type":"number","description":"The upper clamp for stepping and native validation."},"min":{"type":"number","description":"The lower clamp for stepping and native validation."},"name":{"type":"string","description":"The submitted field name - rides the hidden number input."},"placeholder":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Placeholder text for the empty input."},"readonly":{"type":"boolean","description":"Makes the visible input read-only (steppers and typing inert).","default":false},"required":{"type":"boolean","description":"Requires a value - native validation rides the hidden input.","default":false},"small_step":{"type":"number","description":"The Alt-arrow step size (the fine adjustment).","default":0.1},"snap":{"type":"boolean","description":"Snaps stepped values to step multiples counted from min:.","default":false},"step":{"type":"number","description":"The arrow-key / stepper increment.","default":1.0},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Initial value - a number; nil renders empty (null semantics)."},"wheel":{"type":"boolean","description":"Opt-in wheel stepping while the input is focused.","default":false}},"required":["component"]},"Pagination":{"type":"object","description":"Navigation for moving between pages of content. Rules: poetry_pagination(current:, total:, path:) - never hand-build the <nav>/<ul>/<li> list. path: is a callable ->(page) { url } (e.g. ->(p) { products_path(page: p) }). The current page is aria-current=page; current_variant: :outline (the default) or :filled (the primary treatment - unambiguous active state); the rest are ghost links. edges: :icons renders chevron-only Previous/Next (the table-footer posture); :none drops them for a bare page list; pages: false drops the numbers (pair with edges: :icons for the compact pager). Host paginates with kaminari, pagy (v43+), or will_paginate? Run bin/rails g poetry:pagination (no argument = detect and install an adapter for each loaded gem) and keep calling paginate / poetry_pagy_nav / will_paginate(renderer: PoetryLinkRenderer) - never hand-wire poetry_pagination around a paginator gem.","properties":{"component":{"const":"Pagination"},"current":{"type":"integer","description":"The current page number (1-based)."},"current_variant":{"type":"string","enum":["outline","filled"],"default":"outline","description":"How the current page link renders: :outline, or :filled for the primary Button treatment (an unambiguous active state)."},"edges":{"type":"string","enum":["labeled","icons","none"],"default":"labeled","description":"The Previous/Next treatment: :labeled (chevron + responsive text), :icons (chevron only - table footers), :none (no edge links)."},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The nav landmark's accessible name."},"next_label":{"type":"string","description":"The Next link's visible text (hidden on narrow viewports).","default":"Next"},"pages":{"type":"boolean","description":"Set false to drop the numbered links - the compact two-button pager (pair with edges: :icons).","default":true},"previous_label":{"type":"string","description":"The Previous link's visible text (hidden on narrow viewports).","default":"Previous"},"siblings":{"type":"integer","description":"How many page links flank the current page before gaps elide to ellipses.","default":1},"total":{"type":"integer","description":"The total page count."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Popover":{"type":"object","description":"Rich floating content anchored to a trigger. Rules: with_trigger(compose: true) { |wiring| ... } composes YOUR control as the trigger: the block is yielded the trigger wiring (the Stimulus behavior the overlay needs; poppers add id/aria and their trigger slot, modals hand only the open action) - splat it onto a wiring-free control (poetry_sidebar_menu_button, a plain tag); without compose: the classic composed Button renders. Use poetry_popover - never hand-roll an anchored role=dialog panel with Tailwind. Popover content is INTERACTIVE - for text-only hover hints use Tooltip; for pointer-only previews use HoverCard. Give the panel a name: use with_title (preferred) or label: - a role=dialog without a name fails the audit. Icon-only triggers MUST have an accessible name (the composed Button's label: rule). Default is NON-modal (modal: false) - reach for modal: true only when stray outside interaction would corrupt the task; reach for Dialog when the task deserves full modality. Critical-path panels must also be reachable without JS (full page or server-rendered open: true) - popovers are JS-required interaction. Do not nest a Popover inside a Popover - restructure (the layer stack allows it; comprehension does not).","properties":{"component":{"const":"Popover"},"align":{"type":"string","enum":["start","center","end"],"default":"center","description":"Panel alignment along the chosen side."},"align_offset":{"type":"integer","description":"Shift in pixels along the alignment axis.","default":0},"avoid_collisions":{"type":"boolean","description":"Flips and shifts the panel to stay inside the viewport.","default":true},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"role=dialog fallback name when no title part is present."},"modal":{"type":"boolean","description":"Reserves interaction for the panel while open; the default keeps the rest of the page interactive.","default":false},"open":{"type":"boolean","description":"Server-renders the panel open.","default":false},"side":{"type":"string","enum":["top","right","bottom","left"],"default":"bottom","description":"Which side of the anchor the panel opens on."},"side_offset":{"type":"integer","description":"Gap in pixels between the anchor and the panel.","default":4},"trigger":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The control that opens the panel - a composed Button. The slot owns the aria-haspopup/expanded/controls wiring regardless of the composed content, so composition cannot drop the aria; aria-controls renders even while closed (the stable id is the wiring's resolution seam)."},"anchor":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Optional alternate anchor: when present, the panel positions against IT instead of the trigger (targets beat selectors in the positioning fallback chain)."},"title":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Panel heading - presence wires the content's aria-labelledby, so the title names the dialog."},"description":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Supporting text - presence wires the content's aria-describedby."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Progress":{"type":"object","description":"A determinate progress bar toward task completion. Rules: label: is REQUIRED - it is the progressbar's accessible name and the visible caption. value: is the current progress (0..max:, default max 100); the component computes the width. For an UNKNOWN duration use Spinner, not Progress - this bar is determinate.","properties":{"component":{"const":"Progress"},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The progressbar's accessible name and visible caption."},"max":{"type":"integer","description":"The completion value.","default":100},"show_value":{"type":"boolean","description":"Set false to hide the percent readout.","default":true},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The current progress, clamped into 0..max:."}},"required":["component"]},"Questionnaire":{"type":"object","description":"A one-question-at-a-time survey flow with choices, free answers, and validation. Rules: The root is a REAL form (url:/method:) - answers submit as ordinary params; validate server-side and re-render invalid items with error:. One with_item per question (name: is the param key); choices via item.with_choice, an optional free-text answer via item.with_input. multiple: true renders checkboxes named <name>[] (Rails array params) - a recorded divergence from the ported source's repeated bare names. required: true gates Next/submit client-side; the server stays the truth on submit. shortcuts: :letters or :numbers labels each choice with a key (server-rendered) and enables one-keystroke answering. Skip renders only while the active item is optional - never force-hide it. with_progress { custom } replaces the readout; data-current/data-total on the progress element and a [data-progress-count] child stay live for segment bars and counters.","properties":{"component":{"const":"Questionnaire"},"default_item":{"type":"string","description":"The initially active item by name; default is the first item."},"http_method":{"type":"string","description":"The form's HTTP verb. Named http_method (not method:) - an option named `method` would shadow Object#method.","default":"post"},"next_label":{"type":"string","description":"The forward-navigation button's text.","default":"Next"},"previous_label":{"type":"string","description":"The back-navigation button's text.","default":"Previous"},"shortcuts":{"type":"string","enum":["letters","numbers"],"description":"nil (off), :letters (A, B, C...) or :numbers (1-9): server- rendered key labels + one-keystroke answering."},"skip_label":{"type":"string","description":"The skip button's text (shown only while the active item is optional).","default":"Skip"},"submit_label":{"type":"string","description":"The final submit button's text (replaces Next on the last item).","default":"Submit"},"url":{"type":"string","description":"The form's submit URL - answers post here as ordinary params."},"progress":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. with_progress (bare) renders the auto \"Question X of Y\" text; with_progress { custom } replaces it (marked data-custom so the controller leaves it alone). class: merges onto the progress element (e.g. w-full for a full-width segment bar over the base w-fit)."}},"required":["component"]},"RadioGroup":{"type":"object","description":"A set of options where only one can be selected at a time. Rules: Use poetry_radio_group / form.radio_group - never hand-roll role=radio buttons. Every item MUST have a unique value: (ArgumentError on duplicates). The GROUP must be labelled - label: (or aria-labelledby) - an unlabelled radiogroup is an APG violation (ArgumentError). Pair every item with a visible label (item label: renders the Label for= pairing) - a bare dot is not an option. variant: :card renders the choice-card row (title + description: inside a selectable bordered label) - the pick-a-plan pattern; the whole card toggles the radio. NEVER write the checked attributes (data-checked/data-unchecked) without aria-checked (the controller writes both; agents patching DOM must too). Do not use RadioGroup for navigation or immediate actions; checking must not submit or navigate by itself. 7+ options: use Select instead. Wire errors through Field/FormBuilder (invalid: + describedby on the root) - never a bare red ring.","properties":{"component":{"const":"RadioGroup"},"disabled":{"type":"boolean","description":"Disables every item (root-level).","default":false},"invalid":{"type":"boolean","description":"aria-invalid on the items (the destructive ring) - set by Field/FormBuilder from model errors.","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The group accessible name -> aria-label (or wire aria-labelledby yourself) - REQUIRED: an unlabelled radiogroup fails the audit."},"loop":{"type":"boolean","description":"Arrow-key navigation wraps at the ends.","default":true},"name":{"type":"string","description":"The shared form name for every hidden radio (FormBuilder derives object[method])."},"orientation":{"type":"string","enum":["both","vertical","horizontal"],"default":"both","description":"Keyboard axis: :both allows all four arrows (the standard radio pattern); :vertical/:horizontal restrict the axis. No visual effect."},"required":{"type":"boolean","description":"aria-required on the ROOT only - never native required on the hidden inputs (constraint-validation focus would land on an aria-hidden input).","default":false},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The checked item's value; nil = nothing checked (pre-selection)."},"items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"One item per option: a real button[role=radio] carrying its own hidden native radio; label: renders the dot beside a paired Label. variant: :card renders the choice-card row instead - title (+ optional description:) inside a selectable bordered label, the radio pinned to the right."}},"required":["component"]},"Resizable":{"type":"object","description":"Panels with draggable handles for resizing adjacent regions. Rules: Declare panels with with_panel(default_size:, min_size:, max_size:) - sizes are PERCENTAGES and the component interleaves the separator handles. direction: :horizontal is side-by-side (the default); :vertical stacks. Handles are keyboard splitters (arrows step, Home/End jump) - never replace them with styled divs. Nest a group inside a panel for two-axis layouts - groups self-scope.","properties":{"component":{"const":"Resizable"},"direction":{"type":"string","enum":["horizontal","vertical"],"default":"horizontal","description":"The group axis: :horizontal lays panels side by side, :vertical stacks them."},"grip":{"type":"boolean","description":"Renders the grip dots on each handle.","default":false},"panels":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"One panel per call: default_size/min_size/max_size are percentages of the group; the content block is required."}},"required":["component"]},"ScrollArea":{"type":"object","description":"A bounded, keyboard-reachable scroll region with styled scrollbars. Rules: Size the scroll area with classes (h-72 w-48, max-h-96) - content decides the overflow. label: is REQUIRED - the viewport is focusable, and a focusable region needs a name (role=region + aria-label). This is a NATIVE scroll surface - never bolt scroll JS onto it; use MessageScroller for chat transcripts.","properties":{"component":{"const":"ScrollArea"},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The region's accessible name (role=region + aria-label) - required, because a focusable region must be named."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"],"anyOf":[{"required":["text"]},{"required":["children"]}]},"SearchField":{"type":"object","description":"A search input with clear and search affordances. Rules: Search inputs are a SearchField (poetry_search_field) - never a bare Input with a hand-rolled clear button; Escape-clears and focus retention ride the controller. Enter submits the surrounding form natively - wrap it in a form/turbo-frame for live search; listen for poetry:search-field:clear to reset results. Pair with a Label/Field for the accessible name, or pass label: standalone.","properties":{"component":{"const":"SearchField"},"described_by":{"type":"string","description":"aria-describedby on the input - Field hint/error wiring."},"disabled":{"type":"boolean","description":"Disables the input and hides the clear affordance.","default":false},"invalid":{"type":"boolean","description":"aria-invalid on the input - set by Field/FormBuilder from model errors.","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The accessible name (aria-label) for standalone use - or pair with a Label/Field instead."},"name":{"type":"string","description":"The form field name on the search input."},"placeholder":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Hint text shown while the field is empty."},"readonly":{"type":"boolean","description":"The query can be read but not edited; the clear affordance hides.","default":false},"required":{"type":"boolean","description":"Marks the input required for native constraint validation.","default":false},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The pre-filled query; presence unhides the clear affordance."}},"required":["component"]},"Select":{"type":"object","description":"A dropdown for choosing one option from a list. Rules: Use poetry_select (f.poetry_select in forms) - never hand-roll role=listbox popups, and never fake a select with DropdownMenu radio items bound to a hidden field. Options are VALUES. If activating an option should DO something beyond setting a value, it's a DropdownMenu item. In forms, ALWAYS go through f.poetry_select - it wires name/id/value/errors/required; bare poetry_select in a form is a smell. Every Select MUST be named: a Field label (id: + label[for]) or aria-label. A bare unnamed select fails at render - do not suppress it. NEVER write aria-selected without its data-selected twin, and NEVER write the display text without writing the native select's value first - the controller does all three; agents patching DOM must too. Do not put interactive elements inside options (an option IS the interactive unit). Long/filterable/async lists or multi-select -> Combobox, not a 50-option Select; 2-4 options -> RadioGroup. The hidden native select is plumbing - never target it with styles, labels, or Capybara selectors (drive the combobox like a user). Positioning is popper-only: poetry Select drops below the trigger (the item-aligned overlay mode is not ported - a documented parity delta).","properties":{"component":{"const":"Select"},"align":{"type":"string","enum":["start","center","end"],"default":"start","description":"Popup alignment along the chosen side's edge."},"align_item_with_trigger":{"type":"boolean","description":"Opens the popup OVER the trigger with the selected item aligned on it (native-select feel); falls back to regular below-the-trigger positioning on touch, viewport-edge triggers, or squeezed heights.","default":false},"align_offset":{"type":"integer","description":"Pixel shift along the alignment axis.","default":0},"avoid_collisions":{"type":"boolean","description":"Flips/shifts the popup to keep it inside the viewport.","default":true},"dir":{"type":"string","enum":["ltr","rtl"],"description":"Text direction for the select and its popup."},"disabled":{"type":"boolean","description":"Disables the trigger and the hidden native <select>.","default":false},"loop":{"type":"boolean","description":"Arrow-key navigation wraps from the last option back to the first.","default":false},"modal":{"type":"boolean","description":"While open, blocks pointer interaction outside the popup.","default":true},"name":{"type":"string","description":"The form field name, carried by the hidden native <select>."},"open":{"type":"boolean","description":"Server-renders the popup already open.","default":false},"placeholder":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Text shown in the trigger until an option is committed; also rendered as the blank native <option> (posts \"\" when untouched)."},"required":{"type":"boolean","description":"Marks the hidden native <select> required - native constraint validation blocks submission while unset.","default":false},"side":{"type":"string","enum":["top","right","bottom","left"],"default":"bottom","description":"Preferred popup side relative to the trigger."},"side_offset":{"type":"integer","description":"Gap in pixels between trigger and popup.","default":4},"size":{"type":"string","enum":["sm","default"],"default":"default","description":"The trigger size axis."},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The committed option value - the item whose value: matches renders as selected and its label fills the trigger."},"trigger":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Optional custom trigger content rendered BEFORE the value span (rare); the component owns role=combobox + the aria wiring + the chevron regardless, so composition cannot drop the contract."},"items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"The option UNION: item | group (label + items) | separator - one ordered collection (interleaving preserved; items and groups are part COMPONENTS so option registration follows render/DOM order). Scroll buttons, the viewport, and the native select are component-owned anatomy, never caller-placed."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"SensitiveInput":{"type":"object","description":"A masked secret field with a reveal toggle and a copy button. Rules: Secrets shown-on-demand are a SensitiveInput (poetry_sensitive_input) - never a bare password Input with a hand-rolled eye; the masked-container contract (role=button, focus discipline, blur re-mask) rides the controller. label: feeds the masked announcement (\"{label}, masked.\") - pair with a Label/Field for the visible caption. copy: true adds copy-without-revealing; leave it off for password-change forms. Values re-mask on blur BY DESIGN - do not fight it with reveal-state persistence.","properties":{"component":{"const":"SensitiveInput"},"copy":{"type":"boolean","description":"Adds the copy-without-revealing button in the trailing cell.","default":false},"described_by":{"type":"string","description":"aria-describedby on the input - Field hint/error wiring."},"disabled":{"type":"boolean","description":"Disables the input and drops the masked group's tab stop.","default":false},"invalid":{"type":"boolean","description":"aria-invalid on the input - set by Field/FormBuilder from model errors.","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The accessible name - feeds the input's aria-label and the masked announcement (\"{label}, masked.\"); pair with a visible Label/Field caption."},"name":{"type":"string","description":"The form field name on the real input."},"placeholder":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Hint text shown while the field is empty."},"readonly":{"type":"boolean","description":"The value can be revealed and copied but not edited.","default":false},"required":{"type":"boolean","description":"Marks the real input required.","default":false},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The secret's current value; present = first paint is masked, blank = the empty state."}},"required":["component"]},"Separator":{"type":"object","description":"A thin divider between content, decorative or semantic. Rules: A purely visual divider stays decorative (the default): aria-hidden, role absent. Set decorative: false only when the divide is semantically meaningful (role=separator).","properties":{"component":{"const":"Separator"},"decorative":{"type":"boolean","description":"Whether the divide is purely visual (aria-hidden) or a semantic boundary (role=separator).","default":true},"orientation":{"type":"string","enum":["horizontal","vertical"],"default":"horizontal","description":"The divider's axis."}},"required":["component"]},"Sheet":{"type":"object","description":"A dialog that slides in from a screen edge. Rules: Open sheets with with_trigger(...) - never a hand-wired button. with_title is REQUIRED (the accessible name) - the inherited Dialog rule. Pick side by content: navigation left, detail/edit right, pickers bottom. Do not put must-not-lose confirmations in a Sheet - that is AlertDialog. Do not rebuild a centered Dialog with a Sheet; use Dialog.","properties":{"component":{"const":"Sheet"},"side":{"type":"string","enum":["top","right","bottom","left"],"default":"right","description":"The edge the sheet slides in from - a physical direction (right stays right in RTL)."},"dismissible":{"type":"boolean","description":"Backdrop clicks close the dialog; false keeps confirmations from being dismissed accidentally (Esc still closes).","default":true},"show_close_button":{"type":"boolean","description":"Renders the corner X; false forces a deliberate footer choice (footer actions and Esc remain). Sheet inherits this.","default":true},"trigger":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The trigger is a poetry Button wired to open the dialog - agents pass Button props: with_trigger(variant: :outline) { \"Open\" }."},"title":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The heading - the dialog's accessible name; required."},"description":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Muted copy under the title, wired to aria-describedby."},"footer":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The action row at the bottom of the panel."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Sidebar":{"type":"object","description":"A collapsible app-shell navigation column. Rules: Wrap the WHOLE shell: with_nav is the sidebar column, with_inset is the page area (the trigger lives in the inset). Read the persisted state server-side - open: cookies[:sidebar_state] != \"false\" - so the first paint has no collapse flash. collapsible: :icon keeps icon rails visible when collapsed; :offcanvas slides it fully away; :none is a static column. Menu entries are poetry_sidebar_menu_button(href:) links (active: marks the current route) - navigation navigates.","properties":{"component":{"const":"Sidebar"},"collapsible":{"type":"string","enum":["offcanvas","icon","none"],"default":"offcanvas","description":"What collapsing does: slide fully away, shrink to an icon rail, or :none for a static column."},"open":{"type":"boolean","description":"The expanded/collapsed state at first paint - feed it from the persisted cookie so there is no collapse flash.","default":true},"side":{"type":"string","enum":["left","right"],"default":"left","description":"Which edge the column hangs on."},"variant":{"type":"string","enum":["sidebar","floating","inset"],"default":"sidebar","description":"The column treatment: flush column, floating card, or inset panel."},"nav":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The sidebar column's content (required) - groups, menus, header/footer."},"inset":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The page area beside the column - rendered as the <main> inset."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Skeleton":{"type":"object","description":"A pulsing placeholder shown while content loads. Rules: Skeleton is a loading placeholder - size it with classes (h-4 w-32); it has no content of its own. Mark the live region that will replace it (aria-busy on the container), not the skeleton.","properties":{"component":{"const":"Skeleton"}},"required":["component"]},"Slider":{"type":"object","description":"An input for selecting a value or range along a track. Rules: Use poetry_slider / form.slider - never hand-roll a draggable div. Every thumb MUST have a distinct accessible name (label: - array of two for ranges). ArgumentError otherwise. Give value_text: whenever the number alone is meaningless ('$200', '80%') - SR users hear aria-valuetext. Range mode: values must be sorted [low, high]; use min_steps_between_thumbs to keep a meaningful gap. Do not use Slider for precise known-number entry (use Input type=number) or in no-JS-required forms (native input type=range). Debounce on poetry:slider:commit, never on :change (change fires every drag frame). Never transition the thumb/range position with CSS - geometry must track the pointer.","properties":{"component":{"const":"Slider"},"described_by":{"type":"string","description":"Field hint/error wiring -> aria-describedby on EACH thumb."},"disabled":{"type":"boolean","description":"Renders the control inert; the hidden inputs still submit the server value.","default":false},"inverted":{"type":"boolean","description":"Flips the value direction along the axis; composes with RTL (both = ltr math).","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Per-thumb accessible names -> aria-label; range REQUIRES two. Alternatively labelled_by (external wiring). Enforced."},"labelled_by":{"type":"string","description":"aria-labelledby for the thumb(s) - the Field-wrapped single slider derives its name from the field label this way."},"max":{"type":"number","description":"The track's upper bound; must exceed min:.","default":100.0},"min":{"type":"number","description":"The track's lower bound.","default":0.0},"min_steps_between_thumbs":{"type":"integer","description":"Range-mode minimum gap in STEPS: high - low >= n*step; thumbs can never cross.","default":0},"name":{"type":"string","description":"Form name; single thumb -> name; range -> name + \"[]\" per input (the Rails array-param convention)."},"orientation":{"type":"string","enum":["horizontal","vertical"],"default":"horizontal","description":"The track axis."},"step":{"type":"number","description":"Snap increment; decimal steps supported (precision-aware rounding lives in the controller's math core).","default":1.0},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Single-thumb value. ArgumentError if given with values:."},"value_text":{"type":"string","description":"aria-valuetext formatter: a proc (v -> \"$200\") or an i18n key with %{value}; optional - falls back to the bare number."},"values":{"type":"string","description":"Range mode: [low, high] -> two thumbs, sorted. ArgumentError if given with value:, unsorted, or length != 2."}},"required":["component"]},"Spinner":{"type":"object","description":"An indeterminate loading indicator that announces itself. Rules: Spinner announces itself (role=status + aria-label) - never a bare spinning div. Set label: for the loading context ('Saving…'); the default is 'Loading'.","properties":{"component":{"const":"Spinner"},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"What assistive tech announces - name the loading context."}},"required":["component"]},"Stat":{"type":"object","description":"A single KPI: a muted label over a large metric value. Rules: One Stat is ONE metric: label: names it, the content block is the value - compose several in a grid (typically each inside a Card) for a dashboard row. delta: carries the change text ('+12.5%'); trend: (up/down/flat) sets the arrow and the default sentiment. Override sentiment: :positive when DOWN is the good direction (costs, churn, error rate) - color follows sentiment, never the arrow. Keep the value textual - tabular numerals are already applied; units and formatting belong in the content ('$45,231', '99.98%'). A Stat is not a chart: a trend over time goes in the media slot (or use poetry-charts).","properties":{"component":{"const":"Stat"},"delta":{"type":"string","description":"The change text shown in the pill beside the value (\"+12.5%\")."},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The metric's name, shown muted above the value."},"sentiment":{"type":"string","enum":["positive","negative","neutral"],"description":"Overrides the trend-derived sentiment - color follows sentiment, never the arrow (set :positive when DOWN is the good direction)."},"trend":{"type":"string","enum":["up","down","flat"],"default":"up","description":"The arrow direction; also derives the default sentiment."},"description":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Muted supporting copy rendered under the value."},"media":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The trend-visual slot (sparkline, chart, glyph) below the text stack."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"],"anyOf":[{"required":["text"]},{"required":["children"]}]},"Switch":{"type":"object","description":"A toggle for turning a setting on or off. Rules: Use poetry_switch - never a styled checkbox pretending to be a switch (role=switch announces on/off; that's the point). Switch = instant effect; Checkbox = staged for submit. If nothing happens until a Save button, use Checkbox. Every switch needs an accessible name (Label/Field for= or label:). Switches are BINARY - no indeterminate, ever (ArgumentError). A third state means a different component. The instant-effect recipe pairs the flip with server persistence (Turbo auto-submit) - never flip UI-only for a setting the user believes is saved. NEVER write the checked attributes (data-checked/data-unchecked) without aria-checked and the input sync (the controller writes all three).","properties":{"component":{"const":"Switch"},"size":{"type":"string","enum":["default","sm"],"default":"default","description":"The control's size axis; the thumb scales to match."},"checked":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicBoolean","description":"The server-rendered on/off state."},"disabled":{"type":"boolean","description":"Disables the control and its hidden input - a disabled switch neither toggles nor submits.","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The accessible name, rendered as aria-label - not visible text."},"name":{"type":"string","description":"Names the hidden input, making the switch a form participant."},"required":{"type":"boolean","description":"Marks the switch required via aria-required (never the native attribute).","default":false},"unchecked_value":{"type":"string","description":"Submitted when the switch is off, so the field always posts. Ignored without name:.","default":"0"},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Submitted when the switch is on. Ignored without name:."}},"required":["component"]},"Table":{"type":"object","description":"A semantic table for rows and columns of data. Rules: Compose the table with the part helpers (poetry_table_header/_body/_row/_head/_cell) - they carry the data-slot + classes onto real thead/tbody/tr/th/td. A column header is poetry_table_head (a <th>); a data cell is poetry_table_cell (a <td>). Mark a selected row with data-selected on poetry_table_row - never a bespoke highlight class. sticky_header: true pins the thead while the container scrolls - it only scrolls once container_class: caps the height (\"max-h-96\"); without a cap nothing sticks. sticky_header requires scroll_label: - the container becomes a focusable scroll region (tabindex=0 + role=region) and a keyboard-reachable region needs a name (the ScrollArea rule).","properties":{"component":{"const":"Table"},"scroll_label":{"type":"string","description":"The scroll region's accessible name, required with sticky_header: a scrollable region a keyboard can't reach fails WCAG (axe scrollable-region-focusable), and a focusable region needs a name."},"sticky_header":{"type":"boolean","description":"Pins the <thead> while the container scrolls; needs a height cap (container_class:) to take effect, and requires scroll_label:.","default":false},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"],"anyOf":[{"required":["text"]},{"required":["children"]}]},"Tabs":{"type":"object","description":"A tablist of triggers that switch between content panels. Rules: Declare tabs with with_tab(title, value:) + the panel block (or defer: for a lazy turbo-frame panel) - never hand-wire role=tab/tabpanel ids. panel: false declares a list-only tab (no tabpanel renders, the trigger drops aria-controls) - for demos/pattern shells; real tab sets carry panels. default: picks the server-rendered active tab (the first enabled tab otherwise) - the panel is visible without JS. label: names the tablist (aria-label) - recommended whenever the page has several tab sets. Tabs switch VIEWS of one context; use navigation (links) when the URL should change.","properties":{"component":{"const":"Tabs"},"default":{"type":"string","description":"The value of the server-rendered active tab; defaults to the first enabled tab. Raises when it matches no tab."},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The tablist's accessible name - recommended when a page has several tab sets."},"orientation":{"type":"string","enum":["horizontal","vertical"],"default":"horizontal","description":"The tab axis; :vertical stacks the triggers and flips the arrow keys."},"variant":{"type":"string","enum":["default","line"],"default":"default","description":"The list's visual treatment: :default a filled capsule, :line an underline indicator."},"tabs":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Declares one tab: the title, its value:, and the panel as the block (defer: swaps in a lazy turbo-frame panel; panel: false declares a list-only tab). Omitting all three raises."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"TagGroup":{"type":"object","description":"A set of removable chips or tokens. Rules: Removable chips are a TagGroup - never hand-rolled badges with x buttons; removal keyboard (Delete/Backspace), focus recovery, and the live region ride the controller. label: is REQUIRED (the grid's accessible name, rendered as a caption span). name: turns the group into a form value - one hidden <name>[] input per tag submits; removing a tag removes its input. Removal is cancelable: listen for poetry:tag-group:remove and preventDefault to own the removal server-side (Turbo re-render). Choosing from options is Combobox multiple; toggling fixed choices is ToggleGroup - a TagGroup holds items that exist until removed.","properties":{"component":{"const":"TagGroup"},"described_by":{"type":"string","description":"Space-separated hint/error ids for the GRID (the labelled element) - a raw aria-describedby in html_attributes would land on the outer wrapper div, unassociated for AT."},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The grid's accessible name, rendered as a caption span. Required."},"name":{"type":"string","description":"Makes the group a form value: one hidden <name>[] input submits per tag."},"tags":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Declares one chip. value: is its identity (and form value); label: is the accessible name and the visible text when no block is given; removable: false drops the remove button."}},"required":["component"]},"Textarea":{"type":"object","description":"A form control for entering multiple lines of text. Rules: Wire through Field/FormBuilder (control_attributes) - never hand-write the aria plumbing. Placeholder is NOT a label - pair with Label/Field always. Use Textarea for free text; Input for single-line; do not bolt a JS autosizer on (auto-grow is CSS). Do not set native required - required flows as aria-required via Field.","properties":{"component":{"const":"Textarea"},"disabled":{"type":"boolean","description":"Disables the control and forwards to the native element.","default":false},"invalid":{"type":"boolean","description":"Marks the field errored (aria-invalid + the destructive ring); set by Field/FormBuilder from model errors.","default":false},"name":{"type":"string","description":"The submitted field name."},"placeholder":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Hint text shown while empty - never a substitute for a label."},"rows":{"type":"integer","description":"The initial visual rows - the minimum height under CSS auto-grow, and the fixed size in browsers without it."},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The initial text, rendered as the element's content."}},"required":["component"]},"TimeField":{"type":"object","description":"A segmented input for typing a time one part at a time. Rules: Time entry is a TimeField (poetry_time_field / form.time_field) - never a masked Input or a pair of selects; params[<name>] is HH:MM (HH:MM:SS with seconds:). 12- vs 24-hour follows the user's locale automatically (the dayPeriod segment appears only under twelve-hour cycles); hour_cycle: pins it when a product must. For a date AND a time, use a DateTimeField (poetry_date_time_field / form.datetime_field): one control, one datetime-local value.","properties":{"component":{"const":"TimeField"},"described_by":{"type":"string","description":"Ids for aria-describedby (hint or error text)."},"disabled":{"type":"boolean","description":"Disables the field; the segment group dims and goes inert.","default":false},"hour_cycle":{"type":"string","description":"Pins the hour cycle (h12/h23/h11/h24) instead of the locale's."},"invalid":{"type":"boolean","description":"Paints the destructive border/ring and sets aria-invalid.","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Standalone accessible name; inside a form the Field label wires ids instead. Segments announce it themselves."},"locale":{"type":"string","description":"Pins the field to a locale other than the page's."},"max":{"type":"string","description":"The latest allowed date (Date or ISO string) - rides native constraint validation."},"min":{"type":"string","description":"The earliest allowed date (Date or ISO string) - rides native constraint validation."},"name":{"type":"string","description":"The form field name - required; the value posts as ISO yyyy-mm-dd with or without JS."},"placeholder_value":{"type":"string","description":"What the first arrow press on an empty segment lands on; defaults to today."},"readonly":{"type":"boolean","description":"The value shows but cannot be edited.","default":false},"required":{"type":"boolean","description":"Marks the native input required.","default":false},"seconds":{"type":"boolean","description":"Adds the seconds segment; the wire format becomes HH:MM:SS.","default":false},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Date, or an ISO yyyy-mm-dd string; nil renders empty."}},"required":["component"]},"Timeline":{"type":"object","description":"A sequence of dated events as an ordered list. Rules: A sequence of dated events (activity feed, order status, deploy history) is a Timeline - never a hand-rolled stack of dots and left borders (this is the <ol> the sequence owes its readers). Each with_item takes title:, optional time: (renders a <time>), optional icon: (swaps the dot for a glyph), completed: for progress - the description is the block. completed: colors the item's indicator and its rail segment - mark every step up to the current one, not just the latest. orientation: :horizontal lays the steps left-to-right (an order tracker); the vertical default reads as a feed. For steps the USER advances through, use Stepper - a Timeline records, it never navigates.","properties":{"component":{"const":"Timeline"},"orientation":{"type":"string","enum":["vertical","horizontal"],"default":"vertical","description":"The layout axis: :vertical reads as a feed, :horizontal as a step tracker."},"items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Declares one event: title:, optional time: (renders a <time>), optional icon: (replaces the dot), completed: for progress - the description is the block."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Toast":{"type":"object","description":"A brief, auto-dismissing notification message. Rules: Server-side toasts go through turbo_stream.poetry_toast / the flash recipe - never hand-append into #poetry-toaster. Undo/consequence toasts MUST carry an action slot - action-bearing toasts default to persistent (duration nil); give an explicit duration only when missing the action is safe. variant: :destructive is for failures the user must hear about (it announces ASSERTIVELY); do not use it for styling. Never put required interactions in a toast (toasts are missable) - that is AlertDialog. Toasts are supplementary: never the only place an outcome is recorded.","properties":{"component":{"const":"Toast"},"variant":{"type":"string","enum":["default","success","info","warning","destructive","loading"],"default":"default","description":"The intent axis: it picks the icon, and :destructive announces assertively while :loading defaults to persistent."},"duration":{"type":"integer","description":"nil = derived: 5000ms, or PERSISTENT when an action slot is present (the missable-undo guard). <= 0 = persistent."},"politeness":{"type":"string","enum":["polite","assertive"],"default":"dynamic","description":"Derived from the variant: destructive announces assertively."},"show_close_button":{"type":"boolean","description":"The corner dismiss button - named as the dialog family names it.","default":true},"title":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The message (REQUIRED - the announced payload's first line)."},"description":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Supporting copy under the title."},"action":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. Typed Button slot (undo / view / retry): clicking it dismisses the toast with reason \"action\". Its presence makes the toast persistent by default."}},"required":["component"]},"ToastTrigger":{"type":"object","description":" Rules: template: names a <template> element id holding ONE rendered poetry_toast - the trigger stamps a clone into the toaster on press. toaster: scopes the stamp to one region id on multi-toaster pages; omit it for the page's toaster. Give the templated toast a duration (auto-dismiss) - repeated presses stack persistent toasts. Server round-trips keep using turbo_stream.poetry_toast - this trigger is for purely client-side moments (copied, undone, queued).","properties":{"component":{"const":"ToastTrigger"},"size":{"type":"string","description":"The Button size the trigger renders at.","default":"default"},"template":{"type":"string","description":"The id of the <template> element holding the rendered toast to stamp."},"toaster":{"type":"string","description":"Scopes the stamp to one toaster region id on multi-toaster pages; omit for the page's toaster."},"variant":{"type":"string","description":"The Button variant the trigger renders at.","default":"outline"}},"required":["component"]},"Toaster":{"type":"object","description":"The region that stacks and manages toast notifications. Rules: Exactly ONE poetry_toaster per layout; it is data-turbo-permanent. Server-side toasts go through turbo_stream.poetry_toast / the flash recipe - never hand-append into #poetry-toaster. Client-side (no round-trip) toasts go through poetry_toast_trigger(template:) + a <template> holding the rendered poetry_toast - the trigger stamps it into the region. Do not announce() toast content yourself - the toast controller already does; double-announcing is a regression.","properties":{"component":{"const":"Toaster"},"position":{"type":"string","enum":["top-left","top-center","top-right","bottom-left","bottom-center","bottom-right"],"default":"bottom-right","description":"The stack's corner - set here, not per toast; each toast's slide direction follows it."},"hotkey":{"type":"string","description":"The keyboard shortcut that focuses the most recent toast.","default":"F8"},"limit":{"type":"integer","description":"The maximum visible toasts; overflow queues hidden with timers held.","default":3}},"required":["component"]},"Toggle":{"type":"object","description":"A two-state button that can be pressed on or off. Rules: Use poetry_toggle - never a Button with hand-managed aria-pressed. Toggle is UI state, NOT form data: never try to submit it. Form value -> Checkbox; instant setting -> Switch; exclusive/grouped -> ToggleGroup. Icon-only toggles MUST pass label:, and the label must NOT change with state ('Bookmark', never 'Remove bookmark'). aria-pressed is the vocabulary - never aria-checked or aria-expanded on a Toggle. Wire the EFFECT to poetry:toggle:change (or click) and revert via set(false) on failure - a pressed toggle whose effect failed is a lie. Pressed visual is accent - don't override data-pressed colors per-instance (theme-level only).","properties":{"component":{"const":"Toggle"},"size":{"type":"string","enum":["default","sm","lg"],"default":"default","description":"The control's size axis."},"variant":{"type":"string","enum":["default","outline"],"default":"default","description":"The visual treatment; :outline adds a border for standalone use."},"disabled":{"type":"boolean","description":"Disables the control and forwards to the native button.","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"REQUIRED when icon-only; must be state-INVARIANT (APG: aria-pressed carries the state - a flipping name makes SRs announce nonsense)."},"pressed":{"type":"boolean","description":"The server-rendered pressed state.","default":false}},"required":["component"]},"ToggleGroup":{"type":"object","description":"A set of toggle buttons for single or multiple selection. Rules: Use poetry_toggle_group - never hand-assemble Toggles with your own exclusivity logic. ToggleGroup is UI state, NOT form data: submitting single-select -> RadioGroup; submitting multi-select -> Checkbox group. No name: exists; don't route around it. Every item MUST have a unique value: (ArgumentError on duplicates); icon-only items MUST pass label: (state-invariant). Name the group via label: - a nameless radiogroup/toolbar fails the audit. Exclusive view/mode switching that shows PANELS is Tabs (aria-selected + tabpanels), not a single ToggleGroup. Never mix vocabularies: single items carry aria-checked, multiple carry aria-pressed - the controller enforces it; agents patching DOM must too. single deselects to empty by re-press - if your UI needs always-one-selected, handle the empty change in the host.","properties":{"component":{"const":"ToggleGroup"},"size":{"type":"string","enum":["default","sm","lg"],"default":"default","description":"The shared Toggle size axis, cascaded from the root to every item (root wins)."},"variant":{"type":"string","enum":["default","outline"],"default":"default","description":"The shared Toggle variant axis, cascaded from the root to every item (root wins)."},"disabled":{"type":"boolean","description":"Disables every item in the group.","default":false},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The group's accessible name (aria-label) - a nameless radiogroup/toolbar logs a lint warning."},"orientation":{"type":"string","enum":["horizontal","vertical"],"default":"horizontal","description":"The roving axis; :vertical stacks the items and flips the arrow keys."},"spacing":{"type":"integer","description":"0 = the classic segmented control (joined corners, collapsed outline borders); >0 = free-standing items separated by that gap step.","default":2},"type":{"type":"string","description":":single keeps at most one item pressed; :multiple toggles items independently.","default":"single"},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"single: the pressed item's value. ArgumentError with :multiple."},"values":{"type":"array","items":{"type":"string"},"description":"multiple: the pressed items' values. ArgumentError with :single.","default":"dynamic"},"items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Declares one item: value: (unique - duplicates raise), label: (required when icon-only), disabled:; the block is the content. Pressed state comes from value:/values:."}},"required":["component"]},"Toolbar":{"type":"object","description":"A horizontal group of controls that acts as one keyboard tab stop - Tab passes over the group, Arrow keys move between its controls. Rules: A Toolbar is ONE Tab stop: arrows move between its controls - use it for grouped actions over a surface (table bulk actions, editor strips), never as page navigation. Compose through the typed slots: with_button (a real poetry Button - tag: :a makes it a link), with_input (search/filter - the caret keeps its arrow keys), with_separator (orientation flips automatically). label: is the toolbar's accessible name and is required - screen readers announce it on entry. A ToggleGroup composed inside keeps its own arrow navigation (its items rove locally); place it between separators so the seam reads as a group.","properties":{"component":{"const":"Toolbar"},"orientation":{"type":"string","enum":["horizontal","vertical"],"default":"horizontal","description":"The strip's axis; :vertical stacks the controls and flips the arrow keys."},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The toolbar's accessible name. Required."},"loop":{"type":"boolean","description":"Whether arrow navigation wraps at the ends.","default":true},"items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"The control slots: with_button (a real Button - tag: :a makes it a link), with_input (search/filter), with_separator (its orientation flips automatically)."}},"required":["component"]},"Tooltip":{"type":"object","description":"A floating label describing an element on hover or focus. Rules: with_trigger(compose: true) { |wiring| ... } composes YOUR control as the trigger: the block is yielded the trigger wiring (the Stimulus behavior the overlay needs; poppers add id/aria and their trigger slot, modals hand only the open action) - splat it onto a wiring-free control (poetry_sidebar_menu_button, a plain tag); without compose: the classic composed Button renders. Use poetry_tooltip - never hand-roll title-attribute replacements or hover divs. Tooltip content is TEXT and never interactive/focusable - links, buttons, or inputs inside are a contract violation (use Popover). Never put essential information only in a tooltip - touch users NEVER see it (no long-press path, by design). The tooltip DESCRIBES; it never names. Icon-only triggers still require label: on the composed Button. Wrap toolbar/button rows in ONE poetry_tooltip_provider so the warm grace makes the row feel continuous. Rich visual content needs label: (the plain-text announcement). Do not pin tooltips open as onboarding callouts - that is a Popover.","properties":{"component":{"const":"Tooltip"},"align":{"type":"string","enum":["start","center","end"],"default":"center","description":"Panel alignment along the chosen side."},"align_offset":{"type":"integer","description":"Shift in pixels along the alignment axis.","default":0},"avoid_collisions":{"type":"boolean","description":"Flips and shifts the panel to stay inside the viewport.","default":true},"delay_duration":{"type":"integer","description":"The hover-open delay in ms; nil inherits the provider's (default 0)."},"disable_hoverable_content":{"type":"boolean","description":"When true the bubble closes as the pointer leaves the trigger - it cannot be hovered into; nil inherits the provider."},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"Plain-text announcement override for rich content (the visual children stay; the announced body becomes this text)."},"open":{"type":"boolean","description":"Server-renders the tooltip open.","default":false},"side":{"type":"string","enum":["top","right","bottom","left"],"default":"top","description":"Which side of the anchor the panel opens on."},"side_offset":{"type":"integer","description":"Gap in pixels between the anchor and the panel.","default":0},"trigger":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ComponentId","description":"The id of the component rendered here. The described control - commonly a poetry Button (with_trigger(variant: :outline) { \"Hover\" }). The slot owns the state + timing wiring regardless of the composed content. NO aria-haspopup/expanded/controls - the tooltip is invisible as a popup; aria-describedby is written by the controller on open (and server-rendered only when open: true)."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"]},"Tree":{"type":"object","description":"A hierarchical list of expandable, selectable nodes. Rules: Hierarchical expandable lists are a Tree - never hand-rolled nested <ul>s with click handlers; the treegrid semantics, expansion keys, and focus rules ride the controller. label: is REQUIRED (the treegrid's accessible name). Items: tree.with_item(text:, value:, expanded:, disabled:, href:) with nesting via the block - the component flattens and computes aria-level/posinset/setsize. Expansion is client state; persist it by listening for poetry:tree:toggle and re-rendering with expanded: from your store. Navigation destinations take href: (the label renders as a link); a Tree is not a menu - actions belong to DropdownMenu, picking to Select/Combobox.","properties":{"component":{"const":"Tree"},"label":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The tree's accessible name. Required."}},"required":["component"]},"Typeset":{"type":"object","description":"Prose styling for long-form and rendered-markdown content. Rules: Wrap RENDERED markdown / prose HTML (headings, paragraphs, lists, tables) - never app chrome; poetry components style themselves. preset: \"docs\" appends typeset-docs - a preset is a tiny class in the app's own CSS setting --typeset-size/-leading/-flow (and font vars). Opt an embedded component OUT of the prose styling with class: \"not-typeset\" - it covers the whole subtree. Wrap a wide table (or any wide block) in a typeset-scroll div inside the prose to scroll horizontally instead of compressing.","properties":{"component":{"const":"Typeset"},"preset":{"type":"string","description":"Appends typeset-<preset> - a tiny class in the app's own CSS retuning the rhythm variables (e.g. \"docs\")."},"text":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The content block as text (Markdown is not interpreted)."},"children":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/ChildList","description":"Component ids rendered as the content block, in order."}},"required":["component"],"anyOf":[{"required":["text"]},{"required":["children"]}]}},"functions":{"required":{"type":"object","description":"Checks that the value is not null, undefined, or empty.","returnType":"validationResult","allowedCallers":"rendererOnly","allOf":[{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCommon"},{"type":"object","properties":{"call":{"const":"required"},"args":{"type":"object","properties":{"value":{"description":"The value to check."}},"unevaluatedProperties":false,"required":["value"]}},"required":["call","args"]}],"unevaluatedProperties":false},"regex":{"type":"object","description":"Checks that the value matches a regular expression string.","returnType":"validationResult","allowedCallers":"rendererOnly","allOf":[{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCommon"},{"type":"object","properties":{"call":{"const":"regex"},"args":{"type":"object","properties":{"value":{"description":"The value to check."},"pattern":{"type":"string","description":"The regular expression."}},"unevaluatedProperties":false,"required":["value","pattern"]}},"required":["call","args"]}],"unevaluatedProperties":false},"length":{"type":"object","description":"Checks string length constraints.","returnType":"validationResult","allowedCallers":"rendererOnly","allOf":[{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCommon"},{"type":"object","properties":{"call":{"const":"length"},"args":{"type":"object","properties":{"value":{"description":"The value to check."},"min":{"type":"number","description":"The minimum."},"max":{"type":"number","description":"The maximum."}},"unevaluatedProperties":false,"required":["value"]}},"required":["call","args"]}],"unevaluatedProperties":false},"numeric":{"type":"object","description":"Checks numeric range constraints.","returnType":"validationResult","allowedCallers":"rendererOnly","allOf":[{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCommon"},{"type":"object","properties":{"call":{"const":"numeric"},"args":{"type":"object","properties":{"value":{"description":"The value to check."},"min":{"type":"number","description":"The minimum."},"max":{"type":"number","description":"The maximum."}},"unevaluatedProperties":false,"required":["value"]}},"required":["call","args"]}],"unevaluatedProperties":false},"email":{"type":"object","description":"Checks that the value is a valid email address.","returnType":"validationResult","allowedCallers":"rendererOnly","allOf":[{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCommon"},{"type":"object","properties":{"call":{"const":"email"},"args":{"type":"object","properties":{"value":{"description":"The value to check."}},"unevaluatedProperties":false,"required":["value"]}},"required":["call","args"]}],"unevaluatedProperties":false},"formatString":{"type":"object","description":"Interpolates data model values and function results into a string: ${/path}, ${name(arg: value)}.","returnType":"string","allowedCallers":"rendererOnly","allOf":[{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCommon"},{"type":"object","properties":{"call":{"const":"formatString"},"args":{"type":"object","properties":{"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The string with ${} blocks."}},"unevaluatedProperties":false,"required":["value"]}},"required":["call","args"]}],"unevaluatedProperties":false},"formatNumber":{"type":"object","description":"Formats a number with grouping and decimal precision.","returnType":"string","allowedCallers":"rendererOnly","allOf":[{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCommon"},{"type":"object","properties":{"call":{"const":"formatNumber"},"args":{"type":"object","properties":{"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicNumber","description":"The number."},"decimals":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicNumber","description":"Fraction digits to show."},"grouping":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicBoolean","description":"Thousands separators (default true)."}},"unevaluatedProperties":false,"required":["value"]}},"required":["call","args"]}],"unevaluatedProperties":false},"formatCurrency":{"type":"object","description":"Formats a number as a currency string.","returnType":"string","allowedCallers":"rendererOnly","allOf":[{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCommon"},{"type":"object","properties":{"call":{"const":"formatCurrency"},"args":{"type":"object","properties":{"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicNumber","description":"The number."},"currency":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The ISO 4217 code."},"decimals":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicNumber","description":"Fraction digits to show."},"grouping":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicBoolean","description":"Thousands separators (default true)."}},"unevaluatedProperties":false,"required":["value","currency"]}},"required":["call","args"]}],"unevaluatedProperties":false},"formatDate":{"type":"object","description":"Formats a timestamp with a Unicode TR35 pattern (yyyy-MM-dd, MMM d, HH:mm).","returnType":"string","allowedCallers":"rendererOnly","allOf":[{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCommon"},{"type":"object","properties":{"call":{"const":"formatDate"},"args":{"type":"object","properties":{"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicValue","description":"An ISO 8601 string or epoch."},"format":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The TR35 pattern."}},"unevaluatedProperties":false,"required":["value","format"]}},"required":["call","args"]}],"unevaluatedProperties":false},"pluralize":{"type":"object","description":"Picks the string for a number's CLDR plural category.","returnType":"string","allowedCallers":"rendererOnly","allOf":[{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCommon"},{"type":"object","properties":{"call":{"const":"pluralize"},"args":{"type":"object","properties":{"zero":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The zero form."},"one":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The one form."},"two":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The two form."},"few":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The few form."},"many":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The many form."},"other":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString","description":"The other form."},"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicNumber","description":"The number."}},"unevaluatedProperties":false,"required":["value","other"]}},"required":["call","args"]}],"unevaluatedProperties":false},"openUrl":{"type":"object","description":"Opens an http(s) URL; the renderer links to it.","returnType":"void","allowedCallers":"rendererOnly","requiresUserActivation":true,"allOf":[{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCommon"},{"type":"object","properties":{"call":{"const":"openUrl"},"args":{"type":"object","properties":{"url":{"description":"The URL to open."}},"unevaluatedProperties":false,"required":["url"]}},"required":["call","args"]}],"unevaluatedProperties":false},"and":{"type":"object","description":"Logical AND of a list of values.","returnType":"boolean","allowedCallers":"rendererOnly","allOf":[{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCommon"},{"type":"object","properties":{"call":{"const":"and"},"args":{"type":"object","properties":{"values":{"type":"array","description":"The values to combine.","items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicBoolean"},"minItems":2}},"unevaluatedProperties":false,"required":["values"]}},"required":["call","args"]}],"unevaluatedProperties":false},"or":{"type":"object","description":"Logical OR of a list of values.","returnType":"boolean","allowedCallers":"rendererOnly","allOf":[{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCommon"},{"type":"object","properties":{"call":{"const":"or"},"args":{"type":"object","properties":{"values":{"type":"array","description":"The values to combine.","items":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicBoolean"},"minItems":2}},"unevaluatedProperties":false,"required":["values"]}},"required":["call","args"]}],"unevaluatedProperties":false},"not":{"type":"object","description":"Logical NOT of a value.","returnType":"boolean","allowedCallers":"rendererOnly","allOf":[{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCommon"},{"type":"object","properties":{"call":{"const":"not"},"args":{"type":"object","properties":{"value":{"$ref":"https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicBoolean","description":"The value."}},"unevaluatedProperties":false,"required":["value"]}},"required":["call","args"]}],"unevaluatedProperties":false}},"$defs":{"anyComponent":{"oneOf":[{"$ref":"#/components/Accordion"},{"$ref":"#/components/Alert"},{"$ref":"#/components/AlertDialog"},{"$ref":"#/components/AspectRatio"},{"$ref":"#/components/Attachment"},{"$ref":"#/components/Autocomplete"},{"$ref":"#/components/Avatar"},{"$ref":"#/components/Badge"},{"$ref":"#/components/Breadcrumb"},{"$ref":"#/components/Bubble"},{"$ref":"#/components/Button"},{"$ref":"#/components/ButtonGroup"},{"$ref":"#/components/Calendar"},{"$ref":"#/components/Card"},{"$ref":"#/components/Carousel"},{"$ref":"#/components/Checkbox"},{"$ref":"#/components/ClipboardText"},{"$ref":"#/components/CodeBlock"},{"$ref":"#/components/Collapsible"},{"$ref":"#/components/Combobox"},{"$ref":"#/components/Command"},{"$ref":"#/components/ContextMenu"},{"$ref":"#/components/DataTable"},{"$ref":"#/components/DateField"},{"$ref":"#/components/DatePicker"},{"$ref":"#/components/DateTimeField"},{"$ref":"#/components/Deferred"},{"$ref":"#/components/Dialog"},{"$ref":"#/components/Drawer"},{"$ref":"#/components/DropdownMenu"},{"$ref":"#/components/Empty"},{"$ref":"#/components/Field"},{"$ref":"#/components/FieldGroup"},{"$ref":"#/components/FieldSeparator"},{"$ref":"#/components/Fieldset"},{"$ref":"#/components/FileInput"},{"$ref":"#/components/HoverCard"},{"$ref":"#/components/Icon"},{"$ref":"#/components/Input"},{"$ref":"#/components/InputGroup"},{"$ref":"#/components/InputOtp"},{"$ref":"#/components/Item"},{"$ref":"#/components/Kbd"},{"$ref":"#/components/Label"},{"$ref":"#/components/Link"},{"$ref":"#/components/Marker"},{"$ref":"#/components/Menubar"},{"$ref":"#/components/Message"},{"$ref":"#/components/MessageScroller"},{"$ref":"#/components/MetadataList"},{"$ref":"#/components/Meter"},{"$ref":"#/components/NativeSelect"},{"$ref":"#/components/NavigationMenu"},{"$ref":"#/components/NumberField"},{"$ref":"#/components/Pagination"},{"$ref":"#/components/Popover"},{"$ref":"#/components/Progress"},{"$ref":"#/components/Questionnaire"},{"$ref":"#/components/RadioGroup"},{"$ref":"#/components/Resizable"},{"$ref":"#/components/ScrollArea"},{"$ref":"#/components/SearchField"},{"$ref":"#/components/Select"},{"$ref":"#/components/SensitiveInput"},{"$ref":"#/components/Separator"},{"$ref":"#/components/Sheet"},{"$ref":"#/components/Sidebar"},{"$ref":"#/components/Skeleton"},{"$ref":"#/components/Slider"},{"$ref":"#/components/Spinner"},{"$ref":"#/components/Stat"},{"$ref":"#/components/Switch"},{"$ref":"#/components/Table"},{"$ref":"#/components/Tabs"},{"$ref":"#/components/TagGroup"},{"$ref":"#/components/Textarea"},{"$ref":"#/components/TimeField"},{"$ref":"#/components/Timeline"},{"$ref":"#/components/Toast"},{"$ref":"#/components/ToastTrigger"},{"$ref":"#/components/Toaster"},{"$ref":"#/components/Toggle"},{"$ref":"#/components/ToggleGroup"},{"$ref":"#/components/Toolbar"},{"$ref":"#/components/Tooltip"},{"$ref":"#/components/Tree"},{"$ref":"#/components/Typeset"}],"discriminator":{"propertyName":"component"}},"anyFunction":{"oneOf":[{"$ref":"#/functions/required"},{"$ref":"#/functions/regex"},{"$ref":"#/functions/length"},{"$ref":"#/functions/numeric"},{"$ref":"#/functions/email"},{"$ref":"#/functions/formatString"},{"$ref":"#/functions/formatNumber"},{"$ref":"#/functions/formatCurrency"},{"$ref":"#/functions/formatDate"},{"$ref":"#/functions/pluralize"},{"$ref":"#/functions/openUrl"},{"$ref":"#/functions/and"},{"$ref":"#/functions/or"},{"$ref":"#/functions/not"}]}}}