Sidebar

Displays the mobile sidebar.

Command palette

Search for a command to run…

Docs
Installation
Theming
Typography
Testing
I18n
Stimulus
Form Builder
Pagination
Deferred Regions
Optimistic Forms
Recipes
Page Agent
Agent Skills
Editors
API Reference
Data Table
Caching
Stable IDs
Rails Engines
Accessibility
MCP Server
WebMCP
AG-UI Relay
A2UI Surfaces
Libraries
Core
UI
Charts
Agent
Simple Form
Extract
Icons
Lucide
Components
Accordion
Alert
Alert Dialog
Aspect Ratio
Attachment
Autocomplete
Avatar
Badge
Breadcrumb
Bubble
Button
Button Group
Calendar
Card
Carousel
Checkbox
Clipboard Text
Code Block
Collapsible
Combobox
Command
Command Dialog
Context Menu
Data Table
Date Field
Date Picker
Date Time Field
Deferred
Dialog
Drawer
Dropdown Menu
Empty
Field
Field Group
Field Separator
Fieldset
File Input
Hover Card
Icon
Input
Input Group
Input Otp
Item
Kbd
Label
Link
Marker
Menubar
Message
Message Scroller
Metadata List
Meter
Native Select
Navigation Menu
Number Field
Pagination
Popover
Progress
Questionnaire
Radio Group
Resizable
Scroll Area
Search Field
Select
Sensitive Input
Separator
Sheet
Sidebar
Skeleton
Slider
Spinner
Stat
Switch
Table
Tabs
Tag Group
Textarea
Time Field
Timeline
Toast
Toast Trigger
Toaster
Toggle
Toggle Group
Toolbar
Tooltip
Tree
Typeset
Charts
Adapter Chart
Area Chart
Bar Chart
Composed Chart
Line Chart
Pie Chart
Radar Chart
Radial Bar Chart
Scatter Chart
Demos
Chat Replay
AG-UI Relay
A2UI Surface
Interactive Filter
Live Streaming
Synced Tooltips
Brush & Zoom
Reference
Installation — Install
Installation — Upgrade an existing app
Installation — The three ownership tiers
Installation — Copy-ins: poetry:diff
Installation — Scaffold templates
Theming — The nine themes
Theming — Installing a theme
Theming — Owning the design outright
Theming — How this site renders all nine
Theming — The font pairing
Theming — Overriding the theme
Theming — DESIGN.md interop
Theming — What stays out of the theme
Theming — Consumer utilities
Theming — Color scheme (dark mode)
Theming — Portals and scoped themes
Typography — Default
Typography — Headings
Typography — Inline code
Typography — Large
Typography — Lead
Typography — Muted
Typography — Small
Testing — The doctrine
Testing — Lint first: Poetry check
Testing — Tier 1: wiring tests
Testing — Tier 2: behavior
Testing — Tier 3: the browser pass
Testing — The system-test helpers
Testing — What to assert, in one line
I18n — The shipped catalogue
I18n — Overriding keys and adding locales
I18n — Visible copy is yours
I18n — Forms localize through the model
I18n — Dates
Stimulus — The model
Stimulus — 1. Attach your controller to a component
Stimulus — 2. React to what a component does
Stimulus — 3. Drive a component from your markup
Stimulus — 4. Declare it in Ruby
Stimulus — 5. Change what a Poetry controller does
Stimulus — Checked at every rung
Form Builder — Coming from Simple Form?
Form Builder — Default
Form Builder — Errors
Form Builder — Inference
Form Builder — Layout
Pagination — Installation
Pagination — Wire your controller
Pagination — kaminari
Pagination — pagy
Pagination — will_paginate
Pagination — The window, labels, and one page
Pagination — Kaminari
Pagination — Pagy
Pagination — Will paginate
Deferred Regions — Error retry
Deferred Regions — In tabs
Deferred Regions — Lazy
Optimistic Forms — How it works
Optimistic Forms — Setup
Optimistic Forms — Usage
Optimistic Forms — The server contract
Optimistic Forms — The submitted value
Optimistic Forms — When the prediction can be wrong
Optimistic Forms — Authoring streams directly
Optimistic Forms — When to reach for it
Optimistic Forms — Default
Optimistic Forms — Reconciliation
Recipes — How it works
Recipes — In-page agent embed
Recipes — poetry scaffold templates
Recipes — Data index screen
Recipes — Settings screen
Recipes — poetry skill bundle
Recipes — poetry-component skill bundle
Recipes — poetry-design skill bundle
Page Agent — What this is
Page Agent — Or hand the agent tools
Page Agent — Activate
Page Agent — Task list
Page Agent — Sample implementation
Page Agent — Provenance & limits
Agent Skills — How it works
Agent Skills — poetry
Agent Skills — In a Rails app with Poetry
Editors — One generator
Editors — The MCP server
Editors — The check hook
Editors — Snippets
Editors — HTML+ERB tooling
Editors — Design tools
Data Table — The controller
Data Table — The view
Data Table — Sticky headers
Data Table — Row selection
Data Table — Scoped updates (optional)
Data Table — Filter as you type (optional)
Data Table — Accessibility
Data Table — Row mutations belong to another tier
Caching — The digest blind spot
Caching — The recipe: version-keyed cache keys
Caching — Rules for markup that can be cached at all
Caching — Components you copied in
Caching — Ids inside cached fragments
Caching — HTTP caching, ETags, and CSRF
Stable IDs — The identity ladder
Stable IDs — When you must pass identity
Stable IDs — Keeping primary keys out of the DOM
Stable IDs — What morph identity does and does not preserve
Stable IDs — The sequence mode (opt-in, experimental)
Stable IDs — ETags and CSRF: what ids do and don't buy
Rails Engines — Why this works
Rails Engines — The engine recipe
Rails Engines — Page-level utilities: the one wiring step
Rails Engines — Retheming the whole application
Rails Engines — Customizing a component from an engine
Rails Engines — Keep one theme contract
Accessibility — What Poetry guarantees
Accessibility — What automation cannot see
Accessibility — Set up a pass
Accessibility — The universal checklist
Accessibility — Pattern checklists
Accessibility — Form controls
Accessibility — Overlays
Accessibility — Menus and command palettes
Accessibility — Disclosure and navigation
Accessibility — Data, feedback, and streaming
Accessibility — Automated checks in your app
Accessibility — Recording findings
MCP Server — The server
MCP Server — The loop it closes
MCP Server — The ten tools
MCP Server — Over HTTP
MCP Server — Routing a brief
MCP Server — The check verdict
MCP Server — Skills and guidance at runtime
MCP Server — One source, every surface
WebMCP — What it is
WebMCP — Live: the tools on this page
WebMCP — Search the docs: a declarative tool
WebMCP — Two paths
WebMCP — Designing tools for your app
WebMCP — Declaring tools in your own components
WebMCP — Setup
WebMCP — Safety by construction
AG-UI Relay — What it is
AG-UI Relay — Run an agent
AG-UI Relay — Render the transcript
AG-UI Relay — Frontend tools
AG-UI Relay — Interrupts
AG-UI Relay — A2UI surfaces in the stream
AG-UI Relay — Deterministic development
AG-UI Relay — Setup
A2UI Surfaces — Two catalogs
A2UI Surfaces — Fold the envelope
A2UI Surfaces — Render a surface
A2UI Surfaces — Stream updates
A2UI Surfaces — Actions
A2UI Surfaces — Checks
A2UI Surfaces — Functions
A2UI Surfaces — State across updates
A2UI Surfaces — Transports
A2UI Surfaces — Setup
Accordion — Installation
Accordion — Default
Accordion — Borders
Accordion — Card
Accordion — Disabled
Accordion — Multiple
Accordion — Non collapsible
Accordion — Wiring
Alert — Installation
Alert — Default
Alert — Action
Alert — Custom colors
Alert — Destructive
Alert — Dismissible
Alert — With action
Alert — With icon
Alert Dialog — Installation
Alert Dialog — Default
Alert Dialog — Small
Alert Dialog — Small with media
Alert Dialog — With media
Alert Dialog — Wiring
Aspect Ratio — Installation
Aspect Ratio — Default
Aspect Ratio — Portrait
Aspect Ratio — Square
Aspect Ratio — With image
Attachment — Installation
Attachment — Default
Attachment — Group
Attachment — Idle dropzone
Attachment — Sizes
Attachment — States
Attachment — Vertical with actions
Attachment — With trigger
Autocomplete — Installation
Autocomplete — Default
Autocomplete — Value override
Autocomplete — Wiring
Avatar — Installation
Avatar — Default
Avatar — Badge
Avatar — Badge with icon
Avatar — Dropdown
Avatar — Group
Avatar — Group with icon
Avatar — Sizes
Avatar — With image
Badge — Installation
Badge — Default
Badge — Custom colors
Badge — Link
Badge — Status
Badge — Variants
Badge — With icon
Badge — With spinner
Breadcrumb — Installation
Breadcrumb — Default
Breadcrumb — Collapsed
Breadcrumb — Custom separator
Breadcrumb — Dropdown
Breadcrumb — Link component
Bubble — Installation
Bubble — Default
Bubble — Collapsible
Bubble — Group
Bubble — Links and buttons
Bubble — Popover
Bubble — Quick replies
Bubble — Reactions
Bubble — Tooltip
Bubble — Variants
Button — Installation
Button — Default
Button — As link
Button — Button group
Button — Icon buttons
Button — Link
Button — Loading
Button — Rounded
Button — Sizes
Button — With icon
Button Group — Installation
Button Group — Default
Button Group — Input action
Button Group — Input group
Button Group — Nested
Button Group — Popover
Button Group — Separator
Button Group — Sizes
Button Group — Split
Button Group — Text prefix
Button Group — Vertical
Button Group — With dropdown
Button Group — With select
Calendar — Installation
Calendar — Default
Calendar — Bounded
Calendar — Custom cell size
Calendar — Date and time
Calendar — Month and year selector
Calendar — Range
Calendar — Week numbers
Calendar — Wiring
Card — Installation
Card — Default
Card — Login form
Card — Terms of service
Card — With action and footer
Card — With media
Carousel — Installation
Carousel — Default
Carousel — Sizes
Carousel — Spacing
Carousel — Vertical
Carousel — Wiring
Checkbox — Installation
Checkbox — Default
Checkbox — Card
Checkbox — Disabled
Checkbox — Group
Checkbox — In a field
Checkbox — In a table
Checkbox — States
Checkbox — With description
Checkbox — Wiring
Clipboard Text — Installation
Clipboard Text — Default
Clipboard Text — Truncated copy
Clipboard Text — Wiring
Code Block — Installation
Code Block — Default
Code Block — Numbered and highlighted
Code Block — Wiring
Collapsible — Installation
Collapsible — Default
Collapsible — File tree
Collapsible — Initially open
Collapsible — Settings panel
Collapsible — With icon
Collapsible — Wiring
Combobox — Installation
Combobox — Default
Combobox — Auto highlight
Combobox — Clear button
Combobox — Custom items
Combobox — Disabled
Combobox — Disabled options
Combobox — Grouped
Combobox — Invalid
Combobox — Multiple
Combobox — Valued
Combobox — Wide
Combobox — With icon
Combobox — Agent tools
Combobox — Wiring
Command — Installation
Command — Default
Command — Close button
Command — Dialog
Command — Initial value
Command — Scrollable
Command — With icons
Command — Zero match
Command — Wiring
Command Dialog — Installation
Command Dialog — Default
Command Dialog — Empty state
Command Dialog — Hotkey
Command Dialog — Scrollable
Command Dialog — With icons
Command Dialog — Wiring
Context Menu — Installation
Context Menu — Default
Context Menu — Checkboxes
Context Menu — Destructive
Context Menu — Disabled surface
Context Menu — Focusable surface
Context Menu — Groups
Context Menu — Radio
Context Menu — Sides
Context Menu — Submenu
Context Menu — With icon
Context Menu — Wiring
Data Table — Installation
Data Table — Default
Data Table — Cell formatting
Data Table — Empty
Data Table — Filtered and paginated
Data Table — Row actions
Data Table — Selectable
Data Table — Wiring
Date Field — Installation
Date Field — Default
Date Field — With range
Date Field — Year first
Date Field — Wiring
Date Picker — Installation
Date Picker — Default
Date Picker — Date of birth
Date Picker — Input
Date Picker — Preselected
Date Picker — Range
Date Picker — With time
Date Picker — Wiring
Date Time Field — Installation
Date Time Field — Default
Date Time Field — With seconds
Date Time Field — Wiring
Deferred — Installation
Deferred — Default
Deferred — Wiring
Dialog — Installation
Dialog — Default
Dialog — Confirmation
Dialog — No close button
Dialog — Scrollable content
Dialog — Sticky footer
Dialog — Agent tools
Dialog — Wiring
Drawer — Installation
Drawer — Default
Drawer — Edit form
Drawer — Nested
Drawer — Non modal
Drawer — Position
Drawer — Responsive
Drawer — Snap points
Drawer — Swipe handle
Drawer — Agent tools
Drawer — Wiring
Dropdown Menu — Installation
Dropdown Menu — Default
Dropdown Menu — Avatar
Dropdown Menu — Checkbox with icons
Dropdown Menu — Checkboxes
Dropdown Menu — Complex
Dropdown Menu — Inset and disabled
Dropdown Menu — Radio group
Dropdown Menu — Radio with icons
Dropdown Menu — With icons
Dropdown Menu — Wiring
Empty — Installation
Empty — Default
Empty — Background
Empty — Outline
Empty — With avatar
Empty — With avatar group
Empty — With icon and actions
Empty — With input
Field — Installation
Field — Default
Field — Checkbox
Field — Choice card
Field — Fieldset
Field — Group
Field — Horizontal switch
Field — Responsive
Field — With error
Field — With hint
Field — With radio group
Field — With select
Field — With slider
Field — With textarea
Field Group — Installation
Field Group — Default
Field Group — Choices
Field Separator — Installation
Field Separator — Default
Field Separator — With caption
Fieldset — Installation
Fieldset — Default
Fieldset — Label legend
File Input — Installation
File Input — Default
File Input — Dropzone
File Input — Wiring
Hover Card — Installation
Hover Card — Default
Hover Card — Custom delays
Hover Card — Sides
Hover Card — Wiring
Icon — Installation
Icon — Default
Icon — Animated
Icon — Colors
Icon — Labeled
Icon — Sizes
Icon — Stroke width
Icon — With tooltip
Input — Installation
Input — Default
Input — Button group
Input — Disabled
Input — Field
Input — File
Input — Form
Input — Grid
Input — Inline
Input — Invalid
Input — Required
Input — With badge
Input — With value
Input Group — Installation
Input Group — Default
Input Group — Align
Input Group — Button
Input Group — Custom
Input Group — Dropdown
Input Group — Icon
Input Group — Kbd
Input Group — Spinner
Input Group — Text
Input Group — Textarea block
Input Group — Textarea header
Input Otp — Installation
Input Otp — Default
Input Otp — Alphanumeric
Input Otp — Form
Input Otp — Four digits
Input Otp — Groupings
Input Otp — In a field
Input Otp — States
Input Otp — Wiring
Item — Installation
Item — Default
Item — Avatar
Item — Dropdown
Item — Group
Item — Image
Item — Link
Item — Outline with icon and actions
Item — Sizes
Item — Variants
Item — With header and footer
Kbd — Installation
Kbd — Default
Kbd — Chord
Kbd — Command
Kbd — Group
Kbd — In button
Kbd — In tooltip
Kbd — Search hint
Label — Installation
Label — Default
Label — Disabled
Label — Form
Label — With checkbox
Label — With description
Link — Installation
Link — Default
Link — Current
Link — External
Link — Underline
Marker — Installation
Marker — Default
Marker — Border
Marker — Links and buttons
Marker — Separator
Marker — Shimmer
Marker — Status
Marker — Variants
Marker — With icon
Menubar — Installation
Menubar — Default
Menubar — Checkbox
Menubar — Disabled menu
Menubar — Icons
Menubar — Radio
Menubar — Submenu
Menubar — Wiring
Message — Installation
Message — Default
Message — Actions
Message — Align end
Message — Attachment
Message — Conversation
Message — Grouped
Message — Header and footer
Message — Markdown
Message Scroller — Installation
Message Scroller — Default
Message Scroller — Anchored turns
Message Scroller — Commands
Message Scroller — Group chat
Message Scroller — Open at start
Message Scroller — Scroll state
Message Scroller — Streaming
Message Scroller — Without jump button
Message Scroller — Wiring
Metadata List — Installation
Metadata List — Default
Metadata List — Two columns
Meter — Installation
Meter — Default
Meter — With value text
Native Select — Installation
Native Select — Default
Native Select — Grouped
Native Select — Invalid
Native Select — Small disabled
Navigation Menu — Installation
Navigation Menu — Default
Navigation Menu — Viewport
Navigation Menu — With descriptions
Navigation Menu — With icon
Navigation Menu — Wiring
Number Field — Installation
Number Field — Default
Number Field — Currency
Number Field — Decimal steps
Number Field — Wiring
Pagination — Installation
Pagination — Adapters
Pagination — Default
Pagination — First page
Pagination — Icons only
Pagination — Long range
Pagination — Short range
Pagination — Simple
Popover — Installation
Popover — Default
Popover — Align
Popover — Basic
Popover — Modal
Popover — Open
Popover — Side top
Popover — With anchor
Popover — With form
Popover — Wiring
Progress — Installation
Progress — Default
Progress — Complete
Progress — Controlled
Progress — Custom max without value
Progress — Label
Questionnaire — Installation
Questionnaire — Default
Questionnaire — Animated
Questionnaire — Card
Questionnaire — Custom progress
Questionnaire — Dialog
Questionnaire — Shortcuts
Questionnaire — Skip
Questionnaire — Validation
Questionnaire — Wiring
Radio Group — Installation
Radio Group — Default
Radio Group — Cards
Radio Group — Disabled
Radio Group — Fieldset
Radio Group — Horizontal layout
Radio Group — In a field
Radio Group — Invalid
Radio Group — With description
Radio Group — Wiring
Resizable — Installation
Resizable — Default
Resizable — Handle
Resizable — Nested
Resizable — Three panels
Resizable — Vertical
Resizable — Wiring
Scroll Area — Installation
Scroll Area — Default
Scroll Area — Horizontal
Search Field — Installation
Search Field — Default
Search Field — With value
Search Field — Wiring
Select — Installation
Select — Default
Select — Align item with trigger
Select — Disabled
Select — Groups
Select — Invalid
Select — Scrollable
Select — Small
Select — Valued
Select — Wiring
Sensitive Input — Installation
Sensitive Input — Default
Sensitive Input — With copy
Sensitive Input — With copy and tooltip
Sensitive Input — Wiring
Separator — Installation
Separator — Default
Separator — List
Separator — Menu
Separator — Vertical semantic
Sheet — Installation
Sheet — Default
Sheet — Form
Sheet — No close button
Sheet — Sides
Sheet — Agent tools
Sheet — Wiring
Sidebar — Installation
Sidebar — Default
Sidebar — Loading
Sidebar — Offcanvas collapsed
Sidebar — Submenu
Sidebar — User menu
Sidebar — Workspace switcher
Sidebar — Wiring
Skeleton — Installation
Skeleton — Default
Skeleton — Avatar
Skeleton — Block
Skeleton — Card
Skeleton — Composed
Skeleton — Form
Skeleton — Table
Skeleton — Text
Slider — Installation
Slider — Default
Slider — Controlled
Slider — Disabled
Slider — Multiple thumbs
Slider — Range
Slider — Range with gap
Slider — Vertical
Slider — Wiring
Spinner — Installation
Spinner — Default
Spinner — Button
Spinner — Custom
Spinner — Empty state
Spinner — In badge
Spinner — In input group
Spinner — Size
Spinner — With context label
Stat — Installation
Stat — Default
Stat — With description
Switch — Installation
Switch — Default
Switch — Card
Switch — Description
Switch — In a field
Switch — Invalid
Switch — Sizes
Switch — States
Switch — Wiring
Table — Installation
Table — Default
Table — Actions
Table — Footer
Table — Minimal
Table — Sticky
Tabs — Installation
Tabs — Default
Tabs — Disabled
Tabs — Line
Tabs — Vertical
Tabs — With icon
Tabs — Agent tools
Tabs — Wiring
Tag Group — Installation
Tag Group — Default
Tag Group — As form value
Tag Group — Wiring
Textarea — Installation
Textarea — Default
Textarea — Auto grown
Textarea — Button
Textarea — Disabled
Textarea — Field
Textarea — Invalid
Textarea — With hint
Textarea — With label
Time Field — Installation
Time Field — Default
Time Field — Twenty four hour
Time Field — Wiring
Timeline — Installation
Timeline — Default
Timeline — Horizontal
Timeline — With icons
Toast — Installation
Toast — Default
Toast — Promise
Toast — Variants
Toast — With action
Toast — Wiring
Toast Trigger — Installation
Toast Trigger — Default
Toast Trigger — Wiring
Toaster — Installation
Toaster — Default
Toaster — Positions
Toaster — Wiring
Toggle — Installation
Toggle — Default
Toggle — Outline
Toggle — Sizes
Toggle — States
Toggle — With text
Toggle — Wiring
Toggle Group — Installation
Toggle Group — Default
Toggle Group — Custom
Toggle Group — Disabled
Toggle Group — Outline
Toggle Group — Single
Toggle Group — Sizes
Toggle Group — Spaced
Toggle Group — Vertical
Toggle Group — Wiring
Toolbar — Installation
Toolbar — Default
Tooltip — Installation
Tooltip — Default
Tooltip — Disabled trigger
Tooltip — Keyboard
Tooltip — Open
Tooltip — Provider row
Tooltip — Side bottom
Tooltip — Sides
Tooltip — With label
Tooltip — Wiring
Tree — Installation
Tree — Default
Tree — With links
Tree — Wiring
Typeset — Installation
Typeset — Default
Typeset — Chat preset
Typeset — Docs preset
Typeset — Not typeset
Typeset — Overrides
Typeset — Responsive table
Adapter Chart — Installation
Adapter Chart — Bring your own engine
Adapter Chart — Default
Adapter Chart — Wiring
Area Chart — Installation
Area Chart — Default
Area Chart — Legend toggle
Area Chart — Wiring
Bar Chart — Installation
Bar Chart — Default
Bar Chart — Active
Bar Chart — Horizontal
Bar Chart — Label
Bar Chart — Mixed
Bar Chart — Multiple
Bar Chart — Negative
Bar Chart — Stacked
Bar Chart — Wiring
Composed Chart — Installation
Composed Chart — Default
Composed Chart — Full mix
Composed Chart — Wiring
Line Chart — Installation
Line Chart — Default
Line Chart — Dots
Line Chart — Dots colors
Line Chart — Label
Line Chart — Linear
Line Chart — Multiple
Line Chart — References and errors
Line Chart — Step
Line Chart — Wiring
Pie Chart — Installation
Pie Chart — Default
Pie Chart — Donut
Pie Chart — Donut active
Pie Chart — Donut text
Pie Chart — Label list
Pie Chart — Legend
Pie Chart — Stacked
Pie Chart — Wiring
Radar Chart — Installation
Radar Chart — Default
Radar Chart — Dots
Radar Chart — Grid circle
Radar Chart — Grid fill
Radar Chart — Grid none
Radar Chart — Legend
Radar Chart — Lines only
Radar Chart — Multiple
Radar Chart — Wiring
Radial Bar Chart — Installation
Radial Bar Chart — Default
Radial Bar Chart — Grid
Radial Bar Chart — Label
Radial Bar Chart — Shape
Radial Bar Chart — Stacked
Radial Bar Chart — Text
Radial Bar Chart — Wiring
Scatter Chart — Installation
Scatter Chart — Default
Scatter Chart — Bubbles
Scatter Chart — Multiple
Scatter Chart — Reference zones
Scatter Chart — Wiring
Action bar — Installation
Action bar — Default
Action bar — Styling
App shell — Installation
App shell — Default
App shell — Styling
Data index — Installation
Data index — Default
Data index — Styling
Destructive panel — Installation
Destructive panel — Default
Destructive panel — Revoke deploy token
Destructive panel — Styling
Page header — Installation
Page header — Default
Page header — Styling
Section card — Installation
Section card — Default
Section card — Styling
Stepper — Installation
Stepper — Default
Stepper — Styling
Top nav — Installation
Top nav — Default
Top nav — Styling
Chat Replay — Default
AG-UI Relay — Default
A2UI Surface — Default
Interactive Filter — Default
Live Streaming — Default
Synced Tooltips — Default
Brush & Zoom — Default
poetry-core — ActiveModel
poetry-core — ActiveModel::Type
poetry-core — ActiveModel::Type::List
poetry-core — ActiveModel::Type::Symbol
poetry-core — Poetry
@poetry/controllers — IncompleteDate
poetry-ui — Poetry
poetry-charts — Poetry
poetry-agent — Poetry
poetry-simple_form — Poetry
poetry-extract — Poetry
Core — What it is
Core — Install
Core — Build your own component library
Core — The component class
Core — The style dictionary
Core — Behavior from the primitives
Core — The registry
Core — Two styling paths: Tailwind or BEM
Core — Design tokens
Core — The check
Core — Box and Wrapper
Core — Box, the polymorphic element
Core — Wrapper, conditional outer HTML
UI — What it is
UI — Install and first components
UI — The form builder handoff
UI — Nine themes ship in the gem
UI — Own the code when you want to
UI — Blocks and recipes
UI — The agent surface
UI — Testing helpers
Charts — What it is
Charts — Install
Charts — Basic usage
Charts — Works wherever HTML renders
Charts — Live updates
Charts — Bring your own engine
Charts — The theme palette
Agent — What it is
Agent — Install
Agent — The MCP server
Agent — The WebMCP runtime
Agent — The AG-UI relay
Agent — The A2UI catalog and renderer
Agent — What's in the gem
Simple Form — How it works
Simple Form — Install
Simple Form — What restyles
Simple Form — The end state
Extract — Install and run
Extract — The pipeline
Extract — What it emits
Extract — From extract to theme
Lucide — Install
Lucide — Names, misses, and the fallback
Lucide — The pin

poetry-ui API

The component suite's Ruby surface beyond the gallery: the poetry_* view helpers, the model-bound FormBuilder, the Testing helpers, and the chat replay DSL.

Generated from the gem's source documentation. Component classes are documented on their own gallery pages — this page carries the framework surface.

Poetry::Ui::ComponentsHelper

module · app/helpers/poetry/ui/components_helper.rb

The poetry_* view helpers - the agent-facing surface ("use poetry_button, never a raw <button> with hand-written Tailwind"). Every registered component ships one (drift-gated by ComponentsHelperTest); slot blocks receive the component:

<%= poetry_dialog do |dialog| %> <% dialog.with_trigger { "Open" } %> <% dialog.with_title { "Settings" } %> <% end %>

Constant Description
COLOR_SCHEME_JS The inline bootstrap poetry_color_scheme_script emits: applies the stored (or OS) scheme before first paint and wires window.Poetry.colorScheme.
HELPER_CONTRACTS The value contracts runtime-enforced inside wrapper helpers, in registry shape - emitted as the registry's "helpers" section (rakelib/registry.rake) so poetry check and the MCP server validate these literals statically (the crash class: a helper-only literal like align: :leading that no component contract covers, failing only at render). Plain wrapper helpers need no entry here; registry generation lists them name-only.
INPUT_GROUP_ALIGNS The valid align: values for poetry_input_group_addon.
INPUT_GROUP_BUTTON_SIZES The valid size: values for poetry_input_group_button.

#poetry_accordion(**, &)

A vertically stacked set of interactive headings that each reveal a section of content.

poetry_accordion(open: %w[a]) do |accordion|
  accordion.with_item(value: "a", title: "First") { "First panel" }
  accordion.with_item(value: "b", title: "Second") { "Second panel" }
end

#poetry_alert(**, &)

A callout that highlights an important inline message.

poetry_alert(variant: :destructive) do |alert|
  alert.with_title { "Payment failed" }
  "Your card was declined. Update your billing details."
end

#poetry_alert_dialog(**, &)

A modal dialog that interrupts the user and expects a response.

poetry_alert_dialog do |dialog|
  dialog.with_trigger(variant: :destructive) { "Delete project" }
  dialog.with_title { "Delete this project?" }
  dialog.with_description { "This cannot be undone." }
  dialog.with_cancel { "Cancel" }
  dialog.with_action(variant: :destructive) { "Delete" }
end

#poetry_aspect_ratio(**, &)

ratio: is a string fraction ("16/9") - Ruby's 16/9 would truncate.

poetry_aspect_ratio(ratio: "16/9") do
  image_tag "cover.jpg", class: "size-full object-cover"
end

#poetry_attachment(**, &)

A file or image chip showing its name, type, and size.

poetry_attachment do |attachment|
  attachment.with_title { "quarterly-report.pdf" }
  attachment.with_description { "1.2 MB" }
end

#poetry_attachment_group(**attrs, &)

The horizontally-scrolling attachment rail (scroll-fade + snap).

<%= poetry_attachment_group do %>
  <%# poetry_attachment chips %>
<% end %>

#poetry_autocomplete(**, &)

An input that suggests options as you type - the text itself is the value.

poetry_autocomplete(name: "tag", label: "Search tags") do |auto|
  auto.with_item(label: "feature")
  auto.with_item(label: "fix")
end

#poetry_avatar(**, &)

The content block is the initials fallback.

poetry_avatar(src: user.avatar_url, label: "Ada Lovelace") { "AL" }

#poetry_badge(**, &)

A small count or status descriptor.

poetry_badge(variant: :success) { "Fulfilled" }

#poetry_breadcrumb(**, &)

Shows the path to the current page as a trail of links.

<%= poetry_breadcrumb do |crumb| %>
  <% crumb.with_item("Home", href: "/") %>
  <% crumb.with_ellipsis %>
  <% crumb.with_item("Breadcrumb") %>
<% end %>

#poetry_bubble(**, &)

A chat message bubble aligned to its sender.

poetry_bubble(variant: :secondary) { "Here's the summary." }

#poetry_bubble_group(**attrs, &)

A styled wrapper stacking one sender's consecutive bubbles - a dictionary element, not a component (a Bubble concern).

<%= poetry_bubble_group do %>
  <%= poetry_bubble(variant: :sent) { "Hi!" } %>
  <%= poetry_bubble(variant: :sent) { "You there?" } %>
<% end %>

#poetry_button(**, &)

Triggers an action or event, such as submitting a form or opening a dialog.

poetry_button(variant: :default) { "Save" }

#poetry_button_group(**, &)

Visually joins adjacent buttons and controls into one group.

poetry_button_group("aria-label": "Alignment") do
  safe_join([
    poetry_button(variant: :outline) { "Left" },
    poetry_button(variant: :outline) { "Right" }
  ])
end

#poetry_button_group_separator(**attrs)

The vertical divider between grouped controls.

poetry_button_group_separator

#poetry_button_group_text(**attrs, &block)

Non-interactive text inside a ButtonGroup run.

poetry_button_group_text { "of 12" }

#poetry_calendar(**, &)

The Calendar: a server-rendered month grid; name: makes it a form control. poetry--core--calendar adds nav + selection.

poetry_calendar(name: "event[date]", selected: "2026-07-04")

#poetry_card(**, &)

A container that groups related content and actions.

poetry_card do |card|
  card.with_title { "Team" }
  card.with_description { "Invite and manage members." }
  "Body content"
end

#poetry_carousel(**, &)

The Carousel: native scroll-snap slides - with_item per slide; label: names the region.

poetry_carousel(label: "Featured") do |carousel|
  carousel.with_item { "Slide one" }
  carousel.with_item { "Slide two" }
end

#poetry_checkbox(**)

Void control (no content block) - the label is EXTERNAL (Label/Field for= the button id) or label: (the aria-label fallback).

poetry_checkbox(name: "terms", label: "Accept terms")

#poetry_checkbox_group(**attrs, &block)

The select-all family (the APG mixed-state parent): the wrapper carries the poetry--core--checkbox-group controller; the parent checkbox fans out to every enabled item and item toggles re-derive it (all -> checked, none -> unchecked, some -> indeterminate).

poetry_checkbox_group do
  poetry_checkbox_group_all(id: "all")
  poetry_checkbox_group_item(name: "ids[]", value: "1", id: "row-1")
end

#poetry_checkbox_group_all(**attrs)

The group's parent checkbox (target: all) - checking it fans out.

poetry_checkbox_group_all(id: "select-all")

#poetry_checkbox_group_item(**attrs)

One member checkbox (target: item) - toggles re-derive the parent.

poetry_checkbox_group_item(name: "ids[]", value: "1", checked: true)

#poetry_clipboard_text(**)

Read-only value + one copy affordance (API keys, install commands, IDs); text_to_copy: overrides the clipboard when the display truncates. Editable text is poetry_input; masked secrets are poetry_sensitive_input.

poetry_clipboard_text(value: "gem install poetry-ui",
                                                label: "Install command")

#poetry_code_block(**)

Server-rendered highlighted code panel (rouge, soft dependency): language:, CSS-counter line_numbers:, highlight_lines:, and a copy affordance reading the rendered code. Inline code stays plain <code> typography.

poetry_code_block(
  code: "puts \"hello\"", language: "ruby"
)

#poetry_collapsible(**, &)

An interactive element that expands and collapses a section of content.

poetry_collapsible do |collapsible|
  collapsible.with_trigger { "Show details" }
  tag.div("Hidden until disclosed.")
end

#poetry_color_scheme_script

The color-scheme bootstrap (the pothole other ports patch into every host by hand): tokens ship .dark + color-scheme, but WHEN .dark applies is the host's job - and it must happen before first paint or every visit flashes light. Render inside <head>, before the stylesheets. Also wires window.Poetry.colorScheme (current/set/toggle/clear) for toggle controls; an unset preference follows the OS and tracks its changes live. Full recipe: the Theming guide, "Color scheme (dark mode)".

<%# in the layout <head>, before the stylesheets %>
<%= poetry_color_scheme_script %>

#poetry_combobox(**, &)

The type-to-filter value picker (Select's shell x Command's engine): options ARE values, committed to a hidden native select. Must be named (Field label via id: or aria-label). In forms, prefer f.poetry_combobox.

poetry_combobox(name: "framework", "aria-label" => "Framework") do |combobox|
  combobox.with_item(value: "rails") { "Ruby on Rails" }
  combobox.with_item(value: "hanami") { "Hanami" }
end

#poetry_command(**, &)

The filterable command-palette listbox: items DO things - picking a VALUE for a form is Combobox territory.

poetry_command("aria-label": "Command menu") do |command|
  command.with_item(value: "new-file") { "New file" }
  command.with_item(value: "search") { "Search" }
end

#poetry_command_dialog(**, &)

The ⌘K variant: a Command inside the Dialog chrome (sr-only title/description) with the OPT-IN hotkey: global shortcut.

poetry_command_dialog(hotkey: "meta+k") do |dialog|
  dialog.with_trigger(variant: :outline) { "Open palette" }
  dialog.with_item(value: "settings") { "Settings" }
end

#poetry_context_menu(**, &)

A menu of actions revealed by right-clicking an element.

poetry_context_menu do |menu|
  menu.with_trigger(tag: :div) { "Right-click this card" }
  menu.with_item { "Rename" }
  menu.with_item(variant: :destructive) { "Delete" }
end

#poetry_data_table(**, &)

The server-driven DataTable: rows/state/path come from the controller (State.from_params with a sortable: whitelist); columns are declared in the block. Sorting/filter/page are URL state.

<%= poetry_data_table(rows: @notes, state: state, total: @pages,
                      path: ->(p) { notes_path(**p) }, caption: "Notes") do |t| %>
  <% t.with_column("Title", key: :title, sortable: true) { |note| note.title } %>
  <% t.with_column("Created", key: :created_at, sortable: true) { |note| note.created_at.to_date } %>
<% end %>

#poetry_date_field(**)

The segmented date editor: a native input type=date (ISO on the wire, native pickers with no JS) enhanced into per-segment spinbutton editing.

poetry_date_field(name: "event[on]", label: "Event date")

#poetry_date_picker(**, &)

The DatePicker: a field-shaped trigger opening a Calendar in a Popover; name: is the form field.

poetry_date_picker(
  name: "due_on", label: "Due date", value: Date.new(2026, 6, 5)
)

#poetry_date_time_field(**)

A segmented date-and-time editor over a native datetime-local input: one control, one local wall-time value (no zone on the wire).

poetry_date_time_field(
  name: "event[starts_at]", label: "Starts", value: "2026-07-13T09:30"
)

#poetry_deferred(**, &)

A deferred region - Turbo loading physics (lazy/eager) plus poetry-owned placeholder and retryable-error states. The block is the placeholder.

poetry_deferred(src: "/dashboard/activity")

#poetry_dialog(**, &)

A window overlaid on the page for content that requires attention.

poetry_dialog do |dialog|
  dialog.with_trigger(variant: :outline) { "Open" }
  dialog.with_title { "Are you sure?" }
  dialog.with_description { "This cannot be undone." }
end

#poetry_drawer(**, &)

The Drawer: the swipeable edge dialog - trigger/title/ description/footer slots, direction: down/up/left/right.

poetry_drawer(show_swipe_handle: true) do |drawer|
  drawer.with_trigger { "Open drawer" }
  drawer.with_title { "Move goal" }
  drawer.with_description { "Set your daily activity goal." }
  "Drawer body"
end

#poetry_dropdown_menu(**, &)

A menu of actions or options triggered by a button.

poetry_dropdown_menu do |menu|
  menu.with_trigger(variant: :outline) { "Open" }
  menu.with_item { "Rename" }
  menu.with_item(variant: :destructive) { "Delete" }
end

#poetry_empty(**, &)

An empty-state placeholder with an icon, message, and actions.

poetry_empty do |empty|
  empty.with_title { "No projects yet" }
  empty.with_description { "Create your first project to get started." }
  poetry_button { "New project" }
end

#poetry_field(**, &)

Wraps a form control with its label, hint, and validation message.

poetry_field(
  id: "email", label_text: "Email", hint: "We never share it."
) do |field|
  tag.input(type: "email", name: "email", **field.control_attributes)
end

#poetry_field_group(**, &)

Stacks fields/fieldsets with the theme's rhythm; also the CSS @container scope Field's orientation: :responsive measures against.

poetry_field_group do
  safe_join([
    poetry_field { ... },
    poetry_field_separator,
    poetry_field { ... }
  ])
end

#poetry_field_separator(**, &)

Divider between stacked fields; pass a block for the inline caption form ("Or continue with").

poetry_field_separator { "Or continue with" }

#poetry_fieldset(**, &)

The Field family's group layer: a run of related fields inside a real <fieldset>, named by legend: (a real <legend>).

poetry_fieldset(legend: "Shipping address") do
  # poetry_field_group with the fields
end

#poetry_file_input(**, &)

A control for selecting, previewing, and removing files to upload.

poetry_file_input(
  variant: :dropzone, name: "attachments[]", multiple: true,
  hint: "PDF or PNG, up to 10 MB"
)

#poetry_hover_card(**, &)

A card that reveals preview content when its trigger is hovered.

poetry_hover_card do |card|
  card.with_trigger(href: "/users/nextjs") { "@nextjs" }
  "Joined December 2021."
end

#poetry_icon(**)

Renders an inline SVG icon from the icon set.

poetry_icon(name: :plus)

#poetry_id_integrity_script(force: false)

The composed-DOM duplicate-id tripwire: a DEVELOPMENT guard that scans the live page for duplicate [id] values after every composition event (load, Turbo loads, frame loads, morphs, stream insertions) and console-warns - the runtime complement to poetry check's static stable-identity heuristics, and the only check that sees real composition. Render in the development layout's <head>; it emits nothing outside development unless force: true (the dummy's test pages dogfood it that way).

<%# in the development layout's <head> %>
<%= poetry_id_integrity_script %>

#poetry_input(**)

A form control for entering a single line of text.

poetry_input(type: "email", name: "email",
                                        placeholder: "you@example.com")

#poetry_input_group(**, &)

One bordered surface combining an input with buttons, icons, or add-ons.

<%= poetry_input_group do %>
  <%= poetry_input_group_addon do %>
    <%= poetry_icon(name: :search) %>
  <% end %>
  <%= poetry_input_group_input(name: "q", placeholder: "Search...") %>
<% end %>

#poetry_input_group_addon(align: :"inline-start", **attrs, &block)

An addon region inside an InputGroup - icons, text, or small buttons aligned to one of the four edges of the group.

poetry_input_group_addon(align: :"inline-end") { poetry_icon(name: :search) }

#poetry_input_group_button(size: :xs, **attrs, &)

The tiny in-group action: a ghost Button re-sized by the group's dictionary (tailwind_merge lets h-6 beat the Button's own h-9).

poetry_input_group_button(size: :"icon-xs", label: "Copy") { poetry_icon(name: :copy) }

#poetry_input_group_input(**attrs)

The borderless in-group control: the group wears the chrome; the data-slot=input-group-control is what its focus/invalid selectors key on.

poetry_input_group_input(placeholder: "Search...")

#poetry_input_group_text(**attrs, &block)

Inline text inside an InputGroup - prefixes, suffixes, hints.

poetry_input_group_text { "https://" }

#poetry_input_group_textarea(**attrs)

The borderless multiline in-group control - the group wears the chrome, like poetry_input_group_input.

poetry_input_group_textarea(placeholder: "Add a note...")

#poetry_input_otp(**)

Fixed-length code entry: ONE native input over aria-hidden cells (paste/SMS-autofill/IME all native) - never per-cell inputs.

poetry_input_otp(name: "code", length: 6, groups: [3, 3])

#poetry_item(**, &)

A generic list row with media, content, and actions.

poetry_item(variant: :outline) do |item|
  item.with_title { "Backups" }
  item.with_description { "Nightly, retained 30 days" }
end

#poetry_item_separator(**attrs)

The row divider inside an item group: the Separator with the item-separator slot + spacing.

poetry_item_separator

#poetry_kbd(**, &)

The key text is the content block: poetry_kbd { "⌘" }.

poetry_kbd { "Esc" }

#poetry_kbd_group(**attrs, &)

A run of key chords (upstream KbdGroup): a wrapping <kbd> carrying the themed gap - poetry_kbd children render inside.

<%= poetry_kbd_group do %>
  <%= poetry_kbd { "⌘" } %><%= poetry_kbd { "K" } %>
<% end %>

#poetry_label(**, &)

An accessible caption bound to a form control.

poetry_label(for_id: "email") { "Email" }

#poetry_link(**, &)

A styled navigational hyperlink.

poetry_link(href: "/docs") { "Documentation" }

#poetry_marker(**, &)

A transcript divider or inline status marker for chat UIs.

poetry_marker(variant: :separator) { "Yesterday" }

#poetry_menubar(**, &)

A horizontal bar of menus, like a desktop application menu.

poetry_menubar(label: "Application") do |bar|
  bar.with_menu do |menu|
    menu.with_trigger { "File" }
    menu.with_item(shortcut: "⌘N") { "New" }
    menu.with_separator
    menu.with_item { "Print..." }
  end
end

#poetry_message(**, &)

A chat row pairing an author and avatar with message content.

poetry_message do |message|
  message.with_avatar { "AI" }
  message.with_header { "Assistant" }
  tag.div("Here's the plan for today.")
end

#poetry_message_group(**attrs, &)

A styled wrapper stacking one sender's consecutive messages - a dictionary element, not a component (a Message concern).

<%= poetry_message_group do %>
  <%# consecutive poetry_message rows from one sender %>
<% end %>

#poetry_message_scroller(**, &)

A streaming-aware transcript that keeps the latest message in view.

poetry_message_scroller(id: "chat") do
  # poetry_message_scroller_item rows
end

#poetry_message_scroller_item(id:, anchor: false, **attrs, &)

One transcript row - the id is how anchoring and Turbo Streams find it (data-message-id; anchor: pins the reading position).

<%= poetry_message_scroller_item(id: message.id) do %>
  <%= poetry_message(author: "Ada") { message.body } %>
<% end %>

#poetry_metadata_list(**, &)

A key-value list for labeled attributes on detail pages.

poetry_metadata_list(columns: :two) do |list|
  list.with_item(label: "Status") { "Active" }
  list.with_item(label: "Owner") { "Ada Lovelace" }
end

#poetry_meter(**)

A quantity within a known range (disk, seats, strength) - role "meter progressbar" (the two-token fallback); never indeterminate.

poetry_meter(value: 62, label: "Storage used")

#poetry_native_select(**, &)

A styled wrapper around the real native select control.

poetry_native_select(
  name: "sort", label: "Sort by",
  options: [["Newest first", "newest"], ["Oldest first", "oldest"]],
  selected: "newest"
)

#poetry_native_select_optgroup(**attrs, &block)

A styled <optgroup> inside poetry_native_select.

poetry_native_select_optgroup(label: "Europe") do
  poetry_native_select_option(value: "fr") { "France" }
end

#poetry_native_select_option(**attrs, &block)

One styled <option> inside poetry_native_select.

poetry_native_select_option(value: "us") { "United States" }

#poetry_navigation_menu(**, &)

The NavigationMenu: a disclosure bar - with_item for trigger+panel, with_link for destinations; label: names the nav.

<%= poetry_navigation_menu(label: "Main") do |nav| %>
  <% nav.with_item("Products", value: "products") do %>
    <%= poetry_navigation_menu_link(href: products_path) { "All products" } %>
  <% end %>
  <% nav.with_link("Docs", href: docs_path) %>
<% end %>

#poetry_navigation_menu_link(href:, active: false, **attrs, &block)

A panel entry: a REAL link (active: marks the current page).

poetry_navigation_menu_link(href: "/docs", active: true) { "Docs" }

#poetry_number_field(**)

The formatted number field: a formatted text input over a hidden type=number that submits the raw value; steppers with press-and-hold; ArrowUp/Down (Shift/Alt sizes) on the input.

poetry_number_field(
  name: "quantity", value: 2, min: 0, label: "Quantity"
)

#poetry_optimistic_form(attribute_name: nil, value: OptimisticFormBuilder::UNSET, **options, &)

Optimistic UI for a Turbo form: the block's form.optimistic_template authors the PREDICTED state as a turbo-stream in a <template>; the controller paints it on submit and reconciles (morph refresh) only when the server rejects. The server contract - success answers 204 or a targeted stream, NEVER a redirect; failure answers 4xx - and the morph-refresh meta prerequisite live in the Optimistic Forms guide on the docs site. attribute_name:/value: auto-inject the submitted-value hidden field (false survives; form.optimistic_hidden_field places it explicitly).

<%= poetry_optimistic_form(model: task, attribute_name: :done, value: true) do |form| %>
  <%= form.optimistic_template do %>
    <%# the predicted turbo-stream(s) %>
  <% end %>
  <%= form.submit "Done" %>
<% end %>

#poetry_pagination(**)

Data-driven pagination: poetry_pagination(current:, total:, path:) - path: is a callable ->(page) { url }; the component owns the truncation math and the accessible nav.

poetry_pagination(current: 3, total: 12,
                                             path: ->(page) { products_path(page: page) })

#poetry_popover(**, &)

Rich floating content anchored to a trigger.

poetry_popover do |popover|
  popover.with_trigger(variant: :outline) { "Open popover" }
  popover.with_title { "Dimensions" }
  tag.p("Set the dimensions for the layer.")
end

#poetry_progress(**)

A determinate progress bar toward task completion.

poetry_progress(value: 60, label: "Uploading")

#poetry_questionnaire(**, &)

One-question-at-a-time survey: a REAL form of fieldset items - native radio/checkbox/text answers, validate-gated navigation.

poetry_questionnaire(url: "/surveys") do |survey|
  survey.with_item(name: "mood", title: "How was your week?") do |item|
    item.with_choice(value: "good", label: "Good")
    item.with_choice(value: "bad", label: "Bad")
  end
end

#poetry_radio_group(**, &)

The exclusive-choice control: group.with_item(value:, label:) - one hidden native radio per item (collection_radio_buttons-exact serialization). The group MUST be labelled (label: or aria-labelledby).

poetry_radio_group(
  name: "plan", value: "monthly", label: "Billing plan"
) do |group|
  group.with_item(value: "monthly", label: "Monthly")
  group.with_item(value: "yearly", label: "Yearly")
end

#poetry_resizable(**, &)

The Resizable panel group: with_panel x N; handles are interleaved automatically (APG window splitters).

poetry_resizable(class: "h-48 rounded-lg border") do |group|
  group.with_panel(default_size: 25) { tag.div("Sidebar") }
  group.with_panel { tag.div("Content") }
end

#poetry_scroll_area(**, &)

The native scroll region: size it with classes; label: names it.

poetry_scroll_area(label: "Tags", class: "h-72 w-48") do
  safe_join(tags.map { |tag| tag.name })
end

#poetry_search_field(**)

type=search on InputGroup chrome: Escape clears-then-dismisses, focus-holding clear button, native WebKit affordances suppressed.

poetry_search_field(name: "q", label: "Search", placeholder: "Search...")

#poetry_select(**, &)

The value-choosing listbox (options ARE values; actions belong to poetry_dropdown_menu). Must be named: a Field label (id: + label[for]) or aria-label. In forms, prefer f.poetry_select.

poetry_select(name: "fruit", id: "fruit",
                                         placeholder: "Pick a fruit") do |select|
  select.with_item(value: "apple") { "Apple" }
  select.with_item(value: "banana") { "Banana" }
end

#poetry_sensitive_input(**)

Secret shown-on-demand (API keys, tokens): masked container is the reveal button, blur/Escape/eye re-mask, copy: copies without revealing. Plain passwords being SET (not shown) can stay a poetry_input type: :password.

poetry_sensitive_input(name: "api_key", label: "API key",
                                                 value: token, copy: true)

#poetry_separator(**)

A thin divider between content, decorative or semantic.

poetry_separator

#poetry_sheet(**, &)

A dialog that slides in from a screen edge.

poetry_sheet(side: :right) do |sheet|
  sheet.with_trigger { "Edit profile" }
  sheet.with_title { "Edit profile" }
  "Sheet body"
end

#poetry_sidebar(**, &)

The Sidebar: the app-shell frame - with_nav is the column, with_inset the page area; poetry--core--sidebar owns the collapse.

<%= poetry_sidebar(open: cookies[:sidebar_state] != "false", collapsible: :icon) do |shell| %>
  <% shell.with_nav do %>
    <%= poetry_sidebar_group do %>...menu...<% end %>
  <% end %>
  <% shell.with_inset do %>
    <%= poetry_sidebar_trigger %>
    <main>...page...</main>
  <% end %>
<% end %>

#poetry_sidebar_group_action(label: nil, **attrs, &block)

The group-corner action (upstream SidebarGroupAction): pinned to the group's top-right by the theme.

poetry_sidebar_group_action(label: "Add project") { poetry_icon(name: :plus) }

#poetry_sidebar_group_label(**attrs, &block)

The group heading: a muted label above a sidebar menu.

poetry_sidebar_group_label { "Projects" }

#poetry_sidebar_input(**attrs)

The sidebar search input (upstream SidebarInput): the themed Input sized for the header well.

poetry_sidebar_input(placeholder: "Search...")

#poetry_sidebar_menu_action(show_on_hover: false, label: nil, **attrs, &block)

An item-corner action (menu-action): absolutely positioned inside the menu item (the menu button reserves pr-8 room via the group marker). show_on_hover: keeps it invisible until the item is hovered/focused on desktop.

poetry_sidebar_menu_action(label: "More", show_on_hover: true) { poetry_icon(name: :ellipsis) }

#poetry_sidebar_menu_badge(**attrs, &block)

A trailing badge (menu-badge): count/status chrome in the item corner, pointer-transparent.

poetry_sidebar_menu_badge { "24" }

#poetry_sidebar_menu_button(href: nil, active: false, size: :default, variant: :default, **attrs, &block)

A menu button: an anchor (href:) or a button, with the active state + size and variant axes. active: is the current route (data-active styles it); variant: :outline draws the bordered treatment.

poetry_sidebar_menu_button(href: "/projects", active: true) { "Projects" }

#poetry_sidebar_menu_skeleton(icon: false, text_width: "70%", **attrs)

A loading placeholder row (upstream SidebarMenuSkeleton): optional leading icon block + a text bar with a random-ish width the caller can pin via text_width:.

poetry_sidebar_menu_skeleton(icon: true)

#poetry_sidebar_menu_sub_button(href: nil, active: false, **attrs, &block)

A nested menu entry: an anchor (href:) or a button; active: marks the current route (aria-current + data-active).

poetry_sidebar_menu_sub_button(href: "/settings") { "Settings" }

#poetry_sidebar_rail(**attrs)

The rail: an edge strip that toggles the sidebar (tabindex -1 - the trigger is the keyboard affordance).

poetry_sidebar_rail

#poetry_sidebar_separator(**attrs)

The themed divider between sidebar groups.

poetry_sidebar_separator

#poetry_sidebar_trigger(**attrs)

The sidebar collapse toggle (lives in the inset): a ghost icon Button wired to the sidebar controller on the wrapper.

poetry_sidebar_trigger

#poetry_skeleton(**, &)

A pulsing placeholder shown while content loads.

poetry_skeleton(class: "size-10 rounded-full")
poetry_skeleton(class: "h-4 w-32")

#poetry_slider(**)

Void control - numeric value (value:) or [low, high] range (values:) on a continuous track; every thumb needs a distinct accessible name (label:).

poetry_slider(name: "volume", value: 50, label: "Volume")

#poetry_spinner(**)

An indeterminate loading indicator that announces itself.

poetry_spinner(label: "Saving...")

#poetry_stat(**, &)

A single KPI: a muted label over a large metric value.

poetry_stat(label: "Revenue", delta: "+12.5%", trend: :up) do
  "$45,231"
end

#poetry_switch(**)

Void control - instant-effect on/off (role=switch announces on/off); values staged for submit belong to poetry_checkbox.

poetry_switch(name: "notifications", checked: true,
                                         label: "Email notifications")

#poetry_table(**, &)

The Table: the component renders the overflow container + real <table>; the part helpers stamp the data-slot + source-exact classes onto the semantic table elements the consumer composes.

poetry_table do
  safe_join([
    poetry_table_header { poetry_table_row { poetry_table_head { "Invoice" } } },
    poetry_table_body { poetry_table_row { poetry_table_cell { "INV001" } } }
  ])
end

#poetry_tabs(**, &)

Tabs: declare with with_tab(title, value:) + panel blocks; the component owns the ARIA wiring and the two-controller split.

<%= poetry_tabs(default: "account", label: "Account settings") do |tabs| %>
  <% tabs.with_tab("Account", value: "account") do %>...panel...<% end %>
  <% tabs.with_tab("Password", value: "password") do %>...panel...<% end %>
<% end %>

#poetry_tag_group(**, &)

Removable-chip collection (grid semantics, roving arrows, Delete removal w/ focus recovery); name: serializes name[] per tag.

poetry_tag_group(label: "Recipients", name: "recipients") do |group|
  group.with_tag(value: "ada", label: "Ada")
  group.with_tag(value: "grace", label: "Grace")
end

#poetry_textarea(**)

Input's multiline sibling - the value is the CONTENT (value:), not a block; auto-grow is CSS (field-sizing-content), never a JS autosizer.

poetry_textarea(name: "bio", rows: 4,
                                           placeholder: "Tell us about yourself")

#poetry_time_field(**)

DateField at hour granularity: HH:MM[:SS] on the wire; the locale decides 12- vs 24-hour editing (hour_cycle: pins it).

poetry_time_field(
  name: "starts_at", label: "Start time", value: "09:30"
)

#poetry_timeline(**, &)

A sequence of dated events as an ordered list.

poetry_timeline do |timeline|
  timeline.with_item(title: "Order placed", time: "Mar 15", completed: true)
  timeline.with_item(title: "In transit") { "Estimated delivery Thursday." }
end

#poetry_toast(**, &)

A brief, auto-dismissing notification message.

poetry_toast(variant: :success) do |toast|
  toast.with_title { "Message archived" }
  toast.with_action { "Undo" }
end

#poetry_toast_trigger(**, &)

Client-side toast delivery: pressing the trigger clones the addressed <template>'s toast into the toaster region - no server round-trip.

<%= poetry_toast_trigger(template: "copied-toast") do %>
  Copy link
<% end %>
<template id="copied-toast">
  <%= poetry_toast(duration: 4000) do |toast| %>
    <% toast.with_title { "Copied" } %>
  <% end %>
</template>

#poetry_toaster(**, &)

The toast viewport - render ONCE in the application layout (it is data-turbo-permanent; turbo_stream.poetry_toast appends into it).

<%= poetry_toaster do %>
  <% flash.each do |kind, message| %>
    <%= poetry_toast(variant: kind.to_s == "alert" ? :destructive : :default) do |toast| %>
      <% toast.with_title { message } %>
    <% end %>
  <% end %>
<% end %>

#poetry_toggle(**, &)

Pressed-state button (aria-pressed) - UI state, NOT form data; the content block is the icon/text (icon-only requires label:).

poetry_toggle(label: "Bookmark", pressed: bookmarked?) do
  poetry_icon(name: :bookmark)
end

#poetry_toggle_group(**, &)

A set of Toggle-styled items under one value machine + one roving tab stop: group.with_item(value:, label:) { icon/text }.

poetry_toggle_group(value: "left", label: "Text alignment") do |group|
  group.with_item(value: "left", label: "Align left") { icon(:"align-left") }
  group.with_item(value: "center", label: "Align center") { icon(:"align-center") }
end

#poetry_toolbar(**, &)

A horizontal group of controls that acts as one keyboard tab stop - Tab passes over the group, Arrow keys move between its controls.

poetry_toolbar(label: "Bulk actions") do |toolbar|
  toolbar.with_button(variant: :outline) { "Archive" }
  toolbar.with_separator
  toolbar.with_input(name: "q", placeholder: "Filter…")
end

#poetry_tooltip(**, &)

A floating label describing an element on hover or focus.

poetry_tooltip do |tooltip|
  tooltip.with_trigger(variant: :outline, size: :icon, label: "Print") do
    poetry_icon(name: :printer)
  end
  "Print the current page"
end

#poetry_tooltip_provider(delay_duration: 0, skip_delay_duration: 300, disable_hoverable_content: false, **attrs, &)

The tooltip delay/warm SCOPE - a config-carrying div, NOT a controller (the DOM ancestor IS the shared delay scope; the tooltip controller reads closest('[data-slot=tooltip-provider]') and keys the module-level warm registry by it). Wrap control rows in ONE provider so the warm grace makes the row feel continuous.

<%= poetry_tooltip_provider(delay_duration: 300) do %>
  <%# tooltips inside share the delay + warm state %>
<% end %>

#poetry_tree(**, &)

Hierarchical expandable list (flat treegrid): items via the nested with_item builder, expansion persisted by the host through poetry:tree:toggle.

<%= poetry_tree(label: "Files") do |tree| %>
  <% tree.with_item(text: "docs", value: "docs", expanded: true) do |docs| %>
    <% docs.with_item(text: "intro.md", value: "intro", href: "/docs/intro") %>
  <% end %>
<% end %>

#poetry_typeset(**, &)

Prose styling for long-form and rendered-markdown content.

poetry_typeset(preset: "docs") do
  @article_html
end

#poetry_webmcp_form(tool:, **options, &)

Declares a form as a WebMCP tool - the declarative registration path: the <form> carries toolname/tooldescription, so a WebMCP browser registers it as an agent-callable tool with NO JavaScript (the parameter schema is synthesized from the controls; each parameter's description comes from its <label>, so the poetry FormBuilder's model-derived labels describe the tool for free; tool_description: on a field overrides one). Defaults the builder to Poetry::Ui::FormBuilder. autosubmit: true is GET-only by construction - a mutating form always keeps the user's Submit.

<%= poetry_webmcp_form(url: orders_path, method: :get,
                       tool: { name: "find_orders", description: "Search orders by timeframe.",
                               autosubmit: true }) do |form| %>
  <%= form.field(:timeframe, tool_description: "A relative range such as last_7_days.") %>
<% end %>

Poetry::Ui::FormBuilder

class < ActionView::Helpers::FormBuilder · app/helpers/poetry/ui/form_builder.rb

The poetry FormBuilder - the model-truth end of the error quartet: form.field(:email) renders a Field wrapping an Input with everything derived from the object, never hand-wired:

label from human_attribute_name (Rails i18n) value from the object error from object.errors (auto-flow) required from the model's presence validators (-> aria-required only - the label-station rule; never the native attribute) ids/aria field_id + aria-describedby via Field#control_attributes

Usage: form_with(model:, builder: Poetry::Ui::FormBuilder).

Constant Description
AGENT_RULES The agent-facing rules: flow into the registry's form_builder section and llms.txt's Forms section.
COLLECTION_ARGS The as: values whose dispatch method takes the collection as its second positional argument.
COLUMN_TYPES Model column type -> f.input control type.
INPUT_DISPATCH -- f.input: the inferred entrypoint ------------------------------ form.input(:email) - one call, everything derived: type from as: -> attachment duck-typing -> AR enum -> attribute type -> name heuristics; hint/placeholder from the poetry_form (or simple_form) i18n chain when not passed; maxlength/min/max from validations.
METHOD_SUMMARIES One-line summaries of the builder methods, projected into the registry's form_builder section (keys group related methods). The docblocks carry the fuller human reference for the same methods; FormBuilderDocsTest gates the pair against drifting apart.
STRING_HEURISTICS Name heuristics on string-typed attributes (the classic naming regexes, trimmed to the ones poetry renders distinctly).
TYPED_FIELDS The as: values that answer with #field carrying a native input type.

.registry_section

The registry's form_builder section (consumed by LlmsText's Forms section, the MCP server, and skills - boot-free from the committed registry).

#association(method, as: nil, collection: nil, hint: nil, **)

form.association(:company) - reflection-derived: belongs_to -> company_id + Combobox, has_many/HABTM -> singular_ids + the checkbox group; collection from the association klass, label method auto-detected (to_label/name/title/to_s, the conventional chain). as: overrides (:select, :combobox, :checkbox_group).

#autocomplete(method, suggestions = nil, hint: nil, **options, &block)

form.autocomplete(:city, %w[...]) - the input IS the value (suggestions are conveniences, not constraints); items from the suggestions array ([label, value] pairs or bare strings) or a block of auto.with_item calls.

#button(value = nil, **options, &block)

form.button - #submit with block/content support: the block (or value) is the Button's content, type stays submit.

#calendar(method, hint: nil, **options)

form.calendar(:starts_on) - the always-visible month grid as a form participant (mode: :range posts name[]).

#check_box(method, options = {}, checked_value = "1", unchecked_value = "0")

The f.check_box-equivalent (the toggle family's form story): name/id derived, checked: from the object's attribute truthiness, "1"/"0" plus the unchecked-hidden pair (ActionView::Helpers::Tags::CheckBox parity incl. hidden-input-first ordering - the Checkbox component renders the pair). A BARE control mapping: compose with a Field (control_attributes) for the label/hint/error quartet.

#checkbox(method, options = {}, checked_value = "1", unchecked_value = "0")

The Rails 8 spelling of #check_box.

#checkbox_group(method, collection, hint: nil, select_all: false, **options)

The collection_check_boxes-equivalent: a Field(group) wrapping the APG select-all group (DD sweep) - items post name[] and the leading empty hidden clears when none are checked. select_all: true (or a label string) adds the mixed-state parent checkbox.

#collection_check_boxes(method, collection, value_method, text_method, **)

ActionView's collection_check_boxes arity adapted onto checkbox_group (name[] items plus the leading clearing hidden).

#collection_checkboxes(method, collection, value_method, text_method, **)

The Rails 8 spelling of #collection_check_boxes.

#collection_radio_buttons(method, collection, value_method, text_method, **)

ActionView's collection_radio_buttons arity adapted onto radio_group (one hidden native radio per item, same serialization).

#collection_select(method, collection, value_method, text_method, # rubocop:disable Metrics/ParameterLists options = {}, html_options = {})

ActionView's collection_select arity, verbatim, adapted onto poetry_select (value_method/text_method read off each item).

#date_field(method, hint: nil, **options)

form.date_field(:due_on) - a Field wrapping a DateField; params arrive as ISO yyyy-mm-dd with or without JS. OVERRIDES ActionView's date_field (the number_field precedent).

#date_picker(method, hint: nil, **options)

form.date_picker(:due_on) - the calendar-popup pick; value posts ISO like date_field. (Quartet ids land on the composed control's root for now - the input-level aria refinement comes later.)

#datetime_field(method, hint: nil, **options)

A Field-wrapped DateTimeField (one control, one datetime-local value; seconds:/hour_cycle: pass through).

  • method (Symbol) — the model attribute
  • hint (String, nil) — hint text

#email_field(method, hint: nil, **options)

form.email_field(:email) - a Field-wrapped Input (type: :email).

#field(method, as: :input, hint: nil, **input_options)

One field entrypoint for field-shaped controls: as: :input (the default, with type:) or as: :textarea (rows: passes through) - Own-line controls slot in as as: values; group-shaped controls get dedicated methods (radio_group, slider, otp_field).

#fieldset(legend:, hint: nil, **options, &)

form.fieldset(legend: "Shipping") { |f| ... } - the grouped-fields frame; yields the builder for nesting.

#file_input(method, hint: nil, **options)

form.file_input(:document) / form.file_input(:photos, variant: :dropzone, multiple: true) - a Field wrapping a FileInput; the native input is the form value, so ActiveStorage attaches as usual.

#group(**options, &)

form.group { |f| ... } - the FieldGroup stack (the @container the responsive Field orientation measures against).

#input(method, as: nil, collection: nil, hint: nil, required: nil, **options)

The inferred entrypoint: one call, everything derived from the model. The control type comes from as: when given, otherwise from attachment duck-typing, AR enums, the attribute/column type, and name heuristics; hint/placeholder resolve from the poetry_form (or simple_form) i18n chain when not passed; length and numeric validations become maxlength/min/max/step attributes.

  • method (Symbol) — the model attribute
  • as (Symbol, nil) — explicit control type - an INPUT_DISPATCH key, :email/:url/:tel, :boolean, :switch, or :enum (overrides inference)
  • collection (Enumerable, nil) — choices for collection-shaped types (:select, :combobox, :radio_group, :autocomplete, :native_select)
  • hint (String, nil) — hint text (overrides the i18n chain)
  • required (Boolean, nil) — overrides the presence-validator inference for this call (aria-required only, never the native attribute)

Returns (ActiveSupport::SafeBuffer) — the rendered Field

<%= form_with(model: user, builder: Poetry::Ui::FormBuilder) do |f| %>
  <%= f.input :email %>
  <%= f.input :role, as: :select, collection: %w[admin member] %>
  <%= f.input :active, switch: true %>
  <%= f.submit %>
<% end %>

#native_select(method, choices = nil, include_blank: nil, hint: nil, **options)

form.native_select(:country, [["USA", "us"], ...]) - the styled NATIVE <select> (zero JS); poetry_select/poetry_combobox stay the rich paths. include_blank: posts "" (Rails semantics).

#number_field(method, hint: nil, **options)

form.number_field(:quantity, min: 0, max: 100) - a Field wrapping a NumberField, label/error/required from the object. The hidden <input type=number> submits the raw value; format: only shapes the display.

#otp_field(method, length: 6, hint: nil, **options)

The verification-code story: form.otp_field(:code, length: 6) - a Field wrapping an InputOTP, label/error/required from the object. The value is deliberately NEVER round-tripped (a rejected code is dead; re-rendering it invites resubmit-the-same-wrong-code loops)

  • pass value: explicitly to override.

#password_field(method, hint: nil, **)

form.password_field(:password) - a Field-wrapped Input (type: :password); the value NEVER round-trips (Rails' own behavior). Revealable secrets are #sensitive_input.

#telephone_field(method, hint: nil, **options)

form.telephone_field(:phone) - a Field-wrapped Input (type: :tel). The Rails alias for telephone_field.

#poetry_combobox(method, choices = nil, include_blank: nil, hint: nil, **options, &block)

The poetry_select twin for the type-to-filter picker (the combobox capstone): a Field wrapping a Combobox, everything derived from the object. The hidden native <select> is the serialization truth, the Field's control_attributes land on the TRIGGER (id + aria-describedby error-before-hint + aria-invalid), and label[for: id] click-focuses the combobox.

choices accept the same Rails shapes as poetry_select: [["Label", value], ...] pairs, flat %w[a b], grouped {"Group" => [["Label", value], ...]} (group parts wear Command's heading:), or nil plus a block of combobox.with_item calls. include_blank: "Choose..." doubles as the placeholder text (the blank option posts "" and fails native required validation - deselection is a form affordance, never a re-click toggle).

multiple: true is the chips mode: the value is the model's ARRAY (params post name[] - Rails' own convention, the [] derived for you) and the control_attributes land on the INLINE INPUT (the chips field has no trigger); include_blank has no meaning there.

#poetry_select(method, choices = nil, include_blank: nil, hint: nil, **options, &block)

The FormBuilder#select-equivalent (the listbox capstone): a Field wrapping a Select, everything derived from the object. The hidden native <select> is the serialization truth (name/value/ required ride it), the Field's control_attributes land on the TRIGGER (id + aria-describedby error-before-hint + aria-invalid), and label[for: id] click-focuses the combobox.

choices accept the Rails shapes: [["Label", value], ...] pairs, flat %w[a b], grouped {"Group" => [["Label", value], ...]}, or nil plus a block of select.with_item calls. include_blank: "Choose..." doubles as the placeholder text (Rails include_blank semantics - the blank option posts "" and fails native required validation, which is the correct behavior).

#radio_group(method, collection, hint: nil, **options)

The collection_radio_buttons-equivalent (the exclusive-choice story): a Field wrapping a RadioGroup, items from the collection ([[value, label], ...] pairs or bare values), value/label/error/ required derived from the object. Serialization is byte-identical to collection_radio_buttons (one hidden native radio per item, shared name; nothing submits when none is checked).

#range_field(method, hint: nil, **)

Rails' range_field IS poetry's Slider (a naked native range would silently bypass the Field quartet).

#search_field(method, hint: nil, **options)

form.search_field(:query) - a Field wrapping the SearchField (native type=search + the clear affordance).

#select(method, choices = nil, options = {}, html_options = {}, &)

ActionView's select arity adapted onto poetry_select.

#sensitive_input(method, hint: nil, **options)

form.sensitive_input(:api_key) - the revealable-secret story : masked at rest, reveal + optional copy:.

#slider(method, range: false, hint: nil, **options)

The bounded-numeric story: form.slider(:volume) single (label from human_attribute_name), form.slider(:price_range, range: true, label: [...]) reads an Array[2] and submits name[] (params: ["200", "800"] - Rails' own array convention). Field wraps for hint/error; the describedby lands on each THUMB.

#submit(value = nil, **options)

form.submit -> a poetry Button (type submit); label from Rails' own i18n default ("Create Model" / "Update Model"). loading: true opts into the Button loading treatment.

#switch(method, options = {}, checked_value = "1", unchecked_value = "0")

The same mapping wearing switch semantics (Rails has NO native switch builder): role=switch announces on/off; use it for instant-effect settings, check_box for values staged for submit.

#tag_group(method, hint: nil, **options)

form.tag_group(:topics) - the removable-chips story for an ARRAY attribute: one hidden name[] per tag, plus the leading empty hidden (Rails array convention: removing every tag still clears).

#telephone_field(method, hint: nil, **options)

form.telephone_field(:phone) - a Field-wrapped Input (type: :tel).

#text_area(method, hint: nil, **)

form.text_area(:bio) - a Field-wrapped Textarea (rows: passes through; auto-grow is CSS, never a JS autosizer).

#text_field(method, hint: nil, **options)

form.text_field(:name) - a Field-wrapped Input (type: :text).

#textarea(method, hint: nil, **)

The Rails 8 spelling of #text_area.

#time_field(method, hint: nil, **options)

form.time_field(:starts_at) - a Field wrapping a TimeField; params arrive as HH:MM (HH:MM:SS with seconds: true).

#url_field(method, hint: nil, **options)

form.url_field(:website) - a Field-wrapped Input (type: :url).

Poetry::Ui::Testing

module · lib/poetry/ui/testing.rb

Consumer-facing interaction testers: drive poetry components through their REAL keyboard/pointer sequences in a Capybara system test and assert against the public attribute contract (data-open, aria-expanded, data-highlighted) - never against markup internals. Each tester is an executable spec of its component's interaction contract; agents are taught to test through them (the usage skill), so generated tests are correct by construction.

require "poetry/ui/testing"

class PlanTest < ApplicationSystemTestCase include Poetry::Ui::Testing

test "picking a plan" do visit settings_path select = poetry_select("#plan") select.select_option("Pro", via: :keyboard) assert_equal "pro", select.value end end

Testers locate parts by data-slot from the root you hand them, and wait with Capybara's own retry discipline - no sleeps. via: swaps the ENTIRE event sequence (:mouse clicks, :keyboard focuses the trigger and drives keys), because the two paths exercise different controller seams.

#assert_poetry_controllers_registered(session: nil, application: "window.Stimulus")

Asserts that every poetry controller on the page is registered. Flunks under Minitest, raises {RegistrationError} elsewhere, with the identifiers and the two causes to check first.

  • session (Capybara::Session, nil) — defaults to the test's page (or Capybara.current_session)
  • application (String) — JS expression naming the Stimulus application (default window.Stimulus)

#poetry_combobox(root, session: nil)

A Combobox tester rooted at the component (single or multiple).

  • root (String, Capybara::Node::Element) — the component root
  • session (Capybara::Session, nil) — defaults to the test's page (or Capybara.current_session)

#poetry_dialog(root, session: nil)

A Dialog tester rooted at the component.

  • root (String, Capybara::Node::Element) — the element carrying data-component=dialog (trigger + content)
  • session (Capybara::Session, nil) — defaults to the test's page (or Capybara.current_session)

#poetry_dropdown_menu(root, session: nil)

A DropdownMenu tester rooted at the component.

  • root (String, Capybara::Node::Element) — the component root
  • session (Capybara::Session, nil) — defaults to the test's page (or Capybara.current_session)

#poetry_select(root, session: nil)

A Select tester rooted at the component.

  • root (String, Capybara::Node::Element) — the component root
  • session (Capybara::Session, nil) — defaults to the test's page (or Capybara.current_session)

#poetry_unregistered_controllers(session: nil, application: "window.Stimulus")

Every poetry controller identifier on the current page that the host's Stimulus application has NOT registered - empty when the wiring is healthy. Stimulus never errors on an unknown identifier (the element just stays inert), and one failed import in the controllers graph silently takes every poetry controller with it, so nothing else surfaces this.

  • session (Capybara::Session, nil) — defaults to the test's page (or Capybara.current_session)
  • application (String) — JS expression naming the Stimulus application (Rails' default controllers/application.js exposes window.Stimulus)

Returns (Array<String>) — the unregistered identifiers, sorted

Poetry::Ui::Chat

module · lib/poetry/ui/chat.rb

The chat replay DSL: script a user/assistant conversation in Ruby, get a DETERMINISTIC timeline of streaming frames to replay through the real Turbo Stream pipeline - no model, no network, fixed ids, fixed pacing. The DSL is pure data (transcript + timing); rendering frames into Message rows is the consumer's job (the docs replay demo is the reference consumer).

A tool with approval: true PAUSES its segment after the input frame; the frames that follow (its output - or denial - and later parts) belong to the continuation and are produced by continuation_frames(approved:) - the human-in-the-loop model, server-shaped: the pause is a rendered form, the continuation is the stream after the decision.

script = Poetry::Ui::Chat.script do
  user "What's the weather in Tokyo?"
  assistant do |w|
    w.reasoning "Check the weather tool first."
    w.tool("getWeather", input: { city: "Tokyo" }, sleep_ms: 800,
           output: { temp: 21, condition: "clear" })
    w.text "Clear skies and 21°C in Tokyo."
  end
end
script.segments  # => enumerable segments; assistant segments carry
                 #    frames (accumulated part states + sleeps + versions)

.script(&)

Builds a Script from the block - instance_eval'd, so bare user and assistant calls declare the segments in transcript order.

Returns (Script) — the compiled, deterministic script

Poetry

module · lib/poetry/ui.rb

The poetry namespace: poetry-ui shares it with poetry-core (the DSL) and the optional poetry-charts.

Poetry::AddGenerator

class < Rails::Generators::Base · lib/generators/poetry/add/add_generator.rb

rails g poetry:add Button [@acme/fancy-chart ...] - the copy-in tier plus the ecosystem address scheme.

Bare names and @poetry/* are the installed gems' own items: component source copies into app/components where Rails autoload precedence shadows the gem ("own the code"; dependencies resolve recursively, existing files are SKIPPED - local edits win, always), and block names hand off to poetry:block. Everything else is a REMOTE registry address - a URL, a local item file, or @namespace/item against the registries: section of config/poetry_components.yml - resolved through RegistryClient + RegistryInstaller: the dependency DAG fetches recursively and writes in topo order, poetry components satisfy at RUNTIME (the gem provides them - no copy), targets are traversal-checked, and gem dependencies are REPORTED, never installed. Every install records provenance in the manifest.

bin/rails g poetry:add button
Constant Description
DEPENDENCIES Composition edges - single-sourced on the gem module (the registry items emit the same map as registryDependencies).
MANIFEST The copy-in provenance manifest poetry:install seeds.
TAILWIND_ENTRY The host's Tailwind entry stylesheet (for @source injection).

Poetry::AgentRulesGenerator

class < Rails::Generators::Base · lib/generators/poetry/agent_rules/agent_rules_generator.rb

rails g poetry:agent_rules - the two-file, opposite-lifecycle agent ruleset:

.poetry/agent-rules.md gem-owned, FORCE-overwritten every run (generated live from the component registry) .poetry/house-rules.md seeded once, then user-owned - never touched again

plus an idempotent marker-import into CLAUDE.md and AGENTS.md: insert-between-markers, replace-on-rerun, and detect-but-never-rewrite when a file carries a broken half-marker.

bin/rails g poetry:agent_rules
Constant Description
END_MARKER The closing half of the marker pair bounding the import block.
HOUSE_RULES_SEED The once-only seed content of .poetry/house-rules.md.
IMPORT_BLOCK The marker-bounded pointer inserted into CLAUDE.md and AGENTS.md.
START_MARKER The opening half of the marker pair bounding the import block.

Poetry::AgentsGenerator

class < Rails::Generators::Base · lib/generators/poetry/agents/agents_generator.rb

rails g poetry:agents - writes or refreshes the poetry section of the host's AGENTS.md (the agent-facing pointer to llms.txt / poetry check) without running the full install. poetry:install performs the same step.

bin/rails g poetry:agents

Poetry::BlockGenerator

class < Rails::Generators::Base · lib/generators/poetry/block/block_generator.rb

rails g poetry:block <name> - copies a vetted composed screen into the app as source the app OWNS. Blocks are starting points an agent (or a person) edits, not components it configures: the file lands under app/views/blocks/ as a partial, the sample content is meant to be replaced in place, and poetry check keeps validating the result like any other template. --list (or no argument) prints the catalog from the registry's blocks section.

bin/rails g poetry:block login-form
Constant Description
OWNERSHIP_HEADER The header stamped on every copied block: names the generating command and hands ownership to the app.

Poetry::DiffGenerator

class < Rails::Generators::Base · lib/generators/poetry/diff/diff_generator.rb

rails g poetry:diff - the copy-in upgrade report. poetry's three ownership tiers upgrade differently: gem-owned code rides bundle update, the vendored css set rides a poetry:install re-run, and copy-ins are app-OWNED - nothing may rewrite them. This generator closes the loop on that third tier: it reads the provenance manifest (config/poetry_components.yml) and reports, file by file, where the app's copies stand against what the installed gems ship NOW. Read-only by contract - it prints, it never writes; poetry:add re-run is the (skip-if-exists) way to pick up newly-shipped files.

bin/rails g poetry:diff
Constant Description
BLOCKS_ROOT Where block copy-ins land in the host app.
BLOCK_HEADER The ownership header poetry:block stamps on copy (stripped before comparing, like the gem template's own poetry:block header).
COMPONENT_ROOT Where component copy-ins land in the host app.
MANIFEST The copy-in provenance manifest poetry:add records into.

Poetry::EditorGenerator

class < Rails::Generators::Base · lib/generators/poetry/editor/editor_generator.rb

rails g poetry:editor - wire the poetry agent surface into the editors a Rails team actually uses. poetry has no bespoke extension (a possible future direction); what it DOES have is a standard MCP stdio server and a source-generated component registry, so this generator emits:

.mcp.json Claude Code (mcpServers) .cursor/mcp.json Cursor (mcpServers) .vscode/mcp.json VS Code (servers + type: stdio) .vscode/poetry.code-snippets one snippet per poetry_* helper, with the real variant/size enums as tab-stop choices .herb.yml Herb (linter / formatter / language server), unless the app already has one .stimulus-lsp/config.json poetry's controller identifiers for Stimulus LSP (upsert: an existing ignore list keeps its entries)

The MCP writes are upserts: an existing config keeps its other servers and gains a poetry entry; a config that already has one is left untouched; a JSONC file poetry can't parse is reported, never clobbered. Editors whose MCP config is global / IDE-managed (Zed, Windsurf, RubyMine) get a copy-paste block printed instead.

bin/rails g poetry:editor
Constant Description
CLAUDE_HOOK The Claude Code entry: PostToolUse on the file-editing tools.
CURSOR_HOOK Cursor's Write is its file-edit tool type; the payload is read leniently and the hook stays silent when it finds no path.
HERB_CONFIG The Herb toolchain (HTML+ERB parser, linter, formatter, language server) reads one file. Poetry templates are gated to parse AND compile under Herb (Rails core's ERB engine since 8.2), so a Poetry app can run the whole toolchain; two linter rules misread ViewComponent slot setters on helper-yielded builders today, so they start off with the upstream issues to watch. The version pin keeps a linter upgrade from enabling rules silently.
HERB_EDITORS Where the Herb language server comes from, per editor.
HOOK_NOTE What the generator says about the hook once it is written.
HOOK_SCRIPT The check hook: stdlib Ruby that runs poetry check on the one app template an agent just edited and feeds the findings back (errors as a blocking report, warnings as context). Registered for Claude Code and Cursor; the script filters to app templates itself, since neither hook's matcher can see a file path.
MANUAL_EDITORS Editors whose MCP config is global or IDE-managed, so poetry prints a paste-block instead of writing a project file.
STIMULUS_LSP_VERSION Stimulus LSP resolves identifiers from the app's controller directories and node_modules; controllers a gem serves through importmap are invisible to it, so every hand-written poetry--... descriptor reads as an invalid controller with a quick-fix that scaffolds a bogus file. The LSP's one lever is an ignore list of exact identifiers, so poetry lists every controller its installed gems ship. Those descriptors are poetry:check's to validate (kwargs included); the LSP keeps yours.

Poetry::Generators

module · lib/generators/poetry/agents_section.rb

Namespace for poetry's Rails generators and their shared step modules.

Poetry::Generators::PaginationGenerator

class < Rails::Generators::Base · lib/generators/poetry/pagination/pagination_generator.rb

rails g poetry:pagination [kaminari|pagy|will_paginate] - copies a poetry adapter for the host's paginator(s) as OWNED host-app code (the copied-source doctrine; poetry:diff tracks drift). No argument: every paginator loaded in this app gets its adapter.

The doctrine baked into every adapter: poetry owns the window (siblings/edges compute the visible pages) - the paginator's own window options deliberately do not apply, so pagination looks the same whichever gem drives it.

bin/rails g poetry:pagination pagy
Constant Description
ADAPTERS Per-paginator adapter wiring: how to detect the gem, which template to copy, and where it lands.

Poetry::InstallGenerator

class < Rails::Generators::Base · lib/generators/poetry/install/install_generator.rb

rails g poetry:install - wires poetry into a host app:

app/assets/tailwind/poetry/tokens.css the design tokens (:root + .dark) app/assets/tailwind/poetry/theme.css the Tailwind v4 @theme mapping app/assets/tailwind/poetry/animate.css the vendored animation utility layer (Rails hosts have no npm) app/assets/tailwind/poetry/safelist.txt every dictionary + template class (so the host build never purges classes resolved in Ruby) config/initializers/poetry.rb commented configuration config/poetry_components.yml the copy-in manifest (empty)

plus idempotent @import/@source injection into the host's Tailwind entry (append-unless-present - re-running install is always safe).

--charts additionally wires poetry-charts (the gem must already be in the bundle - its engine merges the @poetry/charts importmap pins and the safelist pass picks the chart dictionaries up on its own; what a host still needs by hand is the motion stylesheet in the Tailwind entry and the Stimulus registration, and that is exactly what the flag does).

bin/rails g poetry:install --theme vega
Constant Description
BASE_CSS The base layer (upstream's init writes the same defaults into the host stylesheet): without it body/border/outline don't ride the tokens and dark mode only flips the components. Seeded once, user-owned - re-install never overwrites.
ENTRY_LINES Injected line by line (not as one block) so a re-run after an upgrade appends any line a previous poetry version didn't know about.
STYLE_SLOT The theme slot: --theme swaps its CONTENT, the filename stays put.
TAILWIND_ENTRY The host's Tailwind entry stylesheet (import/@source injection).
THEME_HEADER Every shipped fragment opens with /* poetry <name> theme - the sniff reads the slot's first line back so a plain re-run keeps the theme.

Poetry::ScaffoldTemplatesGenerator

class < Rails::Generators::Base · lib/generators/poetry/scaffold_templates/scaffold_templates_generator.rb

rails g poetry:scaffold_templates: Rails has always let an app override its generator templates from lib/templates/; what was missing was a set that renders with poetry. This copies scaffold view templates - and a matching scaffold controller template - so the STANDARD rails g scaffold produces poetry-composed output: a DataTable index with sanitized URL state (sortable whitelist, filter, pagination), Field-composed forms with attribute-type -> component mapping (plus column-name -> input-type heuristics and null: false -> required), a MetadataList show, and a destructive-variant delete. The copies are the app's to edit; re-runs never overwrite (skip-if-exists, the poetry:add contract).

bin/rails g poetry:scaffold_templates
Constant Description
SKIP_CONTROLLER_DESC The --skip-controller option description (multi-line).
VIEW_TEMPLATES The scaffold view templates the override set ships.

Poetry::SkillGenerator

class < Rails::Generators::Base · lib/generators/poetry/skill/skill_generator.rb

rails g poetry:skill - installs or refreshes poetry's Claude Code skills in the app's .claude/skills/:

poetry/ the component-usage skill, GENERATED from the live registry (SKILL.md menu + per-family references) - re-run this generator after updating poetry gems poetry-design/ the taste layer (theme / compose / audit / study), curated prose riding the DESIGN.md + design-lint rails poetry-component/ the authoring layer (anatomy / documentation / audit) for components the app builds itself

poetry:install runs the same step; this standalone exists as the refresh path (and for hosts that installed before the skills shipped).

bin/rails g poetry:skill

Poetry::Ui

module · lib/poetry/ui.rb

The component library: ViewComponents tracking the full ported component vocabulary, built entirely on poetry-core's PUBLIC DSL - if a component here needs private core API, that is a core API gap, not a license to reach in.

Constant Description
BLOCKS_DIR Where the block templates live: the generator's source tree, scanned by the registry builder, the template-class extraction, the dummy's /blocks previews, and the MCP server's describe_block.
COMPONENT_DEPENDENCIES Curated composition edges: which components a copy-in of X also needs locally. Single-sourced here - the add generator's recursive copy AND every registry item's registryDependencies read this one map. (Replaced by registry-carried anatomy when the contract's anatomy section lands.)
POSITIONAL_PARAM_KINDS The parameter kinds that count as positional in a signature.
SKILL_FAMILIES The usage skill's family partition: every component in exactly one reference file, so the skill's menu stays lean and an agent loads only the family it is composing in. The coverage gate fails on any new component until it is mapped here.
TEMPLATE_CLASSES_PATH The committed static-template-class list (herb-extracted in poetry's CI, drift-gated) - poetry:install reads it so hosts never need herb.
VERSION The gem version.

.agent_skills

The MCP server's runtime skill map (get_skill): the SAME files rails g poetry:skill writes, for hosts that cannot write files. Boot-free by construction - the usage skill regenerates from the COMMITTED registries (never the booted builders above), the design skill reads its static templates - so the exe can serve both without Rails. Lazy: nothing generates until an agent asks.

.block_components(source, titles:)

The distinct components a block composes, folded from its poetry_* helper calls: each call maps to the LONGEST component title that prefixes it (poetry_sidebar_menu_badge -> sidebar; poetry_table_head -> table); calls matching no component (pure wrapper helpers like poetry_input_group_addon fold through input_group) are dropped rather than guessed.

.charts_registry

Tolerant on charts, like the AGENTS.md census: a host without the gem (or with a stubbed/partial one) just drops the charts reference.

.committed_charts_registry

The charts reference rides along whenever the charts gem is present (its committed registry, not its booted builder); absent, the skill simply drops it - the charts_registry tolerance, boot-free.

.helper_names

The public poetry_* helper names, so poetry check / poetry-agent know the full set (group / provider helpers AND the define_method'd part helpers like poetry_table_cell) WITHOUT booting Rails. The module's method bodies reference component constants only at call time, so requiring the file standalone is safe.

.recipe_items

The recipes projection: skill bundles, scaffold template sets, and screen slices as registry items - served at /r/*.json beside components and blocks, installed by poetry:add or any client speaking the same item schema.

.registry

The registry builder this gem commits from (booted contexts only: rake registry:generate/verify and the sync test share it, so the drift gate always compares against the exact construction that generated the file - helpers AND blocks sections included).

.registry_blocks(component_paths:)

The registry "blocks" section: every block template's metadata, all source-derived - title/description/keywords from the mandatory poetry:block header (keywords power the MCP compose tool's brief routing), the composed component list from the template's own poetry_* calls (longest-prefix fold: sidebar_menu_button counts as sidebar), the gem-relative template path for boot-free source reads. No hand-authored catalog to drift.

.registry_descriptions

The editorial per-component descriptions merged into the registry (component_path => one-liner, from config/component_descriptions.yml). Absent file -> nil, so a registry without it stays lint-identical, like every other optional section.

.registry_helpers(component_paths:)

The registry "helpers" section: every poetry_* helper that maps to no component - group/provider/item wrappers - each carrying its declared value contract (ComponentsHelper::HELPER_CONTRACTS) or {} for a plain wrapper.

.registry_items

The interop item projection - registry items in the ecosystem's shared JSON item schema, boot-free from the COMMITTED registry - the docs site serves /r/*.json from this, and the add generator matches gem-satisfied dependencies against its names.

.root

Gem root (the directory containing lib/, app/, config/).

.skill_files

The installable component-usage skill: a lean SKILL.md menu + per-family references, generated from the live registry - the seam the poetry:skill generator and the eval harness share (the agents_section_text pattern).

.template_classes

Returns (Array<String>) — the committed template-static classes

Poetry::Ui::Accordion

module · app/components/poetry/ui/accordion/component.rb

Vertically stacked expand/collapse sections.

Poetry::Ui::Alert

module · app/components/poetry/ui/alert/component.rb

Inline callouts for statuses and errors.

Poetry::Ui::AlertDialog

module · app/components/poetry/ui/alert_dialog/component.rb

Modal confirmations that must be answered.

Poetry::Ui::AspectRatio

module · app/components/poetry/ui/aspect_ratio/component.rb

Ratio-locked media containers.

Poetry::Ui::Attachment

module · app/components/poetry/ui/attachment/component.rb

File/image upload chips.

Poetry::Ui::Autocomplete

module · app/components/poetry/ui/autocomplete/component.rb

Free-text inputs with filtering suggestion popups.

Poetry::Ui::Avatar

module · app/components/poetry/ui/avatar/component.rb

Person images with initials fallbacks.

Poetry::Ui::Badge

module · app/components/poetry/ui/badge/component.rb

Non-interactive status pills.

Poetry::Ui::Breadcrumb

module · app/components/poetry/ui/breadcrumb/component.rb

Ancestor-trail navigation.

Poetry::Ui::Bubble

module · app/components/poetry/ui/bubble/component.rb

Chat message bubbles.

Poetry::Ui::Button

module · app/components/poetry/ui/button/component.rb

Action buttons and button-styled links.

Poetry::Ui::ButtonGroup

module · app/components/poetry/ui/button_group/component.rb

Segmented groups of adjacent controls.

Poetry::Ui::Calendar

module · app/components/poetry/ui/calendar/component.rb

Server-rendered month-grid date pickers.

Poetry::Ui::Card

module · app/components/poetry/ui/card/component.rb

Content surface cards.

module · app/components/poetry/ui/carousel/component.rb

Carousel family: the scroll-snap slide strip and its paging controls.

Poetry::Ui::Checkbox

module · app/components/poetry/ui/checkbox/component.rb

Checkbox family: the form-participating tri-state toggle.

Poetry::Ui::ClipboardText

module · app/components/poetry/ui/clipboard_text/component.rb

ClipboardText family: a read-only value with one copy affordance.

Poetry::Ui::CodeBlock

module · app/components/poetry/ui/code_block/component.rb

CodeBlock family: the server-highlighted code panel.

Poetry::Ui::CodeBlockHighlighter

module · lib/poetry/ui/code_block_highlighter.rb

The CodeBlock's rouge seam: highlighting is a SOFT capability - gem "rouge" in the host Gemfile turns it on; without it the component renders the plain escaped code unchanged (the fallback is simply "no rouge", and nothing shifts because the markup shape is identical).

.available?

Whether rouge is loadable in this host (memoized).

.formatter

Rouge 5 asserts a PLAIN HTML delegate inside its line-wise wrappers (HTMLLinewise cannot wrap HTMLLineHighlighter), so poetry carries its own line formatter over the stable token_lines API.

.highlight(code, language:, highlight_lines: [])

-> html_safe highlighted markup - every line wrapped in .line (the counter hook), the requested ones also .hll (the theme's tint hook) - or nil when rouge is absent. Unknown languages lex as plain text.

Poetry::Ui::Collapsible

module · app/components/poetry/ui/collapsible/component.rb

Collapsible family: the plain show/hide disclosure.

Poetry::Ui::Combobox

module · app/components/poetry/ui/combobox/component.rb

Combobox family: the searchable select - a trigger, a filterable listbox popup, and a native-select form story.

Constant Description
ALIGNS The closed vocabulary for the align axis.
DIRS The closed vocabulary for the reading-direction axis.
SIDES The placement vocabularies, declared ONCE at module level - shared by the validations and the part-state declarations.

Poetry::Ui::Command

module · app/components/poetry/ui/command/component.rb

Command family: the filterable command palette and its dialog form.

Poetry::Ui::ContextMenu

module · app/components/poetry/ui/context_menu/component.rb

ContextMenu family: the right-click/long-press menu.

Constant Description
DIRS Shared vocabularies, declared once at module level so the root Component and the nested menu-level classes read the same lists.
SIDES The closed vocabulary for the side axis.

Poetry::Ui::DataTable

module · app/components/poetry/ui/data_table/component.rb

DataTable family: the sortable, filterable, paginated table plus its URL-state object.

Poetry::Ui::DateField

module · app/components/poetry/ui/date_field/component.rb

DateField family: the segmented date editor over a native input.

Poetry::Ui::DatePicker

module · app/components/poetry/ui/date_picker/component.rb

DatePicker family: a field-shaped trigger opening a calendar popover.

Poetry::Ui::DateTimeField

module · app/components/poetry/ui/date_time_field/component.rb

Segmented date-and-time editors.

Poetry::Ui::Deferred

module · app/components/poetry/ui/deferred/component.rb

Deferred family: lazy-loaded regions with loading and error states.

Poetry::Ui::Dialog

module · app/components/poetry/ui/dialog/component.rb

Dialog family: the modal overlay on the native <dialog> element.

Poetry::Ui::DictionaryFidelity

module · lib/poetry/ui/dictionary_fidelity.rb

The dictionary half of the fidelity ledger: the structural utilities a Style dictionary puts on each part, held against the classNames the source puts on the same data-slot at the pin.

The theme ledger (ThemeFidelity) proves the cn-* rules; nothing proved the Ruby strings that ride the same elements - a token there wins over every theme's rule at once, which is how a classic-only sizing chain and a demo width overrode nine themes without a failing gate.

Two sources are snapshotted per slot: the styled registry (bases/base/ui, hooks plus structural utilities) is the contract the dictionaries must match; the classic registry (new-york-v4/ui, every visual utility inline) tells a classic-only token apart from a poetry-only one. Hooks (cn-*) are the theme ledger's concern and are left out of both sides.

Rendered previews supply poetry's side: for every element a component owns (PartContract's ownership rule), the class tokens that belong to the dictionary. Reconciliation against deviations.yml is exact and two-way, like the theme ledger: an unrecorded difference fails, a recorded one that no longer exists fails as stale.

Constant Description
CLASSIC The classic registry (every visual utility inline) - tells classic-only tokens apart.
CLOSERS Their closing counterparts.
COMPONENT_LISTS Top-level presence lists: source files without a poetry component and vice versa.
COMPOSED A composed part: rendered by another component (a Button), whose own ledger covers its tokens.
DIFF_KINDS Per-slot token diff kinds the ledger records.
DIR The ledger's home: the snapshot and deviations.yml.
HOOK A theme hook (the theme ledger's concern; excluded from both sides).
LIST_KINDS Per-file presence lists the ledger records with a reason.
OPENERS Bracket characters the useRender walker balances.
QUOTES String delimiters the tag scanner steps over.
STYLED The styled registry (hooks plus structural utilities) - the contract.
TAG_START A JSX opening tag: a component or a native element name.
USE_RENDER The call whose balanced body carries a useRender root's props and state.

.current_diffs(root, rendered)

Every component's diff against the committed snapshot.

  • root (String) — the gem root
  • rendered (Hash) — component key => rendered slots (see #diff)

Returns (Hash) — component key => diff, plus the two component lists

.diff(source, rendered)

The per-component diff between the source's slots and poetry's rendered, owned slots. Rendered values are { "tag" => String, "tokens" => Set } or COMPOSED (another component's root wearing the slot - present, but its tokens are that component's ledger).

  • source (Hash) — slot => { "tag", "styled", "classic" }
  • rendered (Hash) — slot => { "tag", "tokens" } | COMPOSED

Returns (Hash) — "missing_slots", "poetry_slots", "slots" => { slot => diff }

.extract(text)

Every element carrying data-slot in one source file, with the structural tokens its className resolves to (string literals, cva base and variant strings, render-prop classNames) and its tag. Roots rendered through useRender are read from their props object.

  • text (String) — a registry .tsx file

Returns (Hash{String => Hash}) — slot => { "tag" => String, "tokens" => Array<String> }

.snapshot(root)

The committed snapshot.

  • root (String) — the gem root

.snapshot_path(root)

The committed snapshot's path - exactly one may exist.

  • root (String) — the gem root

.verify(root, rendered)

Exact two-way reconciliation against deviations.yml. Returns a list of finding strings; empty means the contract holds.

  • root (String) — the gem root
  • rendered (Hash) — component key => rendered slots (see #diff)

.write_snapshot(root, checkout:, pin:)

Writes the frozen source snapshot from a pinned checkout: every styled registry file's slots, with the classic registry's tokens for the same slots beside them.

  • root (String) — the gem root
  • checkout (String) — path to the source checkout
  • pin (String) — the commit to read

Returns (String) — the snapshot path

Poetry::Ui::Drawer

module · app/components/poetry/ui/drawer/component.rb

Drawer family: the swipeable edge sheet on the dialog spine.

Poetry::Ui::DropdownMenu

module · app/components/poetry/ui/dropdown_menu/component.rb

The DropdownMenu family - the button-triggered action menu.

Constant Description
ALIGNS The closed vocabulary for the align placement axis.
DIRS The closed vocabulary for the dir (reading direction) axis.
SIDES The closed vocabulary for the side placement axis.

Poetry::Ui::Empty

module · app/components/poetry/ui/empty/component.rb

The Empty family - the empty-state block for collections with nothing in them.

Poetry::Ui::Engine

class < Rails::Engine · lib/poetry/ui/engine.rb

The Rails engine: wires the component classes, the poetry_* view helpers, the Turbo Stream toast action, and the preview paths into the host app.

Poetry::Ui::Field

module · app/components/poetry/ui/field/component.rb

The Field family - the label/control/hint/error wrapper for one form control.

Poetry::Ui::FieldGroup

module · app/components/poetry/ui/field_group/component.rb

The FieldGroup family - the stacking container for Fields.

Poetry::Ui::FieldSeparator

module · app/components/poetry/ui/field_separator/component.rb

The FieldSeparator family - the divider row between stacked fields.

Poetry::Ui::Fieldset

module · app/components/poetry/ui/fieldset/component.rb

The Fieldset family - a named group of related fields.

Poetry::Ui::FileInput

module · app/components/poetry/ui/file_input/component.rb

The FileInput family - file selection as a plain control or a dropzone.

Poetry::Ui::HoverCard

module · app/components/poetry/ui/hover_card/component.rb

The HoverCard family - the pointer-hover link preview.

Constant Description
ALIGNS The closed vocabulary for the align axis.
SIDES The placement vocabularies - shared with every popup surface.

Poetry::Ui::Icon

module · app/components/poetry/ui/icon/component.rb

The Icon family - inline SVG icons from the configured icon set.

Poetry::Ui::Input

module · app/components/poetry/ui/input/component.rb

The Input family - the single-line native text control.

Poetry::Ui::InputGroup

module · app/components/poetry/ui/input_group/component.rb

The InputGroup family - one bordered field surface for a control plus addons.

Poetry::Ui::InputOtp

module · app/components/poetry/ui/input_otp/component.rb

The InputOtp family - fixed-length one-time-code entry.

Poetry::Ui::Item

module · app/components/poetry/ui/item/component.rb

The Item family - the generic list row.

Poetry::Ui::Kbd

module · app/components/poetry/ui/kbd/component.rb

The Kbd family - the keyboard-key chip.

Poetry::Ui::Label

module · app/components/poetry/ui/label/component.rb

A caption for a form control.

module · app/components/poetry/ui/link/component.rb

A navigation link.

Poetry::Ui::LlmsController

class < ActionController::Base · app/controllers/poetry/ui/llms_controller.rb

Serves llms.txt / llms-full.txt generated live from the component registry - the docs an LLM retrieves can never drift from the code.

#full

GET /poetry/llms-full.txt - the full per-component reference.

#index

GET /poetry/llms.txt - the catalog index.

Poetry::Ui::Marker

module · app/components/poetry/ui/marker/component.rb

A transcript divider or inline status line for chat UIs.

Poetry::Ui::Menubar

module · app/components/poetry/ui/menubar/component.rb

A desktop-style menu bar of drop-down menus.

Constant Description
DIRS Shared vocabularies, declared once at module level so the root Component and the nested menu-level classes read the same lists.

Poetry::Ui::Message

module · app/components/poetry/ui/message/component.rb

One chat turn's row in a conversation transcript.

Poetry::Ui::MessageScroller

module · app/components/poetry/ui/message_scroller/component.rb

A streaming-aware chat transcript scroller.

Poetry::Ui::MetadataList

module · app/components/poetry/ui/metadata_list/component.rb

Labeled facts about one record, as a description list.

Poetry::Ui::Meter

module · app/components/poetry/ui/meter/component.rb

A quantity within a known range.

Poetry::Ui::NativeSelect

module · app/components/poetry/ui/native_select/component.rb

A styled native <select>.

Poetry::Ui::NavigationMenu

module · app/components/poetry/ui/navigation_menu/component.rb

A site-navigation bar with disclosure panels.

Poetry::Ui::NumberField

module · app/components/poetry/ui/number_field/component.rb

A numeric input with steppers and display formatting.

Poetry::Ui::OptimisticFormBuilder

class < ActionView::Helpers::FormBuilder · app/helpers/poetry/ui/optimistic_form_builder.rb

The optimistic-form builder (the hotwire_club-toolbox port): renders the scaffolding poetry_optimistic_form's controller consumes - <template> targets holding the PREDICTED state as turbo-stream(s) (the same vocabulary the server answers in, so prediction and truth never need bespoke DOM patching), and the hidden field carrying the submitted value.

Constant Description
UNSET Distinguishes "no value given" from a real nil/false - a favorite toggle legitimately submits false, so false must survive.

#optimistic_hidden_field(attribute_name, value:)

Explicit placement of the submitted-value field; calling it suppresses the helper's automatic injection.

#optimistic_hidden_field_rendered?

#optimistic_template(target = nil, template = nil, &block)

The predicted state, cloned into the DOM on submit. Positional form wraps a turbo_stream update for you; block form authors the stream(s) directly (several regions, other actions):

form.optimistic_template dom_id(photo, "fav"), icon(!photo.favorite) form.optimistic_template { turbo_stream.update("cart-count") { @count + 1 } }

Poetry::Ui::Pagination

module · app/components/poetry/ui/pagination/component.rb

Numbered page navigation.

Poetry::Ui::Popover

module · app/components/poetry/ui/popover/component.rb

A click-opened panel anchored to its trigger.

Constant Description
ALIGNS The closed vocabulary for the align axis.
SIDES The placement vocabularies - the popper-consumer kit owns them.

Poetry::Ui::Progress

module · app/components/poetry/ui/progress/component.rb

A determinate progress bar.

Poetry::Ui::Questionnaire

module · app/components/poetry/ui/questionnaire/component.rb

A one-question-at-a-time survey form.

Poetry::Ui::RadioGroup

module · app/components/poetry/ui/radio_group/component.rb

A mutually exclusive option set: radio dots or selectable choice cards.

Poetry::Ui::Recipes

module · lib/poetry/ui/recipes.rb

The recipes channel: multi-file payloads served through the registry beside components and blocks. Every definition's files are LAZY callables over gem-shipped sources - the same files the generators install - so the projection cannot drift from the generator path. Names share the flat kebab namespace (RegistryIndex collision-checks recipes against components and blocks at first touch).

Constant Description
SCAFFOLD_TEMPLATES Gem-relative path of the scaffold override template set.
SCREEN_SOURCES Gem-relative path holding the screen-slice and embed sources.

.definitions

Every recipe definition, in registry order: the agent embed, the three skill bundles, the scaffold-template set, and the screen slices.

Returns (Array<Hash>) — name/title/description + lazy "files"

Poetry::Ui::Resizable

module · app/components/poetry/ui/resizable/component.rb

A panel group divided by draggable splitter handles.

Poetry::Ui::ScrollArea

module · app/components/poetry/ui/scroll_area/component.rb

A bounded scroll region with themed scrollbars.

Poetry::Ui::SearchField

module · app/components/poetry/ui/search_field/component.rb

A search input with a leading glyph and a clear affordance.

Poetry::Ui::Select

module · app/components/poetry/ui/select/component.rb

A single-select dropdown field: a combobox trigger opening a popup listbox.

Constant Description
ALIGNS The closed vocabulary for the popup alignment axis.
DIRS The closed vocabulary for the text-direction axis.
SIDES The closed vocabulary for the popup placement-side axis.
SIZES The closed vocabulary for the trigger size axis.

Poetry::Ui::SensitiveInput

module · app/components/poetry/ui/sensitive_input/component.rb

A secret field that stays masked until deliberately revealed.

Poetry::Ui::Separator

module · app/components/poetry/ui/separator/component.rb

A thin divider line between content regions.

Poetry::Ui::Sheet

module · app/components/poetry/ui/sheet/component.rb

A modal panel that slides in from a screen edge.

Poetry::Ui::Sidebar

module · app/components/poetry/ui/sidebar/component.rb

The app-shell frame: collapsible navigation column plus content inset.

Poetry::Ui::Skeleton

module · app/components/poetry/ui/skeleton/component.rb

A pulsing placeholder box shown while content loads.

Poetry::Ui::Slider

module · app/components/poetry/ui/slider/component.rb

A draggable numeric value (or range) on a continuous track.

Poetry::Ui::Spinner

module · app/components/poetry/ui/spinner/component.rb

A spinning loader glyph that announces itself to assistive tech.

Poetry::Ui::Stat

module · app/components/poetry/ui/stat/component.rb

One KPI: a labelled value with an optional sentiment-aware delta.

Poetry::Ui::Switch

module · app/components/poetry/ui/switch/component.rb

Instant-effect on/off controls.

Poetry::Ui::Table

module · app/components/poetry/ui/table/component.rb

Semantic data tables composed from part helpers.

Poetry::Ui::Tabs

module · app/components/poetry/ui/tabs/component.rb

Tabbed views switched by a tablist.

Poetry::Ui::TagGroup

module · app/components/poetry/ui/tag_group/component.rb

Removable-chip collections.

Poetry::Ui::Textarea

module · app/components/poetry/ui/textarea/component.rb

Multiline free-text inputs.

Poetry::Ui::ThemeFidelity

module · lib/poetry/ui/theme_fidelity.rb

The theme transcription-fidelity contract: every way a ported theme's cn-* rules differ from their source at the port pin is recorded - with a reason - in config/theme_fidelity/deviations.yml, and the gate (css:verify_fidelity) holds the two in exact two-way agreement:

  • a theme edit that changes the diff fails until the deviation is

recorded (no undocumented drift lands), and

  • a recorded deviation that no longer exists fails as stale (a

receipt cannot outlive the code it describes).

The source side is a frozen parse snapshot (config/theme_fidelity/upstream-<pin>.json) generated once from the pinned checkout via css:fidelity_snapshot - the pin never moves under a release, so the snapshot is immutable. Bumping the pin is a deliberate ceremony: regenerate the snapshot, re-review the new diff, re-reason the deviations file.

Constant Description
DIFF_KINDS The per-rule diff sides.
DIR Where the snapshot and the deviations contract live.
SELECTOR_KINDS The selector-presence diff sides.
THEMES The eight ported themes (default is the reference, not a port).

.current_diffs(root)

Every theme's diff against the committed snapshot.

.diff(upstream, poetry)

The per-theme diff between a source parse and a poetry parse: selectors only one side has, and per shared selector the dropped (source-only) and added (poetry-only) utilities and raw declarations. Selectors with no difference are omitted.

.parse_css(text)

Parses a theme stylesheet into { selector => { "apply" => [utils], "raw" => [declarations] } }. Handles both shapes: poetry's flat themes/<t>.css and the source's .style-<t> { ... } wrapper. Only cn-* selectors participate; comments are stripped; a .dark ancestor scopes the key with a "[dark] " prefix so both sides stay keyed identically.

.snapshot_path(root)

The committed snapshot's path - exactly one may exist.

.verify(root)

Exact two-way reconciliation against deviations.yml. Returns a list of finding strings; empty means the contract holds.

.write_snapshot(root, checkout:, pin:)

Writes the frozen source snapshot from a pinned checkout - the pin-bump ceremony's first step.

Poetry::Ui::Themes

module · lib/poetry/ui/themes.rb

The theme roster + per-theme DESIGN.md metadata.

Every poetry theme shares ONE token source (poetry-core's DTCG file) - a theme is a component-treatment layer (themes/<name>.css), never a palette. What varies per theme, and therefore what this module knows: the treatment provenance line and the typography PAIRING - which is app-level metadata, not CSS (no poetry theme moves a font token; upstream's create flow biases lyra to JetBrains Mono with radius none and pairs sera with Noto Serif / Instrument Serif; the poetry docs render system stacks keyed off the same story).

Constant Description
CUSTOM_DETAILS A host whose installed theme slot matches no shipped fragment (hand-customized bytes) still exports honestly.
DETAILS Per-theme DESIGN.md metadata: the typography pairing and the treatment provenance line.
MONO The system mono stack mono-biased themes report.
PORTED The provenance line template for ported treatments, format-interpolated with the upstream style name.
SANS The system sans stack sans-paired themes report.
SERIF The system serif stack serif-paired themes report.

.design_md_exports(components_count:, generator: "bin/rake design:export_all")

theme name -> serialized DESIGN.md, for every shipped theme - the single builder rake design:export_all, the drift gate, and the tests all share.

.details(name)

The DESIGN.md metadata for one theme; unknown names (customized host bytes) fall back to CUSTOM_DETAILS.

  • name (String) — the theme name

Returns (Hash) — "typography" pairing + "treatment" provenance

.names

The shipped roster, from the fragment files themselves.

Returns (Array<String>) — theme names, sorted

Poetry::Ui::TimeField

module · app/components/poetry/ui/time_field/component.rb

Segmented time-of-day editors.

Poetry::Ui::Timeline

module · app/components/poetry/ui/timeline/component.rb

Dated event sequences as ordered lists.

Poetry::Ui::Toast

module · app/components/poetry/ui/toast/component.rb

Transient notifications.

Constant Description
POLITENESS The closed vocabulary for announcement politeness.
VARIANTS The closed vocabulary for the variant axis.

Poetry::Ui::ToastStreamActions

module · app/helpers/poetry/ui/toast_stream_actions.rb

The canonical server-side toast (the Toast contract's Rails-native path):

turbo_stream.poetry_toast(title: "Saved", variant: :success) turbo_stream.poetry_toast(title: "Deleted") { |toast| toast.with_action { "Undo" } }

An append into the data-turbo-permanent #poetry-toaster region - usable from controller responses, form streams, and Turbo::StreamsChannel.broadcast_append_to in jobs. Mixed into Turbo::Streams::TagBuilder via the :turbo_streams_tag_builder load hook (poetry-ui does not depend on turbo-rails; hosts that have it get the action automatically - see the engine initializer).

#poetry_toast(title:, description: nil, target: Poetry::Ui::Toaster::Component::DEFAULT_ID, **)

Poetry::Ui::ToastTrigger

module · app/components/poetry/ui/toast_trigger/component.rb

Client-side toast delivery.

Poetry::Ui::Toaster

module · app/components/poetry/ui/toaster/component.rb

The toast viewport.

Constant Description
POSITIONS The closed vocabulary for the corner axis.

Poetry::Ui::Toggle

module · app/components/poetry/ui/toggle/component.rb

Pressed-state buttons.

Poetry::Ui::ToggleGroup

module · app/components/poetry/ui/toggle_group/component.rb

Exclusive or multi-select toggle sets.

Poetry::Ui::Toolbar

module · app/components/poetry/ui/toolbar/component.rb

Single-Tab-stop control strips.

Poetry::Ui::Tooltip

module · app/components/poetry/ui/tooltip/component.rb

Hover/focus text hints.

Constant Description
ALIGNS The closed vocabulary for the align axis.
SIDES The placement vocabularies - the popper-consumer kit owns them.

Poetry::Ui::Tree

module · app/components/poetry/ui/tree/component.rb

Hierarchical expandable lists.

Poetry::Ui::Typeset

module · app/components/poetry/ui/typeset/component.rb

Prose containers for rendered markdown.

Poetry::Ui::Webmcp

module · lib/poetry/ui/webmcp.rb

The declarative WebMCP form contract: the attribute vocabulary Chrome's declarative API registers straight from markup - a <form> carrying toolname + tooldescription IS an agent-callable tool, its controls are the parameters (the browser synthesizes the JSON Schema from control types, required, and select options, reading each parameter's description from the associated <label>), and toolparamdescription overrides a label where the label alone is not agent-sufficient. No JavaScript, inert markup without an agent.

Safety by construction: toolautosubmit (the agent submits without the user pressing Submit) is only allowed on GET forms - read-only lookups. A mutating form keeps the user in the loop, always.

Consumers: {ComponentsHelper#poetry_webmcp_form} (the form), the FormBuilder's tool_description: field option (the override), and poetry-agent's imperative runtime, which shares this vocabulary.

Constant Description
DESCRIPTION_LIMIT The agent-legibility budget for descriptions.
FORM_CONTROLLER poetry-agent's declarative-form controller (the respondWith path); emitted as a plain token so the markup stays inert without the gem.
TOOL_KEYS The keys a tool: declaration may carry.
TOOL_NAME_PATTERN The WebMCP tool-name grammar: 1-128 ASCII alphanumerics, _, -, . (declarative forms register their full name directly).

.form_attributes(tool, method: nil)

The <form> attributes for a declarative tool.

  • tool (Hash)name: (tool-name grammar), description: (present, at most {DESCRIPTION_LIMIT} chars), optional autosubmit: true
  • method (Symbol, String, nil) — the form's HTTP method - autosubmit requires :get

Returns (Hash{Symbol => String})toolname, tooldescription, and toolautosubmit (empty-valued boolean attribute) when set

Poetry::Ui::Webmcp.form_attributes({ name: "find_order", description: "Find an order." }, method: :get)
# => { toolname: "find_order", tooldescription: "Find an order." }

.param_attributes(description)

The control attribute for a parameter-description override.

  • description (String) — the parameter's description for agents

Returns (Hash{Symbol => String})toolparamdescription