Dialog
A window overlaid on the page for content that requires attention.
Installation
Included in poetry-ui — available as
poetry_dialog
the moment you've installed Poetry,
with no per-component step. To own the source and edit it, copy it into your app:
bin/rails g poetry:add dialog
Default
<%= poetry_dialog do |dialog| %>
<% dialog.with_trigger(variant: :outline) { "Edit profile" } %>
<% dialog.with_title { "Edit profile" } %>
<% dialog.with_description { "Make changes and save when done." } %>
<% dialog.with_footer do %>
<%= poetry_button { "Save changes" } %>
<% end %>
Profile form goes here.
<% end %>
Confirmation
<%= poetry_dialog(dismissible: false) do |dialog| %>
<% dialog.with_trigger(variant: :destructive) { "Delete account" } %>
<% dialog.with_title { "Are you absolutely sure?" } %>
<% dialog.with_description { "This permanently deletes your account and all data." } %>
<% dialog.with_footer do %>
<%= poetry_button(variant: :destructive) { "Delete" } %>
<% end %>
<% end %>
No close button
<%# show_close_button: false drops the corner X - the forced-choice
recipe. The footer actions do the closing; Esc and (while
dismissible) a backdrop click still work. %>
<%= poetry_dialog(show_close_button: false) do |dialog| %>
<% dialog.with_trigger(variant: :outline) { "Finish setup" } %>
<% dialog.with_title { "One last step" } %>
<% dialog.with_description { "Pick a workspace name to finish. No corner X on this one - choose below, or press Esc." } %>
<% dialog.with_footer do %>
<%= poetry_button(variant: :outline) { "Skip for now" } %>
<%= poetry_button { "Save and continue" } %>
<% end %>
<%= poetry_input(name: "workspace", placeholder: "acme-inc", "aria-label": "Workspace name") %>
<% end %>
Scrollable content
<%= poetry_dialog do |dialog| %>
<% dialog.with_trigger(variant: :outline) { "View release notes" } %>
<% dialog.with_title { "Release notes" } %>
<% dialog.with_description { "Everything that shipped in version 4.0." } %>
<% dialog.with_footer do %>
<%= poetry_button { "Got it" } %>
<% end %>
<div class="max-h-72 overflow-y-auto pr-2 flex flex-col gap-4 text-sm text-muted-foreground">
<p>We rebuilt the sync engine so changes reach every device before you switch tabs. Conflicts now resolve in the background instead of prompting you mid-edit.</p>
<p>Search understands whole phrases now, not just single words. Results are grouped by workspace and ranked by how recently you touched each item.</p>
<p>Boards gained nested columns, so a single view can hold a quarter of planning without turning into a wall of cards.</p>
<p>Keyboard shortcuts are fully remappable from Settings. Import a scheme, tweak the two keys you disagree with, and share the file with your team.</p>
<p>Exports keep their formatting on the way out. Tables stay tables, checklists stay checklists, and links survive the trip into plain markdown.</p>
<p>Notifications learned to batch. A flurry of edits from one teammate arrives as a single digest instead of a dozen separate pings.</p>
<p>Finally, the whole surface got a contrast pass, so text stays legible in bright rooms and dim ones alike.</p>
</div>
<% end %>
Sticky footer
<%= poetry_dialog do |dialog| %>
<% dialog.with_trigger(variant: :outline) { "Manage preferences" } %>
<% dialog.with_title { "Notification preferences" } %>
<% dialog.with_description { "Choose what reaches you. The action row stays pinned while the list scrolls." } %>
<% dialog.with_footer do %>
<%= poetry_button(variant: :outline) { "Cancel" } %>
<%= poetry_button { "Save preferences" } %>
<% end %>
<div class="max-h-64 overflow-y-auto pr-1 flex flex-col gap-4 text-sm">
<div class="flex flex-col gap-1">
<span class="font-medium text-foreground">Mentions</span>
<span class="text-muted-foreground">Someone names you in a comment or task.</span>
</div>
<div class="flex flex-col gap-1">
<span class="font-medium text-foreground">Assignments</span>
<span class="text-muted-foreground">Work is handed to you or a due date shifts.</span>
</div>
<div class="flex flex-col gap-1">
<span class="font-medium text-foreground">Approvals</span>
<span class="text-muted-foreground">A request is waiting on your review.</span>
</div>
<div class="flex flex-col gap-1">
<span class="font-medium text-foreground">Weekly digest</span>
<span class="text-muted-foreground">A Monday summary of everything in flight.</span>
</div>
<div class="flex flex-col gap-1">
<span class="font-medium text-foreground">Product updates</span>
<span class="text-muted-foreground">New features and the occasional release note.</span>
</div>
<div class="flex flex-col gap-1">
<span class="font-medium text-foreground">Security alerts</span>
<span class="text-muted-foreground">Sign-ins from a new device or location.</span>
</div>
</div>
<% end %>
API
Poetry::Ui::Dialog::Component — options are
constructor keywords (the poetry_* helper forwards them);
slots are composed inside the block. Generated from the gem's source documentation.
| Option | Type | Details | Description |
|---|---|---|---|
| content_class: | String | Extra classes merged onto the <dialog> panel (e.g. \"max-h-[50vh]\" caps a top/bottom sheet). | |
| dismissible: | Boolean | defaults to true |
Backdrop clicks close the dialog; false keeps confirmations from being dismissed accidentally (Esc still closes). |
| show_close_button: | Boolean | defaults to true |
Renders the corner X; false forces a deliberate footer choice (footer actions and Esc remain). Sheet inherits this. |
Slots
| Writer | Description |
|---|---|
| with_description | Muted copy under the title, wired to aria-describedby. |
| with_footer | The action row at the bottom of the panel. |
| with_title | The heading - the dialog's accessible name; required. |
| with_trigger | The trigger is a poetry Button wired to open the dialog - agents pass Button props: with_trigger(variant: :outline) { \"Open\" }. |
Styling
Every part carries a stable data-slot attribute — target
[data-slot=…] from your own CSS to restyle it. State rides
data attributes on the parts below. This contract is verified against rendered DOM in CI.
| Part | Description |
|---|---|
| [data-slot=dialog] | Root wrapper around the trigger and the <dialog> element |
| [data-slot=dialog-content] | The <dialog> panel - positioning, animation, and the open state ride here |
| [data-slot=dialog-header] | Title block at the top of the panel |
| [data-slot=dialog-title] | The heading - the dialog's accessible name (required slot) |
| [data-slot=dialog-description] | Muted copy under the title, wired to aria-describedby |
| [data-slot=dialog-footer] | Action row at the bottom of the panel |
State attributes
| Part | Attribute | Condition | Values |
|---|---|---|---|
| dialog-content | data-open | panel is open (the controller flips the pair at runtime) | — |
| dialog-content | data-closed | panel is closed or animating out (the server-rendered state) | — |
Agent tools
What an agent may do to a rendered instance once the call opts in
(webmcp: "name") — declared beside the wiring, projected
to the registry, registered with the browser's document.modelContext
by poetry-agent.
| Tool | Parameters | Does | Dispatches |
|---|---|---|---|
| open mutating | — | Open the dialog. | poetry--core--dialog#open |
| close mutating | — | Close the dialog. | poetry--core--dialog#close |
Wiring
The Stimulus surface each element carries — declared in the component, verified against rendered DOM in CI. Bare actions fire on the element's default event.
| Element | Controller | Wiring |
|---|---|---|
| root | poetry--core--dialog | registers · value dismissible |
| content | poetry--core--dialog | close on cancel · backdropClose on click · target dialog |
| trigger | poetry--core--dialog | open |
| close | poetry--core--dialog | close |