# VelvetKit

## Dialog (`UI::Dialog`)

A modal dialog on the native <dialog> element.

Add it: `bin/rails generate velvetkit:add dialog`

Also adds: `icon`, `spinner`, `button`

Stimulus: `ui--dialog`

### Arguments

```ruby
UI::Dialog.new(class: nil, **html_attrs)
```

Slots: `with_trigger`, `with_body`, `with_content`, `with_title`, `with_description`, `with_footer`

### Notes

UI::Dialog — modal dialog backed by the native <dialog> element + showModal().

Why native <dialog>:
  - Browser owns the modal mode, focus trap, ESC handling, and inert
    background. We do not reimplement any of that in JS.
  - The Stimulus controller only opens, closes, and dismisses on backdrop
    click. Everything else is delegated to the platform.

Compound:
  UI::Dialog
  UI::Dialog::Trigger    — wraps any element with a click action
  UI::Dialog::Content    — the <dialog> box; exposes with_title /
                           with_description / with_footer block helpers
                           (no further sub-component classes).

### Example: Default

```erb
<%= render(UI::Dialog.new) do |dialog| %>
  <% dialog.with_trigger(variant: :outline) { "Open dialog" } %>
  <% dialog.with_content do |content| %>
    <% content.with_title { "Edit profile" } %>
    <% content.with_description { "Make changes to your profile here. Click save when you're done." } %>
    <% content.with_footer do %>
      <%= render(UI::Dialog::Close.new(variant: :outline, class: "static")) { "Cancel" } %>
      <%= render(UI::Button.new) { "Save changes" } %>
    <% end %>
    <div class="grid gap-3">
      <%= render(UI::Input.new(type: :text, value: "Rain Root")) %>
      <%= render(UI::Input.new(type: :email, value: "rainroot@example.com")) %>
    </div>
  <% end %>
<% end %>
```

### Example: Confirmation

```erb
<%= render(UI::Dialog.new) do |dialog| %>
  <% dialog.with_trigger(variant: :destructive) { "Delete project" } %>
  <% dialog.with_content(size: :sm) do |content| %>
    <% content.with_title { "Delete this project?" } %>
    <% content.with_description { "This action cannot be undone. All project data will be removed permanently." } %>
    <% content.with_footer do %>
      <%= render(UI::Dialog::Close.new(variant: :outline, class: "static")) { "Cancel" } %>
      <%= render(UI::Button.new(variant: :destructive)) { "Delete" } %>
    <% end %>
  <% end %>
<% end %>
```

### Example: Custom Trigger

```erb
<%= render(UI::Dialog.new) do |dialog| %>
  <% dialog.with_trigger(as_child: true, variant: :ghost) do %>
    <a href="#dialog-demo" class="inline-flex items-center gap-2 text-sm font-medium underline underline-offset-4">
      Review release notes
      <%= render(UI::Icon.new(name: "arrow-right", size: :sm)) %>
    </a>
  <% end %>
  <% dialog.with_content do |content| %>
    <% content.with_title { "Release notes" } %>
    <% content.with_description { "A custom trigger can adopt the dialog semantics without rendering a nested button." } %>
    <div class="space-y-2 text-sm text-muted-foreground">
      <p>Dialog trigger attributes are merged into the anchor element.</p>
      <p>The trigger keeps its own classes while inheriting the chosen button variant tokens.</p>
    </div>
  <% end %>
<% end %>
```

### Example: Long Content

```erb
<%= render(UI::Dialog.new) do |dialog| %>
  <% dialog.with_trigger(variant: :secondary) { "Open policy dialog" } %>
  <% dialog.with_content(size: :lg) do |content| %>
    <% content.with_title { "Usage policy" } %>
    <% content.with_description { "Long-form modal content should remain readable and keep actions pinned at the end." } %>
    <div class="space-y-4 text-sm leading-6 text-muted-foreground">
      <p>Dialogs are best reserved for focused tasks, decisions, or short forms.</p>
      <p>Because the component uses the native dialog element, focus is moved into the modal and restored to the opener when it closes.</p>
      <p>The content element is portaled under <code>body</code> at runtime so the backdrop and stacking context stay predictable.</p>
    </div>
    <% content.with_footer do %>
      <%= render(UI::Dialog::Close.new(variant: :outline, class: "static")) { "Close" } %>
      <%= render(UI::Button.new(variant: :secondary)) { "Acknowledge" } %>
    <% end %>
  <% end %>
<% end %>
```

### Example: Sizes

```erb
<div class="flex flex-wrap gap-4">
  <%= render(UI::Dialog.new) do |dialog| %>
    <% dialog.with_trigger(variant: :outline, size: :sm) { "Open sm" } %>
    <% dialog.with_content(size: :sm) do |content| %>
      <% content.with_title { "Small dialog" } %>
      <% content.with_description { "This is the smallest dialog size, great for confirmations." } %>
      <% content.with_footer do %>
        <%= render(UI::Dialog::Close.new(variant: :outline, class: "static")) { "Cancel" } %>
        <%= render(UI::Button.new(size: :sm)) { "Confirm" } %>
      <% end %>
    <% end %>
  <% end %>
  <%= render(UI::Dialog.new) do |dialog| %>
    <% dialog.with_trigger(variant: :outline) { "Open default" } %>
    <% dialog.with_content do |content| %>
      <% content.with_title { "Default dialog" } %>
      <% content.with_description { "This is the default dialog size for most use cases." } %>
      <% content.with_footer do %>
        <%= render(UI::Dialog::Close.new(variant: :outline, class: "static")) { "Cancel" } %>
        <%= render(UI::Button.new) { "Confirm" } %>
      <% end %>
    <% end %>
  <% end %>
  <%= render(UI::Dialog.new) do |dialog| %>
    <% dialog.with_trigger(variant: :outline) { "Open lg" } %>
    <% dialog.with_content(size: :lg) do |content| %>
      <% content.with_title { "Large dialog" } %>
      <% content.with_description { "This is a large dialog, useful for forms and detailed content." } %>
      <% content.with_footer do %>
        <%= render(UI::Dialog::Close.new(variant: :outline, class: "static")) { "Cancel" } %>
        <%= render(UI::Button.new) { "Confirm" } %>
      <% end %>
    <% end %>
  <% end %>
  <%= render(UI::Dialog.new) do |dialog| %>
    <% dialog.with_trigger(variant: :outline) { "Open xl" } %>
    <% dialog.with_content(size: :xl) do |content| %>
      <% content.with_title { "Extra large dialog" } %>
      <% content.with_description { "This is the largest dialog size, ideal for complex workflows." } %>
      <% content.with_footer do %>
        <%= render(UI::Dialog::Close.new(variant: :outline, class: "static")) { "Cancel" } %>
        <%= render(UI::Button.new) { "Confirm" } %>
      <% end %>
    <% end %>
  <% end %>
</div>
```

### Example: Without Close Button

```erb
<%= render(UI::Dialog.new) do |dialog| %>
  <% dialog.with_trigger(variant: :outline) { "Open dialog" } %>
  <% dialog.with_content(show_close: false) do |content| %>
    <% content.with_title { "No close button" } %>
    <% content.with_description { "This dialog hides the default close button. Users must interact with the footer actions to dismiss it." } %>
    <% content.with_footer do %>
      <%= render(UI::Dialog::Close.new(variant: :outline, class: "static")) { "Dismiss" } %>
      <%= render(UI::Button.new) { "Continue" } %>
    <% end %>
  <% end %>
<% end %>
```

### Example: Without Title

```erb
<%= render(UI::Dialog.new) do |dialog| %>
  <% dialog.with_trigger(variant: :outline) { "Open dialog" } %>
  <% dialog.with_content do %>
    <div class="space-y-3 text-sm text-muted-foreground">
      <p>This dialog has no title or description header.</p>
      <p>Use this pattern when the content speaks for itself or when you want a cleaner, minimal modal.</p>
    </div>
    <div class="mt-4 flex justify-end gap-2">
      <%= render(UI::Dialog::Close.new(variant: :outline, class: "static")) { "Close" } %>
      <%= render(UI::Button.new) { "Okay" } %>
    </div>
  <% end %>
<% end %>
```

### Example: Scrollable Body

```erb
<%= render(UI::Dialog.new) do |dialog| %>
  <% dialog.with_trigger(variant: :secondary) { "Open scrollable dialog" } %>
  <% dialog.with_content(size: :lg) do |content| %>
    <% content.with_title { "Terms of Service" } %>
    <% content.with_description { "Please review the full terms before continuing." } %>
    <div class="space-y-4 text-sm leading-6 text-muted-foreground">
      <p><strong>1. Acceptance of Terms</strong><br>
        By accessing or using our services, you agree to be bound by these Terms of Service. If you do not agree to all the terms and conditions, you may not access the service.</p>
      <p><strong>2. Changes to Terms</strong><br>
        We reserve the right, at our sole discretion, to modify or replace these Terms at any time. It is your responsibility to review these Terms periodically for changes.</p>
      <p><strong>3. Use License</strong><br>
        Permission is granted to temporarily use the materials on our website for personal, non-commercial transitory viewing only. This is the grant of a license, not a transfer of title.</p>
      <p><strong>4. Disclaimer</strong><br>
        The materials on our website are provided on an 'as is' basis. We make no warranties, expressed or implied, and hereby disclaim and negate all other warranties including, without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights.</p>
      <p><strong>5. Limitations</strong><br>
        In no event shall we or our suppliers be liable for any damages arising out of the use or inability to use the materials on our website, even if we or an authorized representative has been notified orally or in writing of the possibility of such damage.</p>
      <p><strong>6. Accuracy of Materials</strong><br>
        The materials appearing on our website could include technical, typographical, or photographic errors. We do not warrant that any of the materials on our website are accurate, complete, or current.</p>
      <p><strong>7. Links</strong><br>
        We have not reviewed all of the sites linked to our website and are not responsible for the contents of any such linked site. The inclusion of any link does not imply endorsement by us of the site.</p>
      <p><strong>8. Governing Law</strong><br>
        These terms and conditions are governed by and construed in accordance with the laws of the jurisdiction in which we operate, and you irrevocably submit to the exclusive jurisdiction of the courts in that location.</p>
    </div>
    <% content.with_footer do %>
      <%= render(UI::Dialog::Close.new(variant: :outline, class: "static")) { "Decline" } %>
      <%= render(UI::Button.new) { "Accept" } %>
    <% end %>
  <% end %>
<% end %>
```
