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-core API

The engine: the Component base class, the option / style / part / use_stimulus DSLs, the style dictionary, tokens, icons, and the registry.

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

Poetry::Core::Component

class < ViewComponent::Base · app/components/poetry/core/component.rb

Base component class for all Poetry components.

This class serves as the foundation for all Poetry components, providing:

  • ActiveModel integration for attributes, assignment, and validations
  • Style attribute management with variants and proc defaults
  • HTML attribute handling and merging
  • Translation and wrapping helpers
  • Classname merging functionality
  • Component metadata and identification methods
class MyComponent < Poetry::Core::Component
  style :color, default: :primary, variants: [:primary, :secondary]
  style :size, default: :md, variants: [:sm, :md, :lg]
end

component = MyComponent.new(color: :secondary, size: :lg, class: "custom-class")
component.color         # => :secondary
component.size          # => :lg
component.html_attributes # => { class: "... custom-class" }
class Badge < Poetry::Core::Component
  style :color, default: :gray, variants: [:gray, :red, :blue]
  style :dot_color, default: -> { color }, variants: [:gray, :red, :blue]
end

badge = Badge.new(color: :red)
badge.dot_color  # => :red (inherits from color)
Constant Description
BASE_COMPONENT_CLASS Where ancestor walks stop: declarations above the base component class are nobody's.
DESCRIPTION_LIMIT The agent-legibility budget for a tool description.
DYNAMIC_DEFAULT The registry's marker for a proc default (its value depends on other attributes and is unknowable statically).
KEYWORD_PARAMETER_KINDS The parameter kinds that ARE the keyword surface.
NAME_LIMIT The house character budgets: names stay far under the spec's 128-char full-name cap (instance prefixes join later); descriptions stay inside the agent-legibility budget.
NAME_PATTERN Tool and param names stay inside the WebMCP tool-name grammar (ASCII alphanumerics, _, -, .) with poetry's stricter snake_case convention, so a composed full name (instance prefix + tool name) can never need escaping.
OPEN_PARAMETER_KINDS Parameter kinds that make a keyword surface open or unknowable (**rest accepts anything; a positional can swallow a braceless hash), and the kinds that ARE the keyword surface.
PARAM_DESCRIPTION_LIMIT The agent-legibility budget for one parameter's description (Chrome's guidance: 150 characters per parameter description).
PART_NAME The kebab-case shape a declared part name (data-slot value) must match.
POSITIONAL_KINDS The parameter kinds counted toward positional arity.
STATE_ATTRIBUTE The data-* shape a declared state attribute must match.
STYLE_CLASS_SUFFIX The naming convention joining a component to its sidecar style dictionary (Dot::Component -> Dot::Style).
VAR_NAME A trailing * declares a dynamic family (charts' per-series --color-*), matched by prefix at verify time.
WEBMCP_CONTROLLER The registrar controller (poetry-agent's) that a webmcp: root registers; its manifest entry joins core's catalog when the gem loads, so the wiring validates like any other controller.
WEBMCP_DEFAULT_BUDGET The registrar's built-in per-document registration budget; the root carries an explicit budget only when the configured value ({Poetry::Core::Config#webmcp_registration_budget}) differs.

.component_identifier

Returns the component identifier with path segments joined by double dashes. Useful for CSS class names and HTML data attributes.

Returns (String) — the component identifier (e.g., "poetry--core--dot")

Poetry::Core::Dot::Component.component_identifier # => "poetry--core--dot"
Poetry::Core::Button::Component.component_identifier # => "poetry--core--button"

.component_module

Returns the component module name without the "::Component" suffix.

Returns (String) — the module name (e.g., "Poetry::Core::Dot")

Poetry::Core::Dot::Component.component_module # => "Poetry::Core::Dot"

.component_path

Returns the component name in underscored path format. Removes the "::Component" or "Component" suffix and converts to snake_case.

Returns (String) — the component path (e.g., "poetry/core/dot")

Poetry::Core::Dot::Component.component_path # => "poetry/core/dot"
Poetry::Core::Button::Component.component_path # => "poetry/core/button"

.component_title

Returns the last segment of the component path as the title.

Returns (String) — the component title (e.g., "dot")

Poetry::Core::Dot::Component.component_title # => "dot"
Poetry::Core::Button::Component.component_title # => "button"

.config

The current Poetry::Core configuration - a shortcut to {Poetry::Core::Config.current} for components and their templates.

Returns (Poetry::Core::Config) — the current configuration instance

.internal_component!

Marks this class (and its descendants) as an implementation detail - full machinery, no registry entry.

class DropdownMenu::Item::Component < Poetry::Core::Component
  internal_component!
end

.required_content

The declared content-block hint, inherited like other DSL state.

.requires_content(hint)

Declares that this component cannot render without a content block, with a hint naming what the block is (Avatar: "the initials fallback"). ONE declaration feeds both enforcement layers: the component raises via #ensure_content! at render, and the registry emits requires_content so poetry check flags the omission statically.

  • hint (String) — what the content block is, for the error message
class Avatar::Component < Poetry::Core::Component
  requires_content "the initials fallback"
end

#attributes

Returns all component attributes, ensuring proc defaults are evaluated.

This method overrides ActiveModel's attributes method to trigger evaluation of any proc-based default values that haven't been explicitly set.

Returns (Hash) — the component's attributes with all defaults evaluated

#classnames(*classnames)

Merges multiple class name values into a single string.

Uses the configured classname merger (typically Tailwind Merge) to intelligently combine CSS class names, handling conflicts and duplicates.

  • classnames (Array<String, nil>) — class names to merge

Returns (String) — the merged class names

classnames("text-red-500", "text-blue-500") # => "text-blue-500"

#component_data_attributes

The self-identification markup contract, the convention every component follows: data-component on the component root maps live DOM back to the component that rendered it - the hook agents, the Verifier, and the browser-verification loop key on.

Returns (Hash) — e.g. { "data-component" => "button" }

#dom_id_token(value)

Reduces a value to a token safe for a DOM id and a CSS selector: [A-Za-z0-9_-] only. A user-controlled id would otherwise break out of the <style> block or the id attribute it is interpolated into. Returns nil when nothing safe remains (callers fall back to a random token), preserving the id attribute / JS-selector match by using the same reduced value on both sides.

  • value (Object) — the requested id

Returns (String, nil) — the safe token, or nil if empty

#ensure_content!

Enforces the class-level requires_content declaration - call from before_render. The message is built from the declaration so the runtime raise and the registry's static contract can never disagree.

#html_attributes

Returns HTML attributes with merged CSS classes.

Combines the component's CSS classes (from the css method) with any additional classes passed via the :class HTML attribute.

Returns (Hash) — HTML attributes with merged class names

component = MyComponent.new(class: "custom-class")
component.html_attributes # => { class: "component-base-class custom-class" }

#initialize(attributes = {})

Initializes a new component instance with the given attributes.

This method: 1. Initializes the registered_styles set for tracking explicitly set attributes 2. Marks style attributes with static defaults as initialized 3. Tracks which style attributes are being explicitly initialized via parameters 4. Separates component attributes from HTML attributes 5. Assigns the component attributes to their respective instance variables

The base component intentionally does not chain to ViewComponent::Base#initialize: it fully manages its own ActiveModel-backed attribute setup.

  • attributes (Hash) — the attributes to initialize the component with

Returns (Component) — a new instance of Component

component = MyComponent.new(color: :primary, size: :lg)
component = MyComponent.new(class: "my-class", data: { controller: "example" })
component = MyComponent.new(color: :primary, class: "my-class")

#persisted?

Indicates whether this component instance is persisted. Always returns false as components are not persisted entities.

Returns (Boolean) — always returns false

#poetry_instance_id(prefix)

The instance-id ladder: an explicit caller root id wins; a key: derives a stable component-namespaced token (Turbo morph pairs it across renders, cached fragments stay composable); otherwise random - unkeyed components over-replace under morph, they never falsely retain. Call sites memoize (@instance_id ||=); this stays pure.

  • prefix (String) — the component-namespaced id prefix

Returns (String) — the resolved DOM id

#script_json(json)

HTML-safe JSON for embedding in a <script type="application/json"> data island. Escapes the script-terminating characters (< > &, plus the JS line separators U+2028/U+2029) to their JSON \uXXXX forms, INDEPENDENT of the host's ActiveSupport.escape_html_entities_in_json setting: that flag defaults to true (which escapes them for us) but is host-overridable to false (legitimately, e.g. in API apps), and a component must not depend on a global it neither sets nor checks. </script> closes a script element regardless of its type, so an unescaped value carrying it would break out of the island into live HTML. Idempotent when the host already escapes (the \uXXXX forms carry no literal </>/&), and JSON.parse decodes the escapes back to the original text. Accepts a pre-serialized JSON string or any to_json-able object.

  • json (String, Object) — serialized JSON, or an object to serialize

Returns (ActiveSupport::SafeBuffer) — escaped, HTML-safe JSON text

#slot_data_attributes(part)

data-slot for a named part of the component's anatomy (skeleton parts carry their role: icon, label, spinner, ...).

  • part (Symbol, String) — the anatomy part name

Returns (Hash) — e.g. { "data-slot" => "icon" }

#stable_key

The caller-supplied semantic identity (key:), if any.

#to_html

Renders the component to an HTML string.

Creates a minimal controller and view context to render the component outside of a normal request cycle. Useful for testing and debugging.

Returns (String) — the rendered HTML

component = MyComponent.new(color: :primary)
component.to_html # => "<div class=\"...\">...</div>"

Poetry::Core::Concerns::Options

module · app/components/poetry/core/concerns/options.rb

The Options concern provides a DSL for defining typed attributes in components. It extends the basic attribute functionality with support for ActiveModel types, proc defaults, and tracking of which attributes have been explicitly set vs using defaults.

The registration, tracking, and hierarchy machinery lives in DeclaredAttributes (shared with Styles); this concern owns the option-specific surface: ActiveModel types and value formats.

Unlike Styles, Options:

  • Do not have variants
  • Do not generate CSS
  • Support all ActiveModel types (string, integer, boolean, float, etc.)
class MyComponent < Poetry::Core::Component
  option :title, :string, default: "Untitled"
  option :count, :integer, default: 0
  option :enabled, :boolean, default: true
end
class Card::Component < Poetry::Core::Component
  option :title, :string, default: "Card"
  option :aria_label, :string, default: -> { title }
end

card = Card::Component.new(title: "My Card")
card.aria_label  # => "My Card" (inherited from title)
card.title = "Updated"
card.aria_label  # => "Updated" (still follows title)

card2 = Card::Component.new(title: "Card", aria_label: "Custom Label")
card2.title = "Updated"
card2.aria_label  # => "Custom Label" (explicitly set, doesn't follow title)
class MyComponent < Poetry::Core::Component
  option :id, :string, required: true
end
class MyComponent < Poetry::Core::Component
  option :price, :decimal
  option :score, :float
  option :created_at, :datetime
end
Constant Description
BASE_COMPONENT_CLASS Where ancestor walks stop: declarations above the base component class are nobody's.

.has_option_attribute?(name)

Checks if the given name is a defined option attribute.

  • name (Symbol, String) — the attribute name to check

Returns (Boolean) — true if the attribute is an option attribute

.option(name, type, **options)

Defines an option attribute for the component.

  • name (Symbol, String) — the name of the option attribute
  • type (Symbol) — the ActiveModel type (:string, :integer, :boolean, :float, :decimal, :value, etc.)
  • options (Hash) — configuration options
option :title, :string, default: "Untitled"
option :aria_label, :string, default: -> { title }
option :id, :string, required: true
option :enabled, :boolean, default: false
option :name, :symbol, required: true, format: :"icon-name"

.option_attributes

Returns all option attributes defined on this component and its ancestors.

Returns (Array<Symbol>) — sorted array of all option attribute names

.option_attributes_with_defaults

Returns all option attributes that have default values (static or proc).

Returns (Array<Symbol>) — sorted array of attribute names with defaults

.option_attributes_with_proc_defaults

Returns option attributes that have proc default values. Proc defaults allow dynamic defaults that can reference other attributes.

Returns (Array<Symbol>) — sorted array of attribute names with proc defaults

.option_attributes_with_static_defaults

Returns option attributes that have static (non-proc) default values.

Returns (Array<Symbol>) — sorted array of attribute names with static defaults

.option_docs

The doc: strings declared on this component's options, hierarchy-wide (nearest declaration wins).

.option_format(name)

Returns the declared value format for a given option attribute (e.g. :"icon-name") - the machine-checkable value contract the registry and poetry check read. Nil when the option is free-form.

  • name (Symbol, String) — the attribute name

Returns (Symbol, nil) — the declared format

.option_type(name)

Returns the type for a given option attribute.

  • name (Symbol, String) — the attribute name

Returns (Symbol, nil) — the type of the attribute

#initialized_option_attributes

Returns only the option attributes that have been explicitly set (not using defaults).

Returns (Array<Symbol>) — sorted array of initialized attribute names

#option_attribute?(name)

Checks if the given attribute is an option attribute.

  • name (Symbol, String) — the attribute name to check

Returns (Boolean) — true if the attribute is an option attribute

#option_attribute_initialized?(name)

Checks if an option attribute has been explicitly initialized.

  • name (Symbol, String) — the attribute name to check

Returns (Boolean) — true if the attribute was explicitly set

#option_attributes

Returns all option attributes defined on this component's class.

Returns (Array<Symbol>) — sorted array of all option attribute names

#option_attributes_status

Returns all option attributes with their initialization status.

Returns (Hash{Symbol => Boolean}) — map of attribute names to initialized status

#options

Returns a hash of all option attributes with their current values.

Returns (Hash{Symbol => Object}) — map of attribute names to their values

Poetry::Core::Concerns::Styles

module · app/components/poetry/core/concerns/styles.rb

The Styles concern provides a powerful DSL for defining style attributes in components. It extends the basic attribute functionality with support for variants, proc defaults, and tracking of which attributes have been explicitly set vs using defaults.

The registration, tracking, and hierarchy machinery lives in DeclaredAttributes (shared with Options); this concern owns the style-specific surface: variants, inclusion validation, and CSS emission.

class MyComponent < Poetry::Core::Component
  style :color, default: :primary, variants: [:primary, :secondary, :success]
  style :size, default: :md, variants: [:sm, :md, :lg]
end
class Badge::Component < Poetry::Core::Component
  style :color, default: :gray, variants: [...colors]
  style :dot_color, default: -> { color }, variants: [...colors]
end

badge = Badge::Component.new(color: :red)
badge.dot_color  # => :red (inherits from color)
badge.color = :blue
badge.dot_color  # => :blue (still follows color)

badge2 = Badge::Component.new(color: :red, dot_color: :green)
badge2.color = :blue
badge2.dot_color  # => :green (explicitly set, doesn't follow color)
class MyComponent < Poetry::Core::Component
  style :type, required: true, variants: [:button, :link]
end
class MyComponent < Poetry::Core::Component
  style :outlined, variants: :boolean, default: false
end
Constant Description
BASE_COMPONENT_CLASS Where ancestor walks stop: declarations above the base component class are nobody's.
STYLE_CLASS_SUFFIX The naming convention joining a component to its sidecar style dictionary (Dot::Component -> Dot::Style).

.has_style_attribute?(name)

Checks if the given name is a defined style attribute.

  • name (Symbol, String) — the attribute name to check

Returns (Boolean) — true if the attribute is a style attribute

.style(name, **options)

Defines a style attribute for the component.

  • name (Symbol, String) — the name of the style attribute
  • options (Hash) — configuration options
style :color, default: :primary, variants: [:primary, :secondary]
style :dot_color, default: -> { color }, variants: [:primary, :secondary]
style :type, required: true, variants: [:button, :link]
style :outlined, variants: :boolean, default: false

.style_attributes

Returns all style attributes defined on this component and its ancestors.

Returns (Array<Symbol>) — sorted array of all style attribute names

.style_attributes_with_defaults

Returns all style attributes that have default values (static or proc).

Returns (Array<Symbol>) — sorted array of attribute names with defaults

.style_attributes_with_proc_defaults

Returns style attributes that have proc default values. Proc defaults allow dynamic defaults that can reference other attributes.

Returns (Array<Symbol>) — sorted array of attribute names with proc defaults

.style_attributes_with_static_defaults

Returns style attributes that have static (non-proc) default values.

Returns (Array<Symbol>) — sorted array of attribute names with static defaults

.style_class

Automatically determines the corresponding Style class for this component. Example: Poetry::Core::Dot::Component -> Poetry::Core::Dot::Style

Returns (Class, nil) — the style class if it exists, nil otherwise

.style_docs

The doc: strings declared on this component's styles, hierarchy-wide (nearest declaration wins).

#bem(element = nil, **overrides)

The BEM token IR for this component (the pipeline's Step 2): the block class plus one modifier class per style value - symbols as block--attr-value, booleans as presence modifiers (block--attr). A named element returns block__element.

  • element (Symbol, nil) — a named element
  • overrides (Hash) — style overrides merged over the resolved values

Returns (String) — space-separated BEM classes

#bem_block

The component's BEM block name - the stable, framework-agnostic class contract of the token IR ("poetry/core/dot" -> "poetry-core-dot").

#css(element = nil, **options, &)

Generates CSS classes based on style attributes and additional options.

The emission is governed by css_mode: :tailwind (default) resolves the style values to utility classes through the sidecar Style dictionary; :bem emits the stable BEM token IR instead, for hosts that bring their own CSS (styled against the generated reference stylesheet). Override per call with css_mode:, or globally via Poetry::Core::Config.current.css_mode.

  • element (Symbol, nil) — a named element (BEM block__element)
  • options (Hash) — additional style options to merge

Returns (String, nil) — the generated CSS classes; nil in :tailwind mode for a component without a sidecar Style class

#initialized_style_attributes

Returns only the style attributes that have been explicitly set (not using defaults).

Returns (Array<Symbol>) — sorted array of initialized attribute names

#style_attribute?(name)

Checks if the given attribute is a style attribute.

  • name (Symbol, String) — the attribute name to check

Returns (Boolean) — true if the attribute is a style attribute

#style_attribute_initialized?(name)

Checks if a style attribute has been explicitly initialized.

  • name (Symbol, String) — the attribute name to check

Returns (Boolean) — true if the attribute was explicitly set

#style_attributes

Returns all style attributes defined on this component's class.

Returns (Array<Symbol>) — sorted array of all style attribute names

#style_attributes_status

Returns all style attributes with their initialization status.

Returns (Hash{Symbol => Boolean}) — map of attribute names to initialized status

#styler

Returns the style class for this component.

Returns (Class, nil) — the style class if it exists

#styles

Returns a hash of all style attributes with their current values.

Returns (Hash{Symbol => Object}) — map of attribute names to their values

Poetry::Core::Concerns::Parts

module · app/components/poetry/core/concerns/parts.rb

The part contract: a hand-authored, machine-verified declaration of the component's styling surface - the data-slot parts its DOM exposes, the state attributes each part carries (and when), and the CSS custom properties that seam the part to themes and controllers.

part "dialog-content", "The <dialog> panel - the positioning and animation surface", states: { "data-open" => "panel is open (setState pairs it with data-closed)", "data-closed" => "panel is closed or animating out" }

Binding such a contract with types alone would keep the keys from drifting while leaving every description and condition as unverified prose. poetry binds the declaration to RENDERED DOM instead - PartContract.verify reconciles it against every preview in both directions (rendered-but-undeclared, declared-but-never- rendered), so the published contract cannot lie about the anatomy.

Declarations are OWN-CLASS ONLY, deliberately not inherited: Sheet and Drawer subclass Dialog::Component yet share none of its part names (sheet-content vs dialog-content) - inheritance would leak phantom parts into every subclass with renamed anatomy.

Constant Description
PART_NAME The kebab-case shape a declared part name (data-slot value) must match.
STATE_ATTRIBUTE The data-* shape a declared state attribute must match.
VAR_NAME A trailing * declares a dynamic family (charts' per-series --color-*), matched by prefix at verify time.

.part(name, description, states: {}, vars: {})

Declares one part of the component's rendered anatomy.

  • name (String) — the part's data-slot value
  • description (String) — what the part is - docs/agent prose
  • states (Hash) — state attribute => condition prose, or => { condition: "...", values: %w[...] } for valued attributes (data-side => top/right/bottom/left)
  • vars (Hash) — CSS custom property => description

.part_definitions

The declared contract, registry-shaped (plain string keys, so the YAML round-trips byte-identical). Own-class only - see the module docs for why subclasses never inherit anatomy.

Poetry::Core::Concerns::Stimulus

module · app/components/poetry/core/concerns/stimulus.rb

The use_stimulus contract: a class-level, element-major declaration of the component's Stimulus wiring, replacing both the hand-rolled <element>_stimulus_attributes methods and the previous stimulated_with DSL (controller-major, root-only - it could not express multi-element wiring, and no component ever adopted it).

use_stimulus do on :root do controller :hover_card do register value :open value :open_delay, unless: -> { open_delay.nil? } end controller :popper do register value :side end end on :trigger do controller :hover_card do action :pointer_enter, on: :pointerenter end controller :popper do target :anchor end end end

Render side: stimulus_attributes_for(:trigger) returns the element's merged attribute hash (public - templates and slot lambdas call it directly); stimulus_action(:open) / stimulus_event(:change) build validated descriptor strings for forwarding; stimulus_attributes(:a, :b) { |a, b| ... } is the escape hatch for wiring too dynamic to declare - every builder shares ONE Attributes instance, so multi-controller merges are correct by construction.

Declarations validate against the controllers manifest at CLASS LOAD (unknown controller/value/action/target/event raises at boot, not first render) and are declared once per element: a subclass redeclaring an element REPLACES it wholesale (the Ruby-override intuition; Sheet/Drawer re-controller their roots this way), while on :root, extend: true merges into the inherited element (date_field -> time_field adds values). Untouched elements inherit.

.own_stimulus_elements

This class's own declarations, element name -> Element.

.resolve_stimulus_identifier(identifier)

Resolves a declaration-style identifier (Symbol suffix, String, or Array) to its full manifest identifier - see {Poetry::Core::Stimulus::Declarations.resolve_identifier}.

  • identifier (Symbol, String, Array) — the declaration-style identifier

Returns (String) — the full Stimulus identifier

.stimulus_action(*args, on: nil, at: nil)

Descriptor builders are STATIC facts of the declarations, so they exist at class level - helpers, generator templates, and test selectors consume them without an instance. on:/at: build the evented token ("click->id#method"); without on: the bare descriptor (element-default event).

  • args (Array<Symbol, String>)(method) resolves the method across the declared controllers; (controller, method) pins one
  • on (Symbol, String, Array<Symbol>, nil) — the event(s) to listen for
  • at (Symbol, String, nil) — the event target (:window, :document)

Returns (String) — the action descriptor ("click->poetry--core--x#open")

.stimulus_definitions

The registry-shaped projection of the RESOLVED wiring (post- inheritance, so Sheet publishes sheet controllers) - plain data for the registry, skill text, and docs tables.

Returns (Array<Hash>) — one serialized element per declared element

.stimulus_elements

The effective wiring after inheritance: walk the superclass chain root-first, folding each class's declarations over the inherited set - redeclared elements replace wholesale unless declared with extend: true, which appends to the inherited element's wirings.

.stimulus_event(*args)

A validated event-name string for listening markup; stimulus_event(:change) resolves across the declared controllers, stimulus_event(:controller, :change) pins one.

  • args (Array<Symbol, String>)(name) or (controller, name)

Returns (String) — the full event name

.stimulus_identifiers

Every controller identifier declared anywhere on the class, in declaration order - the search space for unqualified stimulus_action / stimulus_event resolution.

.use_stimulus(&block)

Declares (part of) the component's stimulus wiring. Multiple blocks compose additively within a class; shared wiring modules call this from their included hook.

#stimulus_action(*, on: nil, at: nil)

Delegates to the class-level builder (descriptors are static facts of the declarations); stimulus_action(:open) resolves across declared controllers, on:/at: build the evented token.

  • on (Symbol, String, Array<Symbol>, nil) — the event(s) to listen for
  • at (Symbol, String, nil) — the event target (:window, :document)

Returns (String) — the action descriptor

#stimulus_attributes(*controllers)

The escape hatch for wiring too dynamic to declare: yields one Builder per controller, all sharing ONE Attributes instance. Controllers resolve like declarations (Symbol -> manifest, String/Array -> verbatim).

  • controllers (Array<Symbol, String, Array>) — one or more controller identifiers

Returns (Hash) — the accumulated attributes

#stimulus_attributes_for(element_name)

The declared wiring for one element as a plain attributes hash, ready to merge into the element's tag or forward as component kwargs. Public by design - templates call it, ending the public :inner_stimulus_attributes juggling.

  • element_name (Symbol, String) — a declared element (:root, ...)

Returns (Hash) — the element's data attributes; empty when the element's if:/unless: conditions do not hold

#stimulus_event(*)

Delegates to the class-level builder: stimulus_event(:change) resolves across the declared controllers, stimulus_event(:controller, :change) pins one.

Returns (String) — the full event name

Poetry::Core::Style

class · app/components/poetry/core/style.rb

The sidecar style class for a component: the dictionary from the component's style surface (declared with style :attr, variants: on the component) to CSS utility classes, resolved through the in-tree {CSS::Resolver}.

Defaults are NOT declared here - they live in exactly one place, the component's style :attr, default: (the single source of truth; the component's ActiveModel attributes resolve them before render). A defaults call raises to enforce that.

class Badge::Style < Poetry::Core::Style
  base "inline-flex items-center rounded-md"
  element :icon, "size-3 shrink-0"
  variant :color, red: "bg-destructive/15 text-destructive",
                  gray: "bg-muted text-muted-foreground"
  compound({ color: :red, mode: :dark }, "bg-destructive/25")
end

.base(classes)

Declares the root element's always-present utility classes - the dictionary's base layer, emitted before any variant classes.

  • classes (String) — space-separated utility classes
base "inline-flex items-center rounded-md"

.bem_block

The BEM block this dictionary belongs to, derived from the sibling component ("poetry/core/dot" -> "poetry-core-dot").

.capsule

The capsule digest of this dictionary (the :bem leak-guard).

.component_class

The sibling component class by convention (Poetry::Core::Dot::Style -> Poetry::Core::Dot::Component).

.compound(criteria, classes)

Declares classes emitted only when EVERY criteria pair matches the resolved style values - the cross-axis refinement a single variant cannot express.

  • criteria (Hash{Symbol => Object}) — style attribute => value pairs
  • classes (String) — utility classes added on a full match
compound({ color: :red, mode: :dark }, "bg-destructive/25")

.css(element = nil, **options)

Resolves utility classes for the given criteria (and optional element). The class: option appends caller classes, which win on Tailwind conflicts.

  • element (Symbol, nil) — a declared element name, or nil for the root
  • options (Hash) — style attribute => value criteria, plus class:

Returns (String) — the resolved utility classes

.defaults(*)

Single-source-of-defaults enforcement: defaults belong on the component (style :attr, default:), never in the style dictionary.

.element(name, classes)

Declares the classes of a named inner element of the component's anatomy, resolved with css(:name) at render.

  • name (Symbol) — the element name (:icon, :label, ...)
  • classes (String) — space-separated utility classes
element :icon, "size-3 shrink-0"

.resolver

Each Style class owns a resolver; subclasses extend a copy of their parent's dictionary.

.variant(attr, mapping)

Declares one style axis: the classes each value of the component's matching style :attr declaration resolves to.

  • attr (Symbol) — the style attribute this axis resolves
  • mapping (Hash{Symbol => String}) — variant value => utility classes
variant :color, red: "bg-destructive/15 text-destructive",
                gray: "bg-muted text-muted-foreground"

.variant_options

{ attr => [values] } - the declared variant space, for previews, docs, and the registry.

#css(...)

Instance-level mirror so styler.css(...) keeps working from the Styles concern.

Returns (String) — the resolved utility classes (see {.css})

Poetry::Core::Stimulus::Declarations

module · lib/poetry/core/stimulus/declarations.rb

The use_stimulus declaration model (the DSL behind Concerns::Stimulus). Declarations build at class-load time and are validated against the controllers manifest THERE - an unknown controller, value, action method, target, or event name raises at boot/test-collection instead of first render. The Builder still guards emission for wiring built outside declarations.

Element-major by design: every controller wired to one element builds into ONE HTML::Attributes instance at render, so a plain Hash#merge of two builds can never clobber data-controller - correct by construction, not by convention.

See {Poetry::Core::Concerns::Stimulus} for the component-facing contract and a full declaration example.

use_stimulus do
  on :root do
    controller :hover_card do
      register
      value :open
    end
  end
  on :trigger do
    controller :hover_card do
      action :pointer_enter, on: :pointerenter
    end
    controller :popper do
      target :anchor
    end
  end
end
Constant Description
CONDITION_KEYS The condition keywords every DSL call accepts.

.event_name(controller, name)

A validated cross-controller event name for action on: sources, resolved to the manifest's REAL emitted name ("poetry:calendar:change"; layer controllers keep the identifier prefix) - ends the hand-written string seam between dispatching and listening controllers.

  • controller (Symbol, String) — the dispatching controller
  • name (Symbol, String) — the event's short name (the suffix after the final colon)

Returns (String) — the full emitted event name

event(:calendar, :change) # => "poetry:calendar:change"

.resolve_identifier(identifier)

Symbols resolve against the manifest by unique suffix (:hover_card -> "poetry--core--hover-card"), so declarations never hand-write gem namespaces; strings and arrays pass through the Builder's existing policy (strict for poetry--*, unvalidated for host-app controllers).

  • identifier (Symbol, String, Array) — a manifest suffix (:hover_card), a full identifier, or the Builder's array form

Returns (String) — the full Stimulus identifier

Poetry::Core::Tokens

class · lib/poetry/core/tokens.rb

The canonical design-token model. Loads tokens/tokens.dtcg.json - the single source of truth - and exposes the semantic color roles per mode plus the radius dimension. Everything else (tokens.css, tailwind-theme.css, the DESIGN.md front matter) is generated from an instance of this class; the AAA-contrast gate asserts against it.

tokens = Poetry::Core::Tokens.load
tokens.color("light", "primary").css # => "oklch(0.205 0 0)"
tokens.radius_css                    # => "0.625rem"
Constant Description
DEFAULT_RELATIVE_PATH Where the canonical DTCG token file lives, relative to the gem root.
POETRY_STATUS_VARS Poetry-original extensions BEYOND the compat set: the soft status vocabulary that set lacks. Kept separate so the drop-in contract stays sharp: a drop-in theme block replaces the compat set wholesale, and these keep their poetry defaults unless the theme chooses to override.
SHADCN_V4_COMPAT_VARS The exact CSS custom-property set of the widely-distributed v4 theme convention (cssVarsV4, plus --radius). This is the drop-in contract: any theme block written to that convention defines exactly these names, so it can replace poetry's tokens.css wholesale.

.default_path

The gem's canonical token file path ({DEFAULT_RELATIVE_PATH} under the gem root).

.load(path = default_path)

Loads a DTCG token file into a Tokens instance.

  • path (String, Pathname) — a DTCG JSON file; defaults to the gem's canonical tokens

#color(mode, name)

The color of one semantic role in one mode.

  • mode (String) — "light" or "dark"
  • name (String) — the role name ("primary", "destructive", ...)

#color_names(mode)

The semantic color-role names of one mode, in file order.

  • mode (String) — "light" or "dark"

Returns (Array<String>) — role names ("primary", "muted-foreground", ...)

#data

Returns the value of attribute data.

#initialize(data)

Wraps one parsed DTCG document; {.load} is the file-backed form.

  • data (Hash) — the parsed DTCG JSON

Returns (Tokens) — a new instance of Tokens

#modes

Mode names ("light", "dark"), skipping DTCG $-metadata keys.

#radius_css

The radius dimension as CSS ("0.625rem").

Poetry::Core::Icons

module · lib/poetry/core/icons.rb

The pluggable icon-set registry (Lucide default, per-set adapters). An icon set is anything responding to #include?(name), #fetch(name) -> inner SVG markup, and #names. Sets register themselves on require (poetry-lucide does); the active set is selected by config.icon_library and can be overridden per render.

SECURITY: #fetch's return value is rendered html_safe by the Icon component. The shipped sets vendor SVGs sanitized AT VENDOR TIME (poetry-lucide's fetch script strips <script>/<foreignObject>/handlers /external-href <use>/<image>), so render never parses untrusted markup. A custom set registered by a host MUST pre-sanitize its SVGs the same way - the vendored pipeline is the reference; a set that serves raw, attacker-influenced SVG is an XSS sink.

.register(key, set)

Registers an icon set under a library key - the extension point an icon gem calls on require. A set is any object responding to #include?(name), #fetch(name) (returning inner SVG markup), and #names. The set contract: #fetch raises {Poetry::Core::IconNotFound} for any name it cannot serve - the Icon component's missing-icon policy rescues exactly that class. See the SECURITY note above for the pre-sanitization requirement on custom sets.

  • key (Symbol, String) — the library key config.icon_library selects the set by
  • set (Object) — the icon set (a {FileSet} over a directory of vendored SVGs, or any object honoring the same contract)

Returns (Object) — the set, now registered

Poetry::Core::Icons.register(:my_icons,
  Poetry::Core::Icons::FileSet.new(dir: root.join("icons")))
# config/initializers/poetry.rb: config.icon_library = :my_icons

.registry

The registered icon sets, library key => set object. Icon gems add themselves here on require.

.set(library = nil)

The set for the given library key, defaulting to config.icon_library. Raises with the fix when unregistered.

  • library (Symbol, String, nil) — the library key; nil reads Poetry::Core::Config.current.icon_library

Returns (Object) — the registered set

.suggest(name, names)

Did-you-mean for icon names. The reversed-compound form is checked before edit distance: Lucide v1 swapped modifier and noun (alert-circle -> circle-alert, x-circle -> circle-x), a rename class DidYouMean's checker misses every time - the reversal IS the fix.

  • name (Symbol, String) — the unknown name (underscores tolerated)
  • names (Enumerable) — the valid names to suggest from

Returns (String, nil) — the closest valid name, or nil

Poetry::Core::Registry

class · lib/poetry/core/registry.rb

The generated component registry: the machine-readable index of every component's public surface, built entirely FROM SOURCE - the prop_definitions introspection shim + the Style dictionaries - and CI-verified against a fresh build (a committed registry that can never drift from the code).

Consumers: the docs site, the MCP server, the agent skill, and the A2UI / WebMCP projections - one contract, every surface.

Poetry::Core::Registry.new(source_root: gem_root).generate!
Constant Description
HEADER The banner written at the top of the generated YAML.
LIFECYCLE_METHODS Stimulus lifecycle callbacks are not consumer-callable actions.
MINT_PATTERN The id-minting funnel, as source text: a component family whose sources never match renders no poetry-minted random id (the "identity" derivation - see the entry's identity key).
RELATIVE_PATH Where the committed registry lives, relative to a gem root.

.committed(root)

Loads the committed registry YAML boot-free.

  • root (String, Pathname) — the gem root holding the registry file

#blocks

The block catalog and the root template paths resolve against - LlmsText reads both to inline block source into llms-full.txt.

#components

The discovered component classes (the registry's working set).

#entries

The full registry payload's "components" section: one contract hash per discovered component, keyed by component path.

#form_builder

The FormBuilder surface (optional section) - LlmsText renders it as the Forms section.

#generate!(root: @source_root)

Writes the registry YAML to its committed location under root.

  • root (Pathname, String) — the gem root to write under

Returns (Pathname) — the written file path

#initialize(components: nil, source_root: Poetry::Core.root, # rubocop:disable Metrics/ParameterLists helpers: nil, blocks: nil, helper_args: nil, descriptions: nil, form_builder: nil)

  • components (Enumerable<Class>, nil) — component classes; defaults to every named Poetry::Core::Component descendant (eager-loaded) whose source lives under source_root.
  • source_root (Pathname, String) — the gem root to discover in and write the registry to - poetry-ui passes its own root.
  • helpers (Hash, nil) — wrapper helpers that are NOT components (poetry_input_group_addon et al): helper name => contract hash ({"options" => [...]}, or {} for a plain wrapper). Emitted as the registry's "helpers" section so boot-free consumers (poetry check, the MCP server) know the full valid helper set AND the value contracts runtime-enforced inside those helpers.
  • blocks (Hash, nil) — the gem's block catalog: block name => {"title", "description", "components", "template"} - title/ description parsed from each template's poetry:block header, the composed components derived from its poetry_* calls, template the gem-relative source path boot-free consumers (the MCP server) read. Emitted as the registry's "blocks" section.
  • helper_args (Hash, nil) — max POSITIONAL arity per poetry_* helper, introspected from the gem's helper-module signatures (e.g. poetry_link "text", href: on a kwargs-only helper raises at render). Rest-signatures are omitted (unknowable); the linter enforces arity only where a key exists, so registries without the map stay lint-identical.
  • descriptions (Hash, nil) — one-line human descriptions per component_path (the gem's editorial component_descriptions.yml), merged into each entry as "description" - the summary llms.txt, the MCP describe_component, and the docs page all read from one source.
  • form_builder (Hash, nil) — the gem's FormBuilder surface ({"rules" => [...], "methods" => {name => one-liner}, "input_types" => [...]}) - emitted as the registry's "form_builder" section so llms.txt, the MCP server, and skills can teach the model-bound form story without booting the gem. Optional like every other section: absent -> the registry stays byte-identical.

Returns (Registry) — a new instance of Registry

#source_root

The block catalog and the root template paths resolve against - LlmsText reads both to inline block source into llms-full.txt.

#to_yaml

The complete registry serialized as plain-data YAML (components plus the optional helpers/blocks/helper_args/form_builder sections), headed by the do-not-edit banner.

#verified?(root: @source_root)

False when the committed registry does not match a fresh build.

  • root (Pathname, String) — the gem root holding the committed file

ActiveModel

module · lib/active_model/type/list.rb

Reopened to register poetry's option types alongside Rails' own.

ActiveModel::Type

module · lib/active_model/type/list.rb

Reopened: poetry registers its list (and symbol) attribute types here so the option/style DSLs can declare them.

ActiveModel::Type::List

class < ActiveModel::Type::Value · lib/active_model/type/list.rb

A string-array option type (Accordion's open:, future multi-selects). Casts scalars to one-element arrays and stringifies members, so open: :shipping and open: %w[a b] both normalize.

#cast(value)

Normalizes any value to an array of strings: nil becomes [], a scalar becomes a one-element array, and members are stringified.

  • value (Object) — the declared option value

#type

Reports :list to introspection (prop_definitions, the registry).

ActiveModel::Type::Symbol

class < ActiveModel::Type::String · lib/active_model/type/symbol.rb

A symbol option type for the option DSL: casts anything responding to #to_sym and reports :symbol to introspection.

Strict by design: a value that cannot become a Symbol raises at assignment - poetry surfaces bad option values where they are written, not as a validation error nothing reads at render. Leniency is List's job, not this type's.

#cast(value)

Casts a value to a Symbol; nil stays nil.

  • value (#to_sym, nil) — the raw option value

#type

Without this the type reports :string (inherited) - introspection (prop_definitions, the registry) must see :symbol.

Returns (Symbol) — :symbol

Poetry

module · lib/poetry/core/errors.rb

Poetry::Core module provides extensions and enhancements for Rails views and components.

Poetry::Core

module · lib/poetry/core.rb

The framework layer of poetry: the Rails engine, the component DSL, and the shared primitives. Concrete components live in poetry-ui.

Constant Description
VERSION The gem version. Every gem in the family carries the same version and pins poetry-core to it exactly.

.loader

The dedicated Zeitwerk loader for poetry-core's lib/ tree.

.root

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

Poetry::Core::Box

module · app/components/poetry/core/box/component.rb

Namespace of the Box primitive.

Poetry::Core::Box::Component

class < Poetry::Core::Component · app/components/poetry/core/box/component.rb

The polymorphic-tag primitive: one element, any tag, the full poetry attribute machinery. Box exists for the bare styled element a real component would be too much for - a spacer, a semantic wrapper, a grid cell - while keeping the class merger, Stimulus data: merging, and self-identification that a raw content_tag skips. Void elements (br, hr, img, ...) self-close and take no content block.

Anatomy (data-slot parts):

  • box - The rendered element itself - the whole component is one part
render Poetry::Core::Box::Component.new(html_tag: "section", class: "grid gap-4") do
  "content"
end
render Poetry::Core::Box::Component.new(html_tag: "hr", class: "my-6")
Constant Description
TAG_NAME Tag names: letters, digits, and dashes (custom elements), starting with a letter.
VOID_TAGS The HTML void elements: they emit a self-closing tag and take no content block.

#call

Emits the chosen tag: self-closing for void elements, wrapping the content block otherwise. A void element with a content block raises - the content would be silently unrenderable.

Options

#

The element to render ('div', 'section', 'span', ...); void elements (br, hr, img, ...) self-close and take no content block.

Constructor option html_tag:.

Returns (String) — defaults to "div"

Poetry::Core::CSS

module · lib/poetry/core/css/tailwind_merger.rb

The CSS tooling namespace: class-name merging and the compiled-build gates.

Poetry::Core::CSS::BemMerger

class · lib/poetry/core/css/bem_merger.rb

The BEM-mode classname merger: the classname_merger a host pairs with css_mode = :bem. Same contract as {TailwindMerger} (flatten, stringify, drop blanks, nil for empty input) with exactly two behaviors on top: order-preserving token dedupe and a space join - no Tailwind conflict semantics applied to a BEM host's classes.

Deliberately NO modifier-axis conflict resolution: the BEM modifier grammar is dictionary-dependent at the string level (values carry dashes - --align-inline-start - and names carry underscores), so a string merger guessing axes would be wrong. Style axes are driven through component options; conflicts between raw caller classes belong to the host stylesheet's cascade.

Poetry::Core::Config.current.css_mode = :bem
Poetry::Core::Config.current.classname_merger =
  Poetry::Core::CSS::BemMerger.new
Poetry::Core::CSS::BemMerger.new.merge("pill pill--variant-danger", "pill", "p-4 p-2")
# => "pill pill--variant-danger p-4 p-2"

#merge(*classes)

Merges class lists BEM-style: normalize, dedupe at the token level (first occurrence keeps its position), join.

  • classes (Array<String, Symbol, Array, nil>) — class names, arrays of class names, or nils

Returns (String, nil) — the merged classes, or nil when empty

Poetry::Core::Concerns

module · app/components/poetry/core/concerns/styles.rb

The concerns composed into {Poetry::Core::Component}: styles, options, slots, Stimulus wiring, introspection, and part declarations.

Poetry::Core::Concerns::AgentTools

module · app/components/poetry/core/concerns/agent_tools.rb

The agent-tool contract: class-level declarations of the component's agent-callable tools - the "operate" projection of the component's Stimulus surface. A tool names one action an in-page agent may invoke on a rendered instance (WebMCP's document.modelContext, or any client that reads the registry), described in MCP Tool shape: name, description, JSON-Schema input, and safety annotations.

tool :set_value, description: "Select the option whose value matches.", params: { value: { type: "string", required: true, description: "The option value to select." } }, executes: :set_value, mutating: true

Declarations are validated at CLASS LOAD, like use_stimulus: executes: resolves through {Concerns::Stimulus#stimulus_action} against the component's declared controllers and the controllers manifest, so a tool can never name an action the JS does not define - declare use_stimulus before tool. The resolved descriptor ("poetry--core--combobox#setValue") is the tool's wire form: registration runtimes dispatch it verbatim.

Safety doctrine (non-negotiable): tools are read-only unless declared mutating: true (annotations.readOnlyHint inverts it), untrusted_content: true marks tools whose output carries user-authored content, and declaring a tool exposes NOTHING by itself - emission is opt-in per rendered instance, owned by the registration runtime, never default-on.

Projection: {ClassMethods#tool_definitions} feeds the registry's per-component tools section (plain strings, YAML-safe), which the agent surfaces (llms.txt, MCP server, skills, docs) and the WebMCP registration payload all read - one contract, every surface.

Constant Description
DESCRIPTION_LIMIT The agent-legibility budget for a tool description.
NAME_LIMIT The house character budgets: names stay far under the spec's 128-char full-name cap (instance prefixes join later); descriptions stay inside the agent-legibility budget.
NAME_PATTERN Tool and param names stay inside the WebMCP tool-name grammar (ASCII alphanumerics, _, -, .) with poetry's stricter snake_case convention, so a composed full name (instance prefix + tool name) can never need escaping.
PARAM_DESCRIPTION_LIMIT The agent-legibility budget for one parameter's description (Chrome's guidance: 150 characters per parameter description).
WEBMCP_CONTROLLER The registrar controller (poetry-agent's) that a webmcp: root registers; its manifest entry joins core's catalog when the gem loads, so the wiring validates like any other controller.
WEBMCP_DEFAULT_BUDGET The registrar's built-in per-document registration budget; the root carries an explicit budget only when the configured value ({Poetry::Core::Config#webmcp_registration_budget}) differs.

.own_tools

This class's own tool declarations, name => {Tool}.

.resolve_tool_action(tool)

The bare Stimulus descriptor a tool dispatches on THIS class.

  • tool (Tool)

Returns (String) — e.g. "poetry--core--combobox#setValue"

.tool(name, description:, executes:, # rubocop:disable Metrics/ParameterLists -- one keyword per Tool field params: nil, input_schema: nil, title: nil, mutating: false, untrusted_content: false)

Declares one agent-callable tool of this component.

  • name (Symbol) — the tool's name (snake_case; composes into the registered full name, so it must satisfy the WebMCP name grammar)
  • description (String) — what the tool does and when to use it - positive, single-function, non-empty (agents pick tools by this text)
  • executes (Symbol, Array(Symbol, Symbol)) — the Stimulus action the tool dispatches: a bare method resolves across the component's declared controllers; [:controller, :method] pins one. Validated against the controllers manifest at class load.
  • params (Hash{Symbol => Hash}, nil) — the tool's parameters as name => spec; each spec requires type: and may carry required: true (folded into the schema's required list) plus any JSON-Schema keywords (description: - at most {PARAM_DESCRIPTION_LIMIT} characters -, enum:, ...). The generated schema closes with additionalProperties: false, so a runtime rejects parameters the tool never declared.
  • input_schema (Hash, nil) — a complete JSON-Schema object for advanced shapes - mutually exclusive with params:
  • title (String, nil) — human-readable label for agent UIs (the spec's UA-displayable field); defaults to the tool name humanized (set_value -> "Set value")
  • mutating (Boolean) — declare true when invoking the tool changes state; tools are read-only by default (annotations.readOnlyHint is the inverse of this flag)
  • untrusted_content (Boolean) — declare true when the tool's output can carry user-authored content
tool :open, description: "Open the dialog.",
     executes: :open, mutating: true

.tool_definitions

The registry-shaped projection of the resolved tools: MCP Tool fields (name / title / description / inputSchema / annotations) plus the executes dispatch descriptor - plain string keys, YAML round-trippable.

executes resolves HERE, against the projecting class: a subclass that re-controllers its root (Sheet over Dialog) projects its own controller's descriptor for an inherited bare executes: :open, and a pinned controller the subclass no longer wires raises instead of projecting a descriptor the rendered DOM cannot dispatch.

.tools

The effective tools after inheritance: superclass chain root-first, a subclass redeclaring a name replaces it.

#webmcp_enabled?

Whether this rendered instance opted into WebMCP registration (webmcp: true or webmcp: "name" on the call).

#webmcp_name

The instance name the registrar composes tool names from (poetry.{name}.{tool}): the explicit webmcp: string, else the component title (give instances explicit names when a page renders several of one component - the browser rejects duplicate tool names).

Returns (String, nil) — nil when not enabled

#webmcp_tool_definition(definition)

The per-instance enrichment hook - override to refine a tool's projected definition with what only the rendered instance knows. Must return a JSON-serializable Hash with the same keys.

  • definition (Hash) — the class-level projection

#webmcp_tools

The instance's registration payload: its resolved tool definitions, each passed through {#webmcp_tool_definition} so a component can enrich a schema with rendered facts (Tabs adds the rendered tab values as an enum).

Poetry::Core::Concerns::AgentTools::Tool

class < Struct · app/components/poetry/core/concerns/agent_tools.rb

One declared tool. executes is the declared action spec ([method] or [controller, method]) - resolved to a bare Stimulus descriptor per PROJECTING class (see {ClassMethods#tool_definitions}); input_schema is the normalized JSON-Schema object (string keys) or nil for a parameterless tool.

#description

Returns the value of attribute description

Returns (Object) — the current value of description

#description=(value)

Sets the attribute description

  • value (Object) — the value to set the attribute description to.

Returns (Object) — the newly set value

#executes

Returns the value of attribute executes

Returns (Object) — the current value of executes

#executes=(value)

Sets the attribute executes

  • value (Object) — the value to set the attribute executes to.

Returns (Object) — the newly set value

#input_schema

Returns the value of attribute input_schema

Returns (Object) — the current value of input_schema

#input_schema=(value)

Sets the attribute input_schema

  • value (Object) — the value to set the attribute input_schema to.

Returns (Object) — the newly set value

#mutating

Returns the value of attribute mutating

Returns (Object) — the current value of mutating

#mutating=(value)

Sets the attribute mutating

  • value (Object) — the value to set the attribute mutating to.

Returns (Object) — the newly set value

#name

Returns the value of attribute name

Returns (Object) — the current value of name

#name=(value)

Sets the attribute name

  • value (Object) — the value to set the attribute name to.

Returns (Object) — the newly set value

#title

Returns the value of attribute title

Returns (Object) — the current value of title

#title=(value)

Sets the attribute title

  • value (Object) — the value to set the attribute title to.

Returns (Object) — the newly set value

#untrusted_content

Returns the value of attribute untrusted_content

Returns (Object) — the current value of untrusted_content

#untrusted_content=(value)

Sets the attribute untrusted_content

  • value (Object) — the value to set the attribute untrusted_content to.

Returns (Object) — the newly set value

Poetry::Core::Concerns::AgentTools::ToolError

class < Poetry::Core::Error · app/components/poetry/core/concerns/agent_tools.rb

Raised at class load for an invalid tool declaration.

Poetry::Core::Concerns::DeclaredAttributes

module · app/components/poetry/core/concerns/declared_attributes.rb

The declared-attribute engine shared by the Styles and Options DSLs. Each DSL is one "kind" of declared attribute: registration lands in per-kind class-level collections (@_<kind>_attributes, @_<kind>_attributes_with_defaults, @_<kind>_proc_defaults), explicit initialization is tracked per instance through the registered_<kind>s class_attribute the owning concern declares, and hierarchy-wide queries walk the ancestry up to the base component class. Kind-specific surface - variants and CSS emission for styles, ActiveModel types and value formats for options - stays in the owning concern.

Constant Description
BASE_COMPONENT_CLASS Where ancestor walks stop: declarations above the base component class are nobody's.

Poetry::Core::Concerns::Introspection

module · app/components/poetry/core/concerns/introspection.rb

The prop-introspection shim: a machine-readable description of a component's public surface - style attributes, options, and slots - derived from the metadata the Styles/Options DSLs and ViewComponent already carry. This is the single source the generated registry, the docs tables, and the MCP prop schema are built from; nothing here is hand-authored.

Constant Description
DYNAMIC_DEFAULT The registry's marker for a proc default (its value depends on other attributes and is unknowable statically).
KEYWORD_PARAMETER_KINDS The parameter kinds that ARE the keyword surface.
OPEN_PARAMETER_KINDS Parameter kinds that make a keyword surface open or unknowable (**rest accepts anything; a positional can swallow a braceless hash), and the kinds that ARE the keyword surface.
POSITIONAL_KINDS The parameter kinds counted toward positional arity.

.hand_rolled_setters(klass, definitions)

Every own with_* method that is neither a slot-generated setter (with_<name>/<singular>/<type> and their _content twins) nor inherited - NavigationMenu#with_link, PieChart's with_py.

  • klass (Class) — the slot-owning class
  • definitions (Array<Hash>) — the class's {slot_surface}

Returns (Array<String>) — the setter names without their with_ prefix, sorted

.prop_definitions

The component's full declared surface - styles, options, slots (with descriptions), required slots, and requires_any groups - as the registry generator serializes it.

.renders_many(slot_name, callable = nil, **opts)

ViewComponent's renders_many with the same keyword surface as {renders_one}; doc: describes the collection contract and is keyed by the plural declared name, exactly like {slot_doc}.

  • slot_name (Symbol) — the plural slot name
  • callable (Object, nil) — ViewComponent's positional callable (a component class, class-name string, or lambda)
  • opts (Hash) — doc:, renders:, and/or types:
renders_many :items,
             doc: "The rows, rendered in call order.",
             renders: lambda { |label:, **options| ... }

.renders_one(slot_name, callable = nil, **opts)

ViewComponent's renders_one, with the doc riding the declaration: doc: is lifted into {slot_doc}, renders: passes the callable as a keyword so the doc can come first, and the polymorphic types: form is re-formed into the positional hash ViewComponent expects - the ViewComponent surface underneath is unchanged. Unknown keywords raise at class load, and a positional callable cannot be combined with renders:/types:.

  • slot_name (Symbol) — the slot name
  • callable (Object, nil) — ViewComponent's positional callable (a component class, class-name string, or lambda)
  • opts (Hash) — doc:, renders:, and/or types:
renders_one :leading, doc: "Optional leading visual."
renders_one :trigger,
            doc: "The button that opens the dialog.",
            renders: lambda { |**options, &block| ... }

.required_slots_surface(klass, definitions)

The validated REQUIRED_SLOTS declaration of a slot-owning class: each key must name a setter the given slot definitions actually generate (the slot itself, a collection's singular, or a polymorphic type).

  • klass (Class) — the slot-owning class
  • definitions (Array<Hash>) — the class's {slot_surface}

Returns (Hash{String => String}) — setter name => hint

.requires_any_surface(klass, definitions)

The validated REQUIRES_ANY declaration: each group needs a hint plus at least one alternative, and slot alternatives must name setters the definitions actually generate.

  • klass (Class) — the slot-owning class
  • definitions (Array<Hash>) — the class's {slot_surface}

Returns (Array<Hash>) — one normalized group per declaration (hint, plus content/slots/options as declared)

.slot_doc(name, text)

The component's full prop surface.

Documents a slot declared with renders_one/renders_many. The string travels the same road as option/style doc: params - the registry, the agent surface, and the generated API docs. Prefer the doc: keyword on the declaration itself (it lands here); call slot_doc directly only when the doc and the declaration live in different modules.

  • name (Symbol) — the slot name as declared (plural for renders_many)
  • text (String) — one reference-register sentence

Returns (Hash) — { styles: [...], options: [...], slots: [...] }

slot_doc :trigger, "The button that opens the dialog."

.slot_docs

The slot_doc strings, hierarchy-wide (nearest wins).

.slot_surface(klass, seen: [])

The registry-shaped slot contracts of one slot-owning class - see the walker notes above for every emitted key.

  • klass (Class) — a component or builder class
  • seen (Array<Class>) — the recursion guard

Poetry::Core::Config

class · lib/poetry/core/config.rb

Manages configuration settings for the Poetry::Core module.

This class provides a flexible configuration system using ActiveSupport::OrderedOptions under the hood, allowing access to configuration values using either hash-style or method-style syntax. It supports both a singleton pattern via {.current} for global configuration and the ability to create custom configuration instances.

The configuration system is designed to be easily extensible while providing sensible defaults for all Poetry::Core components.

Poetry::Core::Config.current.classname_merger
# => #<Poetry::Core::CSS::TailwindMerger:0x00007f8b1c0a3b40>
Poetry::Core::Config.current.icon_library = :my_icons
config = Poetry::Core::Config.new
config.classname_merger = MyCustomMerger.new
Poetry::Core::Config.current[:classname_merger]
Poetry::Core::Config.current[:custom_setting] = "value"
Constant Description
SETTINGS The declared configuration surface: every key {.defaults} ships. Each is a real reader/writer pair (delegated just below); unknown keys still flow through delegate_missing_to, so hosts may stash their own values.

.current

Returns the global singleton configuration instance.

This method provides access to the shared configuration used throughout the application. The instance is created lazily on first access and persists for the lifetime of the application.

Returns (Poetry::Core::Config) — The global configuration instance

Poetry::Core::Config.current.css_mode
# => :tailwind
Poetry::Core::Config.current.classname_merger = CustomMerger.new

.default

Creates a new configuration instance with default settings.

This is aliased from the standard {#initialize} method to provide a more semantic way to create default configurations.

Returns (Poetry::Core::Config) — A new configuration instance with default values

config = Poetry::Core::Config.default

.defaults

Returns the default configuration values.

These defaults are used when initializing new configuration instances and define the standard behavior for all Poetry::Core components.

The keys and their defaults:

  • classname_merger ({Poetry::Core::CSS::TailwindMerger}) - resolves

conflicting utility classes when caller classes meet component classes.

  • stimulus_merger ({Poetry::Core::Stimulus::Merger}) - combines

Stimulus data attributes without duplicating controllers or actions.

  • css_mode (:tailwind) - :tailwind emits resolved utility

classes; :bem emits the BEM token IR for bring-your-own-CSS hosts.

  • icon_library (:lucide) - the active icon set, by the key it

registered under ({Poetry::Core::Icons.register}).

  • raise_on_missing_icon (nil) - the policy for a dynamic icon

name that resolves to nothing: nil raises in local environments and degrades to the fallback elsewhere; true/false force one behavior.

  • icon_fallback (:"circle-question-mark") - rendered instead

of a missing icon when not raising; nil re-raises.

  • on_missing_icon (nil) - an optional callable

(name:, library:, error:) fired before the fallback renders.

  • webmcp_registration_budget (20) - the per-document cap on

WebMCP tool registrations a page's opted-in instances may make; poetry-agent's registration_budget setting writes through.

  • stable_id_mode (:off) - the opt-in :sequence mode seeds a

per-request deterministic id sequence (read the hazards in StableId before enabling).

  • stable_id_seed - the request-to-seed callable for that mode

(defaults to the request path).

Returns (ActiveSupport::OrderedOptions) — the default configuration

defaults = Poetry::Core::Config.defaults
defaults.classname_merger # => #<Poetry::Core::CSS::TailwindMerger:0x00007f8b1c0a3b40>
defaults.css_mode # => :tailwind

#classname_merger

The merger that resolves conflicting utility classes when caller classes meet component classes.

Returns (Poetry::Core::CSS::TailwindMerger) — The CSS class merger instance

#classname_merger=(merger)

Replaces the class merger.

  • merger (Poetry::Core::CSS::TailwindMerger) — The CSS class merger instance to use

#css_mode

The class emission mode: :tailwind resolves style values to utility classes, :bem emits the BEM token IR.

Returns (Symbol) — :tailwind or :bem

#css_mode=(mode)

Sets the class emission mode.

  • mode (Symbol) — :tailwind or :bem

#icon_fallback

The icon rendered instead of a missing one when not raising; nil re-raises. Must exist in every registered set.

#icon_fallback=(name)

Sets the fallback icon.

  • name (Symbol, nil)

#icon_library

The key of the active icon set ({Poetry::Core::Icons.register}).

#icon_library=(key)

Selects the active icon set.

  • key (Symbol, String) — a registered library key

#initialize

Initializes a new configuration instance with default values.

The new instance gets a clone of the default configuration, ensuring each configuration object is independent and modifications won't affect the defaults or other instances.

Returns (Poetry::Core::Config) — A new configuration instance

config = Poetry::Core::Config.new
config.css_mode = :bem
Poetry::Core::Config.current.css_mode # => :tailwind (unchanged)

#on_missing_icon

The instrumentation hook fired before a fallback icon renders, called with name:, library:, and error:.

#on_missing_icon=(callable)

Sets the missing-icon hook.

  • callable (#call, nil)

#raise_on_missing_icon

The policy for a dynamic icon name that resolves to nothing: nil raises in local environments and degrades to the fallback elsewhere; true/false force one behavior.

#raise_on_missing_icon=(policy)

Sets the missing-icon policy.

  • policy (Boolean, nil)

#stable_id_mode

The StableId sequence mode: :off, or the opt-in :sequence.

#stable_id_mode=(mode)

Sets the StableId sequence mode.

  • mode (Symbol) — :off or :sequence

#stable_id_seed

The request-to-seed callable the :sequence mode derives its per-request id sequence from.

#stable_id_seed=(callable)

Sets the seed callable.

  • callable (#call) — receives the request, returns the seed

#stimulus_merger

The merger that combines Stimulus data attributes without duplicating controllers or actions.

Returns (Poetry::Core::Stimulus::Merger) — The Stimulus attribute merger instance

#stimulus_merger=(merger)

Replaces the Stimulus attribute merger.

  • merger (Poetry::Core::Stimulus::Merger) — The Stimulus attribute merger instance to use

#webmcp_registration_budget

The per-document WebMCP registration budget the registrar enforces (default 20).

#webmcp_registration_budget=(count)

Sets the per-document WebMCP registration budget.

  • count (Integer)

Poetry::Core::Contrib

module · lib/poetry/core/contrib/wrapped_helper.rb

Opt-in helper mixins shipped alongside the core.

Poetry::Core::Contrib::WrappedHelper

module · lib/poetry/core/contrib/wrapped_helper.rb

Provides a convenient method to wrap components with custom HTML code. Adapted from an MIT-licensed source (source and license in THIRD_PARTY_NOTICES.md).

This module adds the #wrapped method to components, allowing them to be easily wrapped with a {Poetry::Core::Wrapper::Component}. The wrapper component enables adding custom HTML around a component without modifying the component itself, and respects the component's render? conditional logic.

class MyComponent < Poetry::Core::Component
  # WrappedHelper is already included via Poetry::Core::Component
end

# In a view or template
component = MyComponent.new(title: "Hello")
wrapper = component.wrapped
<%# app/components/my_component/component.html.erb %>
<% component = MyComponent.new(title: "Hello") %>
<%= render component.wrapped do |wrapper| %>
  <div class="custom-wrapper">
    <h2>Wrapped Content:</h2>
    <%= wrapper.component %>
  </div>
<% end %>
# The wrapper only renders if the wrapped component's render? returns true
class ConditionalComponent < Poetry::Core::Component
  def render?
    @show_content
  end
end

# If @show_content is false, neither the wrapper nor component will render
<%= render component.wrapped do |wrapper| %>
  <div class="wrapper"><%= wrapper.component %></div>
<% end %>

#wrapped

Wraps the current component instance in a {Poetry::Core::Wrapper::Component}.

This creates a wrapper that can be rendered with custom HTML surrounding the component. The wrapper respects the wrapped component's render? method, only rendering if it returns true.

Returns (Poetry::Core::Wrapper::Component) — a wrapper component containing self

component = MyComponent.new
wrapper = component.wrapped
wrapper.component_instance # => the original component

Poetry::Core::Engine

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

The Rails engine: wires the component autoload paths, previews, importmap pins, asset paths, and the StableId / TagHelper mixins into a host app at boot.

Poetry::Core::Error

class < StandardError · lib/poetry/core/errors.rb

Base error class for all Poetry::Core-related errors.

All custom errors in the Poetry::Core module inherit from this class, which in turn inherits from Ruby's StandardError. This provides a common ancestor for rescuing all Poetry::Core-specific exceptions.

begin
  # Poetry::Core operations
rescue Poetry::Core::Error => e
  Rails.logger.error("Poetry::Core error: #{e.message}")
end

Poetry::Core::HTML

module · lib/poetry/core/html/attributes.rb

HTML attribute plumbing: the merge-aware attributes hash.

Poetry::Core::HTML::Attributes

class < ActiveSupport::HashWithIndifferentAccess · lib/poetry/core/html/attributes.rb

A specialized hash for managing HTML attributes with intelligent merging capabilities.

This class extends ActiveSupport::HashWithIndifferentAccess to provide enhanced functionality for handling HTML attributes, particularly CSS classes, Stimulus controllers/actions, data attributes, and ARIA attributes.

Features:

  • Smart merging of CSS classes without duplication
  • Intelligent merging of Stimulus controllers and actions
  • Automatic flattening of nested data and aria attributes
  • Proper handling of HTML boolean attributes
  • Both mutating (!) and non-mutating versions of merge methods
attrs = Poetry::Core::HTML::Attributes.new(class: "btn", disabled: true)
attrs.merge_classes!("btn-primary")
attrs.to_attributes # => { "class" => "btn btn-primary", "disabled" => "disabled" }
attrs = Poetry::Core::HTML::Attributes.new
attrs.merge_stimulus_controllers!("dropdown", "modal")
attrs.to_attributes # => { "data-controller" => "dropdown modal" }
attrs = Poetry::Core::HTML::Attributes.new(data: { id: 1, name: "test" })
attrs.to_attributes # => { "data-id" => "1", "data-name" => "test" }
Constant Description
BOOLEAN_ATTRIBUTES List of HTML boolean attributes that should be rendered as attribute name only when truthy, or omitted when falsy.

.merged(*hashes)

The safe way to combine component wiring with caller-supplied attributes into a plain hash for content_tag / button_to: every hash flows through one Attributes instance, so stimulus keys (data-controller / data-action, either spelling) concatenate instead of clobbering, classes tailwind-merge, and to_attributes unifies double-spelled slots deterministically. Plain Hash#merge of wiring with caller options silently drops one side's wiring - never do that; call this.

  • hashes (Array<Hash, nil>) — wiring first, caller last

Returns (Hash) — flat, render-ready attributes

#get_attribute(key, nested_key = nil)

Gets an attribute value, handling both flat and nested formats.

  • key (String, Symbol) — The attribute key
  • nested_key (String, Symbol, nil) — Optional nested key for data/aria attributes

Returns (Object, nil) — The attribute value or nil if not found

attrs = Poetry::Core::HTML::Attributes.new(data: { controller: "dropdown" })
attrs.get_attribute("data", "controller") # => "dropdown"
attrs = Poetry::Core::HTML::Attributes.new("data-controller" => "dropdown")
attrs.get_attribute("data", "controller") # => "dropdown"

#has_attribute?(key, nested_key = nil)

Checks if an attribute is set, handling both flat and nested formats.

This method is smart about data and aria attributes, checking both the nested and flat formats.

  • key (String, Symbol) — The attribute key to check
  • nested_key (String, Symbol, nil) — Optional nested key for data/aria attributes

Returns (Boolean) — True if the attribute exists

attrs = Poetry::Core::HTML::Attributes.new(class: "btn", id: "my-btn")
attrs.has_attribute?(:class) # => true
attrs.has_attribute?(:disabled) # => false
attrs = Poetry::Core::HTML::Attributes.new(data: { controller: "dropdown" })
attrs.has_attribute?("data", "controller") # => true
attrs.has_attribute?("data", "action") # => false
attrs = Poetry::Core::HTML::Attributes.new("data-controller" => "dropdown")
attrs.has_attribute?("data", "controller") # => true

#merge_classes(*classnames)

Merges CSS classes into the attributes, returning a new instance.

This non-mutating method creates a deep copy of the attributes and merges the provided classnames intelligently, avoiding duplicates and handling conditional classes.

  • classnames (Array<String, Hash, Array>) — One or more classnames to merge. Can be strings, hashes with conditional classes, or arrays.

Returns (Poetry::Core::HTML::Attributes) — A new instance with merged classes

attrs = Poetry::Core::HTML::Attributes.new(class: "btn")
new_attrs = attrs.merge_classes("btn-primary", "btn-lg")
new_attrs["class"] # => "btn btn-primary btn-lg"
attrs["class"] # => "btn" (original unchanged)

#merge_classes!(*classnames)

Merges CSS classes into the attributes, mutating the current instance.

This mutating method modifies the current attributes object by merging the provided classnames.

  • classnames (Array<String, Hash, Array>) — One or more classnames to merge

Returns (Poetry::Core::HTML::Attributes) — Self for method chaining

attrs = Poetry::Core::HTML::Attributes.new(class: "btn")
attrs.merge_classes!("btn-primary")
attrs["class"] # => "btn btn-primary"

#merge_if_not_set(other_hash)

Merges attributes only if they are not already set, returning a new instance.

This method intelligently merges attributes by only adding keys that don't exist in the current attributes. It handles both flat and nested data attributes:

  • "data-controller" and data: { controller: "..." } are treated as the same
  • "aria-label" and aria: { label: "..." } are treated as the same
  • other_hash (Hash) — Hash of attributes to merge if not set

Returns (Poetry::Core::HTML::Attributes) — A new instance with conditionally merged attributes

attrs = Poetry::Core::HTML::Attributes.new(class: "btn", data: { controller: "dropdown" })
defaults = { class: "btn-default", data: { action: "click->modal#open" }, id: "my-btn" }
new_attrs = attrs.merge_if_not_set(defaults)
# Result: class: "btn", data: { controller: "dropdown", action: "click->modal#open" }, id: "my-btn"
# Note: "btn-default" not added because class was already set
# Note: action added because it wasn't set, but controller kept original value

#merge_if_not_set!(other_hash)

Merges attributes only if they are not already set, mutating the current instance.

  • other_hash (Hash) — Hash of attributes to merge if not set

Returns (Poetry::Core::HTML::Attributes) — Self for method chaining

attrs = Poetry::Core::HTML::Attributes.new(data: { controller: "dropdown" })
attrs.merge_if_not_set!(class: "btn", data: { controller: "modal", action: "click->dropdown#toggle" })
# Result: class: "btn", data: { controller: "dropdown", action: "click->dropdown#toggle" }

#merge_stimulus(*stimulus_hash, &)

Merges Stimulus data attributes, returning a new instance.

This method intelligently merges data attributes, handling controllers, actions, and other data attributes appropriately.

  • stimulus_hash (Array<Hash>) — One or more hashes of Stimulus data attributes

Returns (Poetry::Core::HTML::Attributes) — A new instance with merged Stimulus data

attrs = Poetry::Core::HTML::Attributes.new(data: { controller: "dropdown" })
new_attrs = attrs.merge_stimulus({ action: "click->modal#open", target: "output" })
new_attrs["data"] # => { "controller" => "dropdown", "action" => "click->modal#open", "target" => "output" }

#merge_stimulus!(*stimulus_hash, &)

Merges Stimulus data attributes, mutating the current instance.

  • stimulus_hash (Array<Hash>) — One or more hashes of Stimulus data attributes

Returns (Poetry::Core::HTML::Attributes) — Self for method chaining

attrs = Poetry::Core::HTML::Attributes.new
attrs.merge_stimulus!({ controller: "dropdown", action: "click->dropdown#toggle" })
attrs["data"] # => { "controller" => "dropdown", "action" => "click->dropdown#toggle" }

#merge_stimulus_actions(*actions)

Merges Stimulus actions into the data-action attribute, returning a new instance.

  • actions (Array<String, Hash>) — One or more action strings or hashes

Returns (Poetry::Core::HTML::Attributes) — A new instance with merged actions

attrs = Poetry::Core::HTML::Attributes.new
new_attrs = attrs.merge_stimulus_actions("click->modal#open", "keydown->modal#close")
new_attrs["data"]["action"] # => "click->modal#open keydown->modal#close"

#merge_stimulus_actions!(*actions)

Merges Stimulus actions into the data-action attribute, mutating the current instance.

  • actions (Array<String, Hash>) — One or more action strings

Returns (Poetry::Core::HTML::Attributes) — Self for method chaining

attrs = Poetry::Core::HTML::Attributes.new(data: { action: "click->modal#open" })
attrs.merge_stimulus_actions!("keydown->modal#close")
attrs["data"]["action"] # => "click->modal#open keydown->modal#close"

#merge_stimulus_controllers(*controllers)

Merges Stimulus controllers into the data-controller attribute, returning a new instance.

  • controllers (Array<String, Hash>) — One or more controller names or hashes with controller names and options

Returns (Poetry::Core::HTML::Attributes) — A new instance with merged controllers

attrs = Poetry::Core::HTML::Attributes.new
new_attrs = attrs.merge_stimulus_controllers("dropdown", "modal")
new_attrs["data"]["controller"] # => "dropdown modal"

#merge_stimulus_controllers!(*controllers)

Merges Stimulus controllers into the data-controller attribute, mutating the current instance.

  • controllers (Array<String, Hash>) — One or more controller names

Returns (Poetry::Core::HTML::Attributes) — Self for method chaining

attrs = Poetry::Core::HTML::Attributes.new(data: { controller: "dropdown" })
attrs.merge_stimulus_controllers!("modal")
attrs["data"]["controller"] # => "dropdown modal"

#to_attributes

Converts the attributes hash to a flat hash suitable for HTML rendering.

This method performs several transformations:

  • Flattens nested data attributes (data: { id: 1 } => "data-id" => "1")
  • Flattens nested aria attributes (aria: { label: "Close" } => "aria-label" => "Close")
  • Handles boolean attributes (disabled: true => "disabled" => "disabled")
  • Skips nil values for all attributes
  • Converts complex values to JSON strings when appropriate

When both spellings of the same attribute coexist in the store (flat "data-x" AND nested data: { x: }), the output is deterministic instead of insertion-order roulette: stimulus keys (data-controller / data-action) CONCATENATE so wiring is never lost, and every other duplicate resolves flat-spelling-wins (the normalize_flat_attributes! convention).

Returns (Hash<String, String>) — A flat hash of HTML attribute names to values

attrs = Poetry::Core::HTML::Attributes.new(
  class: "btn btn-primary",
  disabled: true,
  id: nil,
  data: { id: 1, controller: "dropdown", target: nil },
  aria: { label: "Close", hidden: nil }
)
attrs.to_attributes
# => {
#   "class" => "btn btn-primary",
#   "disabled" => "disabled",
#   "data-id" => "1",
#   "data-controller" => "dropdown",
#   "aria-label" => "Close"
# }

Poetry::Core::IconNotFound

class < Poetry::Core::Error · lib/poetry/core/errors.rb

Raised by an icon set's #fetch for a name it cannot serve - malformed, or simply not in the set. This is the public rescue point of the icon system: the Icon component's missing-icon policy rescues exactly this class, so a custom set registered via {Poetry::Core::Icons.register} must raise it too (that is the set contract).

begin
  Poetry::Core::Icons.set.fetch(user_preference)
rescue Poetry::Core::IconNotFound => e
  logger.info("missing icon #{e.name.inspect}") # e.suggestion may name the fix
end

#initialize(message, name:, suggestion: nil)

Carries the requested name (and the did-you-mean fix, when one exists) alongside the message.

  • message (String) — the full error message
  • name (Symbol, String) — the requested icon name
  • suggestion (String, nil) — the closest valid name, when known

Returns (IconNotFound) — a new instance of IconNotFound

#name

Returns (Symbol, String) — the requested icon name, as given

#suggestion

Returns (String, nil) — the closest valid name, when one exists

Poetry::Core::Icons::FileSet

class · lib/poetry/core/icons.rb

A directory of vendored, pre-sanitized icon files - one <name>.svg per icon holding the INNER markup (the component owns the <svg> wrapper). Reads are memory-cached; names are validated against a strict format before touching the filesystem (icon names can carry user input - no path traversal).

Constant Description
NAME_FORMAT The kebab-case shape every icon name must match before it touches the filesystem.

#dir

Returns the value of attribute dir.

#fetch(name)

The inner SVG markup of one icon, read once and cached.

  • name (Symbol, String) — the icon name

Returns (String) — the icon's inner SVG markup

#include?(name)

Whether the set has an icon of this name: the name must be well-formed and its SVG present on disk.

  • name (Symbol, String) — the icon name

#initialize(dir:)

A set over one directory of sanitized SVG files, one file per icon name (circle-alert.svg); each icon is read once and cached.

  • dir (String, Pathname) — the directory holding the SVGs

Returns (FileSet) — a new instance of FileSet

#names

Every icon name in the set, sorted.

Poetry::Core::Registry::Committed

class · lib/poetry/core/registry.rb

The committed-registry view: the four generated sections plus the root they resolve against, read straight from the YAML a registry:generate run committed - no component classes, no Rails. It satisfies every LlmsText/SkillText read (entries / blocks / source_root), so boot-free consumers (the MCP server, runtime skill delivery) share one loader instead of each parsing the payload.

#blocks

Returns the value of attribute blocks.

#entries

Returns the value of attribute entries.

#form_builder

Returns the value of attribute form_builder.

#helper_args

Returns the value of attribute helper_args.

#helpers

Returns the value of attribute helpers.

#initialize(entries:, blocks:, helpers:, helper_args:, source_root:, form_builder: nil)

Holds the sections of one committed registry file; {Registry.committed} is the loader.

  • entries (Array<Hash>) — the "components" section
  • blocks (Array<Hash>, nil) — the "blocks" section
  • helpers (Hash, nil) — the "helpers" section
  • helper_args (Hash, nil) — the "helper_args" section
  • source_root (Pathname) — the gem root the paths resolve against
  • form_builder (Hash, nil) — the "form_builder" section

Returns (Committed) — a new instance of Committed

#source_root

Returns the value of attribute source_root.

Poetry::Core::RegistryAddress

class · lib/poetry/core/registry_address.rb

The uniform registry address scheme: no --from flag, ONE classifier for every generator argument and every registryDependencies entry. An address is exactly one of:

https://acme.dev/r/fancy-chart.json :url any endpoint ./registry/fancy-chart.json :file a local item file @acme/fancy-chart :namespace a configured registry button / Button / input_group :bare the installed gems

Item names normalize to kebab-case everywhere (InputGroup and input_group are both input-group) - the block catalog's existing naming, and the wider registry ecosystem's.

Poetry::Core::RegistryAddress.parse("@acme/fancy-chart").kind # => :namespace
Constant Description
BARE The bare item-name address shape (the installed gems).
NAMESPACED The @namespace/item-name address shape (@acme/fancy-chart): a registry namespace, a slash, an item name.

.normalize(name)

CamelCase / snake_case / kebab-case all land on the kebab item name.

  • name (String) — an item name in any of the three spellings

Returns (String) — the kebab-case item name

.parse(raw)

Parses one address into its kind: http(s):// is a :url, @x/y a :namespace, a .json path or a ./, ../, /, ~ prefix a :file, and a plain item name a :bare address.

  • raw (String, #to_s) — the address as typed

.parse_namespaced(raw)

Parses an @namespace/item-name address.

  • raw (String) — the address, already known to start with @

Returns (RegistryAddress) — a :namespace address

#initialize(kind:, raw:, namespace: nil, name: nil, location: nil)

Builds a frozen address; {.parse} is the usual entry point.

  • kind (Symbol) — :bare, :namespace, :url, or :file
  • raw (String) — the address as typed
  • namespace (String, nil) — the @registry part of a :namespace address
  • name (String, nil) — the normalized item name (:bare, :namespace)
  • location (String, nil) — the URL or path (:url, :file)

Returns (RegistryAddress) — a new instance of RegistryAddress

#kind

Returns the value of attribute kind.

#location

Returns the value of attribute location.

#name

Returns the value of attribute name.

#namespace

Returns the value of attribute namespace.

#raw

Returns the value of attribute raw.

#remote?

Whether the item has to be fetched rather than found among the installed gems - every kind but :bare.

#sibling(dep_name)

The address of a bare dependency named inside a parent item - the sibling convention: an @acme item's bare deps are @acme items; a url/file item's bare deps sit next to it.

  • dep_name (String) — the dependency's bare item name

Poetry::Core::Stimulus

module · lib/poetry/core/stimulus/merger.rb

The Stimulus layer: builders, declarations, and attribute merging.

Poetry::Core::Stimulus::Builder

class · lib/poetry/core/stimulus/builder.rb

Builder class for constructing Stimulus controller HTML attributes in a Ruby-friendly way.

This class provides a clean API for adding Stimulus data attributes to HTML elements without manually constructing attribute strings. It handles:

  • Controller registration
  • Values (data passed to controllers)
  • CSS class references (for Stimulus classes API)
  • Outlets (connections to other controllers)
  • Actions (event listeners)
  • Custom parameters
builder = Poetry::Core::Stimulus::Builder.new("dropdown", html_attributes)
builder.register_controller
builder.with_value(:open, false)
builder.with_action(:toggle, on: :click)
# Produces: data-controller="dropdown"
#           data-dropdown-open-value="false"
#           data-action="click->dropdown#toggle"
builder = Poetry::Core::Stimulus::Builder.new("menu", html_attributes,
  values: { visible: true },
  actions: { show: :mouseenter, hide: :mouseleave },
  classes: { active: "bg-blue-500" }
)
Constant Description
EVENT_ALIASES Event aliases for common event combinations

.format_identifier(identifier)

Formats an identifier by converting underscores to dashes

  • identifier (String, Symbol, Array) — The identifier(s) to format

Returns (String) — The formatted identifier

format_identifier(:my_controller)
# => "my-controller"
format_identifier([:admin, :dropdown])
# => "admin--dropdown"

#action(method, on: nil, at: nil)

Builds a Stimulus action descriptor string without adding it to the attributes ({#with_action} adds it).

  • method (String, Symbol) — The controller method to call
  • on (Symbol, String, Array<Symbol>, nil) — The event(s) to listen for
  • at (Symbol, String, nil) — The event target (e.g., :window, :document)

Returns (String) — The formatted action string

builder.action(:toggle)
# => "dropdown#toggle"
builder.action(:toggle, on: :click)
# => "click->dropdown#toggle"
builder.action(:close, on: :keydown, at: :window)
# => "keydown@window->dropdown#close"
builder.action(:show, on: [:mouseenter, :focus])
# => "mouseenter->dropdown#show focus->dropdown#show"

#event(event)

Builds a custom Stimulus event name

  • event (String, Symbol) — The event name

Returns (String) — The namespaced event name

builder.event(:opened)
# => "dropdown:opened"
// Dispatch custom event: this.dispatch("opened")
// Listen in HTML: data-action="dropdown:opened->other#handleOpened"

#html_attributes

Returns (Object) — The HTML attributes object that will be modified

#identifier

Returns (String) — The formatted Stimulus controller identifier

#initialize(identifier, html_attributes, options = {})

Creates a new Stimulus builder instance

  • identifier (String, Symbol, Array) — The Stimulus controller identifier(s)
  • html_attributes (Object) — An object that responds to merge_stimulus! methods
  • options (Hash) — Optional configurations

Returns (Builder) — a new instance of Builder

Builder.new("dropdown", html_attributes)
Builder.new([:admin, :dropdown], html_attributes)
Builder.new("modal", html_attributes,
  values: { open: false, size: "large" },
  actions: { toggle: :click, close: { on: :keydown, at: :window } }
)

#param_attribute_name(name)

Returns the attribute name for a parameter

  • name (String, Symbol) — The parameter name

Returns (Symbol) — The parameter attribute name

builder.param_attribute_name(:size)
# => :dropdown_size_param

#register_controller

Registers this controller in the data-controller attribute

builder.register_controller
# Adds: data-controller="dropdown"

#target(name)

Validates and returns the JS target name

  • name (String, Symbol) — The target name

Returns (String) — The camelCase target name

#target_attribute_name

Returns the attribute name for a Stimulus target

Returns (Symbol) — The target attribute name (e.g., :dropdown_target)

builder.target_attribute_name
# => :dropdown_target

#with_action(method, on: nil, at: nil)

Adds a Stimulus action (event listener) to the HTML attributes

  • method (String, Symbol) — The controller method to call
  • on (Symbol, String, Array<Symbol>, nil) — The event(s) to listen for
  • at (Symbol, String, nil) — The event target (e.g., :window, :document)
builder.with_action(:toggle, on: :click)
# Adds: data-action="click->dropdown#toggle"
builder.with_action(:show, on: :hover_in)
# Adds: data-action="mouseenter->dropdown#show focus->dropdown#show"
builder.with_action(:close, on: :keydown, at: :window)
# Adds: data-action="keydown@window->dropdown#close"

#with_class(name, value)

Adds a Stimulus class reference to the HTML attributes

Classes are used to reference CSS classes that the controller can toggle.

  • name (String, Symbol) — The name of the class reference
  • value (String) — The CSS class name(s)
builder.with_class(:active, "bg-blue-500 text-white")
# Adds: data-dropdown-active-class="bg-blue-500 text-white"

#with_outlet(name, value)

Adds a Stimulus outlet reference to the HTML attributes

Outlets allow one controller to reference and interact with other controllers.

  • name (String, Symbol) — The name of the outlet
  • value (String) — The outlet selector
builder.with_outlet(:modal, ".modal-controller")
# Adds: data-dropdown-modal-outlet=".modal-controller"

#with_param(name, value)

Adds an action parameter attribute (data-<identifier>-<name>-param), which the controller reads from event.params.

  • name (String, Symbol) — The parameter name
  • value (Object) — The parameter value
builder.with_param(:size, "large")
# Adds: data-dropdown-size-param="large"

#with_target(name)

Adds a Stimulus target attribute to the HTML attributes

  • name (String, Symbol) — The target name (snake_case camelizes)
builder.with_target(:dialog)
# Adds: data-dropdown-target="dialog"

#with_value(name, value)

Adds a Stimulus value to the HTML attributes

Values are used to pass data from HTML to Stimulus controllers.

  • name (String, Symbol) — The name of the value
  • value (Object) — The value to set (will be JSON-encoded if necessary)
builder.with_value(:open, false)
# Adds: data-dropdown-open-value="false"

Poetry::Core::Stimulus::Declarations::DeclarationError

class < Poetry::Core::Error · lib/poetry/core/stimulus/declarations.rb

Raised at class load for an invalid or manifest-unknown declaration.

Poetry::Core::Stimulus::Declarations::Element

class < Struct · lib/poetry/core/stimulus/declarations.rb

One declared element (:root or a named part) and the per-controller wirings attached to it.

#conditions

Returns the value of attribute conditions

Returns (Object) — the current value of conditions

#conditions=(value)

Sets the attribute conditions

  • value (Object) — the value to set the attribute conditions to.

Returns (Object) — the newly set value

#extend_inherited

Returns the value of attribute extend_inherited

Returns (Object) — the current value of extend_inherited

#extend_inherited=(value)

Sets the attribute extend_inherited

  • value (Object) — the value to set the attribute extend_inherited to.

Returns (Object) — the newly set value

#name

Returns the value of attribute name

Returns (Object) — the current value of name

#name=(value)

Sets the attribute name

  • value (Object) — the value to set the attribute name to.

Returns (Object) — the newly set value

#wirings

Returns the value of attribute wirings

Returns (Object) — the current value of wirings

#wirings=(value)

Sets the attribute wirings

  • value (Object) — the value to set the attribute wirings to.

Returns (Object) — the newly set value

Poetry::Core::Stimulus::Declarations::ElementDSL

class · lib/poetry/core/stimulus/declarations.rb

Inside on :element do ... end: {#controller} attaches one controller's wiring to the element.

#controller(identifier, **options, &block)

Wires one Stimulus controller to this element.

  • identifier (Symbol, String, Array) — a Symbol resolves against the controllers manifest by unique suffix (:hover_card); pass a String for a host-app controller
  • options (Hash) — if:/unless: conditions (Symbol predicate or Proc)

#event(controller, name)

A validated cross-controller event name - see {Declarations.event_name}.

  • controller (Symbol, String) — the dispatching controller
  • name (Symbol, String) — the event's short name

Returns (String) — the full emitted event name

#initialize(declaring, element)

Binds the DSL to the element its block fills.

  • declaring (String) — the declaring class's name, for error messages
  • element (Element) — the element being wired

Returns (ElementDSL) — a new instance of ElementDSL

Poetry::Core::Stimulus::Declarations::Entry

class < Struct · lib/poetry/core/stimulus/declarations.rb

kind: :register | :value | :action | :target. Values carry source {type: :implicit|:literal|:method, value:}; actions carry on:/at:. conditions is nil or {if:/unless: Symbol|Proc}.

#at

Returns the value of attribute at

Returns (Object) — the current value of at

#at=(value)

Sets the attribute at

  • value (Object) — the value to set the attribute at to.

Returns (Object) — the newly set value

#conditions

Returns the value of attribute conditions

Returns (Object) — the current value of conditions

#conditions=(value)

Sets the attribute conditions

  • value (Object) — the value to set the attribute conditions to.

Returns (Object) — the newly set value

#kind

Returns the value of attribute kind

Returns (Object) — the current value of kind

#kind=(value)

Sets the attribute kind

  • value (Object) — the value to set the attribute kind to.

Returns (Object) — the newly set value

#name

Returns the value of attribute name

Returns (Object) — the current value of name

#name=(value)

Sets the attribute name

  • value (Object) — the value to set the attribute name to.

Returns (Object) — the newly set value

#on

Returns the value of attribute on

Returns (Object) — the current value of on

#on=(value)

Sets the attribute on

  • value (Object) — the value to set the attribute on to.

Returns (Object) — the newly set value

#source

Returns the value of attribute source

Returns (Object) — the current value of source

#source=(value)

Sets the attribute source

  • value (Object) — the value to set the attribute source to.

Returns (Object) — the newly set value

Poetry::Core::Stimulus::Declarations::RootDSL

class · lib/poetry/core/stimulus/declarations.rb

Evaluates one use_stimulus block; #elements is the harvest. The block's vocabulary is {#on} (declare an element) and {#event} (build a validated event name).

#elements

Returns the value of attribute elements.

#event(controller, name)

A validated cross-controller event name - see {Declarations.event_name}.

  • controller (Symbol, String) — the dispatching controller
  • name (Symbol, String) — the event's short name

Returns (String) — the full emitted event name

#initialize(declaring)

Starts an empty harvest for one declaring class.

  • declaring (String) — the declaring class's name, for error messages

Returns (RootDSL) — a new instance of RootDSL

#on(name, extend: false, **options, &block)

Declares the wiring of one element of the component's anatomy. :root is the component root; other names match the element keys templates read back through stimulus_attributes_for. Redeclaring an element in a subclass replaces it wholesale unless extend: true merges into the inherited wiring.

  • name (Symbol, String) — the element to wire (:root, :trigger, ...)
  • extend (Boolean) — merge into the inherited element instead of replacing it
  • options (Hash) — if:/unless: conditions (Symbol predicate or Proc)
on :trigger do
  controller :popper do
    target :anchor
  end
end

Poetry::Core::Stimulus::Declarations::Wiring

class < Struct · lib/poetry/core/stimulus/declarations.rb

:entries shadows Enumerable#entries, which Wiring never uses - the member is literally a list of Entry structs, so the natural name wins.

#conditions

Returns the value of attribute conditions

Returns (Object) — the current value of conditions

#conditions=(value)

Sets the attribute conditions

  • value (Object) — the value to set the attribute conditions to.

Returns (Object) — the newly set value

#entries

Returns the value of attribute entries

Returns (Object) — the current value of entries

#entries=(value)

Sets the attribute entries

  • value (Object) — the value to set the attribute entries to.

Returns (Object) — the newly set value

#identifier

Returns the value of attribute identifier

Returns (Object) — the current value of identifier

#identifier=(value)

Sets the attribute identifier

  • value (Object) — the value to set the attribute identifier to.

Returns (Object) — the newly set value

Poetry::Core::Stimulus::Declarations::WiringDSL

class · lib/poetry/core/stimulus/declarations.rb

Inside controller :name do ... end: the wiring vocabulary. {#register} boots the controller on the element; {#value}, {#action}, and {#target} declare the data attributes the render emits - each name validated against the controllers manifest at class load.

#action(method, on: nil, at: nil, **options)

Declares one action the render emits into this element's data-action. on: nil declares a BARE descriptor (Stimulus element-default event - the forwarding shape: "poetry--core--x#method").

  • method (Symbol) — the controller method (validated against the controllers manifest; declared snake_case, emitted lowerCamel)
  • on (Symbol, String, Array<Symbol>, nil) — the event(s) to listen for
  • at (Symbol, String, nil) — the event target (:window, :document)
  • options (Hash) — if:/unless: conditions (Symbol predicate or Proc)

#event(controller, name)

A validated cross-controller event name - see {Declarations.event_name}.

  • controller (Symbol, String) — the dispatching controller
  • name (Symbol, String) — the event's short name

Returns (String) — the full emitted event name

#initialize(declaring, wiring)

Binds the DSL to one controller wiring and looks up the controller's manifest definition the entries validate against.

  • declaring (String) — the declaring class's name, for error messages
  • wiring (Wiring) — the controller wiring the entries collect into

Returns (WiringDSL) — a new instance of WiringDSL

#register(**options)

Emits the controller's identifier into this element's data-controller - a controller instance boots here. Value, action, and target entries alone never register a controller.

  • options (Hash) — if:/unless: conditions (Symbol predicate or Proc)

#target(name, **options)

Marks this element as one of the controller's named targets.

  • name (Symbol) — the target name (validated against the controllers manifest; declared snake_case, emitted lowerCamel)
  • options (Hash) — if:/unless: conditions (Symbol predicate or Proc)

#value(name, *literal, from: nil, **options)

Declares one Stimulus value the render emits as data-<identifier>-<name>-value. Three source shapes:

value :open # reads the same-named method/option value :orientation, :horizontal # literal value :selected, from: :selected_iso # named method reference

Literal presence is arity-detected, so value :x, false and value :x, nil stay literals.

  • name (Symbol) — the value name (validated against the controllers manifest)
  • literal (Array) — at most one literal value
  • from (Symbol, nil) — the method the value reads at render
  • options (Hash) — if:/unless: conditions (Symbol predicate or Proc)

Poetry::Core::Stimulus::Manifest

module · lib/poetry/core/stimulus/manifest.rb

The controllers manifest: the JS-side API surface (targets / values / classes / methods) introspected from the live controller classes in CI (test/javascript/controllers_manifest.test.js, regenerated with npm run manifest) and committed at config/controllers_manifest.json.

The Builder validates every name it emits against this, so a renamed controller method can never silently strand gem-rendered wiring - the Ruby<->JS seam is guarded at render time.

Policy: poetry-namespaced identifiers ("poetry--*") are validated strictly (unknown one raises); host-app controllers are unknown to poetry and pass through unvalidated.

Constant Description
POETRY_PREFIX The identifier prefix marking a controller as poetry-owned (and therefore manifest-validated).

.catalog

The merged controller catalog, identifier => definition ({"targets" =>, "values" =>, "classes" =>, "methods" =>}), loaded from poetry-core's committed manifest on first read.

.definition(identifier)

The catalog definition of one controller.

  • identifier (String) — the full Stimulus identifier ("poetry--core--popper")

Returns (Hash, nil) — the controller's definition; nil for host-app (non-poetry) identifiers, which are not poetry's to validate.

.register(path)

Other poetry gems merge their committed manifests here.

  • path (String, Pathname) — a committed controllers_manifest.json

Returns (Hash{String => Hash}) — the catalog after the merge

Poetry::Core::Stimulus::Manifest::UnknownController

class < Poetry::Core::Error · lib/poetry/core/stimulus/manifest.rb

Raised for a poetry-- identifier the manifest does not know.

Poetry::Core::Stimulus::Manifest::UnknownName

class < Poetry::Core::Error · lib/poetry/core/stimulus/manifest.rb

Raised for a target/value/action name the controller's manifest entry does not list.

Poetry::Core::Stimulus::Merger

class · lib/poetry/core/stimulus/merger.rb

Intelligently merges Stimulus controller data attributes from multiple sources.

This class handles the complex task of combining Stimulus data attributes (controllers, actions, targets, values, classes, etc.) without duplicating controllers or actions. This is particularly useful when building components that may have Stimulus attributes from multiple concerns or sources.

merger = Poetry::Core::Stimulus::Merger.new
attrs1 = { data: { controller: "dropdown modal" } }
attrs2 = { data: { controller: "dropdown tooltip" } }
merged = merger.merge_attributes(attrs1, attrs2)
# => { data: { controller: "dropdown modal tooltip" } }
merger = Poetry::Core::Stimulus::Merger.new
attrs1 = { data: { controller: "form", action: "submit->form#save" } }
attrs2 = { data: { action: "keyup->form#validate" } }
merged = merger.merge_attributes(attrs1, attrs2)
# => { data: { controller: "form", action: "submit->form#save keyup->form#validate" } }

#merge(*hashes, &)

Merges multiple stimulus data hashes with special handling for controllers and actions.

This is the main merging method that intelligently combines stimulus data hashes. Controller and action values are deduplicated using their respective merge methods, while other data attributes are merged normally.

  • hashes (Array<Hash>) — One or more stimulus data hashes

Returns (Hash) — A new hash with merged stimulus data

merge(
  { controller: "dropdown", action: "click->dropdown#toggle" },
  { controller: "dropdown tooltip", target_value: "main" }
)
# => { controller: "dropdown tooltip", action: "click->dropdown#toggle", target_value: "main" }

#merge_actions(*actions)

Merges multiple action strings into a single deduplicated string.

Action strings are flattened, filtered for presence, deduplicated, and joined. Unlike controllers, actions maintain their insertion order.

  • actions (Array<String, nil>) — One or more action strings

Returns (String, nil) — Space-separated action strings, or nil if all inputs were blank

merge_actions("click->modal#open", "click->modal#open", "keyup->form#validate")
# => "click->modal#open keyup->form#validate"

#merge_attributes(attributes, *other_attributes)

Merges stimulus attributes non-destructively by deep duplicating the original.

  • attributes (Hash) — The base attributes hash (will not be modified)
  • other_attributes (Array<Hash>) — One or more attribute hashes to merge

Returns (Hash) — A new hash with merged attributes

#merge_attributes!(attributes, *other_attributes)

Merges stimulus attributes in place, modifying the original attributes hash.

  • attributes (Hash) — The base attributes hash to merge into (will be modified)
  • other_attributes (Array<Hash>) — One or more attribute hashes to merge

Returns (Hash) — The modified attributes hash

#merge_controllers(*controllers)

Merges multiple controller strings into a single deduplicated string.

Controller names are split on spaces, deduplicated, and rejoined. Blank or nil values are ignored.

  • controllers (Array<String, nil>) — One or more controller strings

Returns (String, nil) — Space-separated controller names, or nil if all inputs were blank

merge_controllers("dropdown modal", "dropdown tooltip")
# => "dropdown modal tooltip"

Poetry::Core::TagHelper

module · lib/poetry/core/tag_helper.rb

The view-helper seam: included into ActionView by the engine (initializer "poetry_core.tag_helper"). Carries no helpers in core.

Poetry::Core::Tokens::Color

class · lib/poetry/core/tokens/color.rb

An OKLCH color value (with optional alpha) plus the conversion and contrast math the AAA-contrast gate is built on:

OKLCH -> OKLab -> linear sRGB -> gamma sRGB (Björn Ottosson's matrices) WCAG 2.x relative luminance + contrast ratio browser-style alpha compositing (gamma-encoded sRGB blend)

Pure Ruby, no dependencies - cheap enough to run on every CI build.

color = Poetry::Core::Tokens::Color.parse("#1A1C1E")
color.css # => "oklch(0.225 0.005 248.047)"
color.contrast_ratio(Poetry::Core::Tokens::Color::WHITE) # => 17.09...
Constant Description
BLACK Pure black - the dark end of the contrast scale, the counterpart of {WHITE}.
HEX_CSS The hex color forms #parse accepts (3/4/6/8 digits).
OKLCH_CSS The CSS color spellings DESIGN.md files carry across the design-skill ecosystem: poetry emits oklch; foreign-authored files arrive in hex or rgb().
RGB_CSS The rgb()/rgba() color forms #parse accepts.
WHITE Pure white - the literal foreground on destructive surfaces.

.from_dtcg(value)

Build from a DTCG color $value: {"colorSpace" => "oklch", "components" => [l, c, h], "alpha" => 0.1 (optional)}.

  • value (Hash) — the token's $value

.from_srgb(srgb, alpha: 1.0)

Gamma-encoded sRGB [0,1] triplet -> Color, via Ottosson's inverse path (linear sRGB -> LMS -> OKLab -> LCH). Components round to the 3-decimal precision distributed theme files publish.

  • srgb (Array<Numeric>) — gamma-encoded r, g, b, each in [0, 1]
  • alpha (Numeric) — opacity, 0 to 1

.parse(css)

Parse a CSS color string into a Color, or nil for anything else (named colors, var() refs, gradients) - the DESIGN.md importer DROPS what it cannot parse, never guesses. oklch input keeps its components verbatim so poetry-authored values round-trip byte-exact through parse -> css.

  • css (String, #to_s) — a CSS color: oklch(), #hex, rgb()/rgba()

#alpha

Returns the value of attribute alpha.

#c

Returns the value of attribute c.

#composite_over(background)

Alpha-composite this color over an opaque background, the way a browser blends (per-channel, gamma-encoded). Returns a Blend.

  • background (Color, Blend) — the opaque color underneath (anything answering #srgb)

#css

The CSS serialization, matching the distributed themes' formatting: "oklch(0.577 0.245 27.325)" / "oklch(1 0 0 / 10%)".

#h

Returns the value of attribute h.

#initialize(l:, c: 0.0, h: 0.0, alpha: 1.0)

An OKLCH color; every component is stored as a Float.

  • l (Numeric) — lightness, 0 to 1
  • c (Numeric) — chroma
  • h (Numeric) — hue, in degrees
  • alpha (Numeric) — opacity, 0 to 1

Returns (Color) — a new instance of Color

#l

Returns the value of attribute l.

#srgb

Gamma-encoded sRGB components, each clamped to [0, 1].

Returns (Array<Float>) — r, g, b

#with(l: self.l, c: self.c, h: self.h, alpha: self.alpha)

A copy with any component replaced. The import AA-walk moves L in fixed steps while chroma holds - deterministic.

  • l (Numeric) — lightness, 0 to 1
  • c (Numeric) — chroma
  • h (Numeric) — hue, in degrees
  • alpha (Numeric) — opacity, 0 to 1

Poetry::Core::Tokens::Color::Blend

class < Struct · lib/poetry/core/tokens/color.rb

The result of alpha-compositing one color over another: a plain gamma-encoded sRGB triplet that still knows how to measure contrast.

#srgb

Returns the value of attribute srgb

Returns (Object) — the current value of srgb

#srgb=(value)

Sets the attribute srgb

  • value (Object) — the value to set the attribute srgb to.

Returns (Object) — the newly set value

Poetry::Core::Tokens::Color::Contrast

module · lib/poetry/core/tokens/color.rb

WCAG 2.x contrast shared by Color and Blend: relative luminance is computed from gamma-encoded sRGB (the value a browser actually paints).

#contrast_ratio(other)

The WCAG contrast ratio against another color, from 1 to 21.

  • other (#luminance) — a Color or Blend

#luminance

WCAG 2.x relative luminance of the painted color.

Returns (Float) — 0.0 (black) to 1.0 (white)

Poetry::Core::Wrapper

module · app/components/poetry/core/wrapper/component.rb

Namespace of the conditional wrapper: {Wrapper::Component} renders outer HTML around a child component only when the child itself renders.

Poetry::Core::Wrapper::Component

class < ViewComponent::Base · app/components/poetry/core/wrapper/component.rb

Wraps any component with custom HTML. The whole wrapper is only rendered when the child component's #render? returns true, so it can conditionally render the outer HTML for a component without conditionals in templates.

Adapted from an MIT-licensed source (source and license in THIRD_PARTY_NOTICES.md).

The child's render? is consulted before the child gains a view context: a render? that calls view helpers works standalone but not under the wrapper.

render Poetry::Core::Wrapper::Component.new(badge) do |wrapper|
  tag.div(class: "mt-2") { wrapper.component }
end

#call

Returns the block's output, which must have rendered the child - a block that skips {#component} would drop the child silently, so it teaches instead. (An alias couldn't be used here: ViewComponent checks method presence when choosing between #call and a template.)

Returns (ActiveSupport::SafeBuffer, nil) — the block's output

#component

Returns the rendered child component. The name is chosen for convenient usage in templates, so = wrapper.component reads naturally at the spot where the child belongs.

Returns (ActiveSupport::SafeBuffer) — the rendered child HTML

#component_instance

Returns the value of attribute component_instance.

#initialize(component)

Wraps a single child component; intentionally does not chain to ViewComponent::Base#initialize (it only needs the child reference).

  • component (ViewComponent::Base) — the child component to wrap

Returns (Component) — a new instance of Component

Poetry::Core::Wrapper::Component::DoubleRenderError

class < Poetry::Core::Error · app/components/poetry/core/wrapper/component.rb

Raised when the block calls #component more than once - each wrapper renders its child exactly one time.

#initialize(component)

Names the child in the message.

  • component (ViewComponent::Base) — the child rendered twice

Returns (DoubleRenderError) — a new instance of DoubleRenderError

Poetry::Core::Wrapper::Component::UnrenderedChildError

class < Poetry::Core::Error · app/components/poetry/core/wrapper/component.rb

Raised when the wrapper's block never rendered the child - the wrap would silently drop it.

#initialize(component)

Names the dropped child in the message.

  • component (ViewComponent::Base) — the child the block skipped

Returns (UnrenderedChildError) — a new instance of UnrenderedChildError