Skip to main content

priv/registry/alert_dialog.json

{
  "files": [
    {
      "content": "defmodule Shadix.Components.AlertDialog do\n  @moduledoc \"\"\"\n  A modal alert dialog for destructive or otherwise consequential confirmations.\n\n  This is the `Dialog`'s stricter sibling: it renders with `role=\"alertdialog\"`\n  and deliberately offers no dismissal affordances other than an explicit\n  choice. There is **no X close button** and clicking the overlay does **not**\n  close it — the user must pick the action or cancel. `Escape` is still honored\n  via `phx-window-keydown` so keyboard users are never trapped.\n\n  Like `Dialog`, it is portal-free and fully client-driven: `show_alert_dialog/1`\n  and `hide_alert_dialog/1` are `Phoenix.LiveView.JS` command builders that toggle\n  the overlay and content panel (with transitions) and dispatch\n  `shadix:alert-dialog-open` / `shadix:alert-dialog-close` events. The\n  `ShadixAlertDialog` hook (assets/ts/alert_dialog.ts) listens for those events to\n  lock body scroll and manage focus. Focus is trapped with Phoenix's built-in\n  `<.focus_wrap>`.\n\n  The trigger and content live side by side under a `*-root` wrapper carrying the\n  hook; ids are derived from the required `:id` so titles/descriptions wire up to\n  `aria-labelledby` / `aria-describedby` stably.\n  \"\"\"\n  use Phoenix.Component\n\n  alias Phoenix.LiveView.JS\n\n  import Shadix.Cn\n\n  @doc \"\"\"\n  Renders a modal alert dialog with a `:trigger` slot and arbitrary content.\n\n  The trigger is wrapped in a `display: contents` span wired to\n  `show_alert_dialog/1`, so the caller's own button/element opens the dialog\n  without extra markup. The overlay is intentionally not clickable and no close\n  button is rendered; only `Escape` and the explicit action/cancel controls\n  dismiss it.\n  \"\"\"\n  attr(:id, :string, required: true)\n  attr(:class, :string, default: nil)\n  slot(:trigger, required: true)\n  slot(:inner_block, required: true)\n\n  def alert_dialog(assigns) do\n    ~H\"\"\"\n    <span phx-click={show_alert_dialog(@id)} class=\"contents\">{render_slot(@trigger)}</span>\n    <div id={\"#{@id}-root\"} phx-hook=\"ShadixAlertDialog\">\n      <div\n        id={\"#{@id}-overlay\"}\n        class=\"hidden fixed inset-0 z-50 bg-black/50\"\n        aria-hidden=\"true\"\n        data-slot=\"alert-dialog-overlay\"\n      />\n      <.focus_wrap\n        id={\"#{@id}-content\"}\n        class={\n          cn([\n            \"hidden fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border bg-background p-6 shadow-lg\",\n            @class\n          ])\n        }\n        role=\"alertdialog\"\n        aria-modal=\"true\"\n        aria-labelledby={\"#{@id}-title\"}\n        aria-describedby={\"#{@id}-description\"}\n        data-slot=\"alert-dialog-content\"\n        phx-window-keydown={hide_alert_dialog(@id)}\n        phx-key=\"escape\"\n      >\n        {render_slot(@inner_block)}\n      </.focus_wrap>\n    </div>\n    \"\"\"\n  end\n\n  @doc \"\"\"\n  Returns the `Phoenix.LiveView.JS` command that opens the alert dialog with `id`.\n\n  Shows the overlay and content panel with enter transitions and dispatches\n  `shadix:alert-dialog-open` on the `*-root` element for the `ShadixAlertDialog`\n  hook.\n  \"\"\"\n  def show_alert_dialog(id) do\n    %JS{}\n    |> JS.show(\n      to: \"##{id}-overlay\",\n      transition: {\"transition-opacity ease-out duration-200\", \"opacity-0\", \"opacity-100\"}\n    )\n    |> JS.show(\n      to: \"##{id}-content\",\n      display: \"grid\",\n      transition:\n        {\"transition ease-out duration-200\", \"opacity-0 scale-95\", \"opacity-100 scale-100\"}\n    )\n    |> JS.dispatch(\"shadix:alert-dialog-open\", to: \"##{id}-root\")\n    |> JS.focus_first(to: \"##{id}-content\")\n  end\n\n  @doc \"\"\"\n  Returns the `Phoenix.LiveView.JS` command that closes the alert dialog with `id`.\n\n  Hides the overlay and content panel with leave transitions and dispatches\n  `shadix:alert-dialog-close` on the `*-root` element for the `ShadixAlertDialog`\n  hook.\n  \"\"\"\n  def hide_alert_dialog(id) do\n    %JS{}\n    |> JS.hide(\n      to: \"##{id}-overlay\",\n      transition: {\"transition-opacity ease-in duration-150\", \"opacity-100\", \"opacity-0\"}\n    )\n    |> JS.hide(\n      to: \"##{id}-content\",\n      transition:\n        {\"transition ease-in duration-150\", \"opacity-100 scale-100\", \"opacity-0 scale-95\"}\n    )\n    |> JS.dispatch(\"shadix:alert-dialog-close\", to: \"##{id}-root\")\n  end\n\n  @doc \"Header region of an alert dialog; stacks title/description with sensible spacing.\"\n  attr(:class, :string, default: nil)\n  attr(:rest, :global)\n  slot(:inner_block, required: true)\n\n  def alert_dialog_header(assigns) do\n    ~H\"\"\"\n    <div\n      data-slot=\"alert-dialog-header\"\n      class={cn([\"flex flex-col gap-2 text-center sm:text-left\", @class])}\n      {@rest}\n    >\n      {render_slot(@inner_block)}\n    </div>\n    \"\"\"\n  end\n\n  @doc \"Footer region of an alert dialog; right-aligns the cancel/action controls on larger screens.\"\n  attr(:class, :string, default: nil)\n  attr(:rest, :global)\n  slot(:inner_block, required: true)\n\n  def alert_dialog_footer(assigns) do\n    ~H\"\"\"\n    <div\n      data-slot=\"alert-dialog-footer\"\n      class={cn([\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\", @class])}\n      {@rest}\n    >\n      {render_slot(@inner_block)}\n    </div>\n    \"\"\"\n  end\n\n  @doc ~S\"\"\"\n  Alert dialog title. `:id` is the *dialog's* id; the title renders with\n  `\"#{id}-title\"` so it matches the content panel's `aria-labelledby`.\n  \"\"\"\n  attr(:id, :string, required: true)\n  attr(:class, :string, default: nil)\n  attr(:rest, :global)\n  slot(:inner_block, required: true)\n\n  def alert_dialog_title(assigns) do\n    ~H\"\"\"\n    <h2\n      id={\"#{@id}-title\"}\n      data-slot=\"alert-dialog-title\"\n      class={cn([\"text-lg leading-none font-semibold\", @class])}\n      {@rest}\n    >\n      {render_slot(@inner_block)}\n    </h2>\n    \"\"\"\n  end\n\n  @doc ~S\"\"\"\n  Alert dialog description. `:id` is the *dialog's* id; renders with\n  `\"#{id}-description\"` so it matches the content panel's `aria-describedby`.\n  \"\"\"\n  attr(:id, :string, required: true)\n  attr(:class, :string, default: nil)\n  attr(:rest, :global)\n  slot(:inner_block, required: true)\n\n  def alert_dialog_description(assigns) do\n    ~H\"\"\"\n    <p\n      id={\"#{@id}-description\"}\n      data-slot=\"alert-dialog-description\"\n      class={cn([\"text-muted-foreground text-sm\", @class])}\n      {@rest}\n    >\n      {render_slot(@inner_block)}\n    </p>\n    \"\"\"\n  end\n\n  @doc ~S\"\"\"\n  The primary (often destructive) action button.\n\n  Renders a plain primary `<button>` carrying `data-slot=\"alert-dialog-action\"`.\n  The caller is expected to wire it with `phx-click` (and may pass any other\n  attributes via the global `:rest`) to perform the confirmed operation; it does\n  not close the dialog on its own.\n  \"\"\"\n  attr(:class, :string, default: nil)\n  attr(:type, :string, default: \"button\")\n  attr(:rest, :global)\n  slot(:inner_block, required: true)\n\n  def alert_dialog_action(assigns) do\n    ~H\"\"\"\n    <button\n      type={@type}\n      data-slot=\"alert-dialog-action\"\n      class={\n        cn([\n          \"inline-flex h-9 shrink-0 items-center justify-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium whitespace-nowrap text-primary-foreground shadow-xs transition-all outline-none hover:bg-primary/90 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50\",\n          @class\n        ])\n      }\n      {@rest}\n    >\n      {render_slot(@inner_block)}\n    </button>\n    \"\"\"\n  end\n\n  @doc ~S\"\"\"\n  The cancel button: an outline-styled `<button>` that closes the alert dialog.\n\n  `:id` is the *dialog's* id so the button can call `hide_alert_dialog/1` on the\n  correct panel. Carries `data-slot=\"alert-dialog-cancel\"`.\n  \"\"\"\n  attr(:id, :string, required: true)\n  attr(:class, :string, default: nil)\n  attr(:type, :string, default: \"button\")\n  attr(:rest, :global)\n  slot(:inner_block, required: true)\n\n  def alert_dialog_cancel(assigns) do\n    ~H\"\"\"\n    <button\n      type={@type}\n      data-slot=\"alert-dialog-cancel\"\n      phx-click={hide_alert_dialog(@id)}\n      class={\n        cn([\n          \"inline-flex h-9 shrink-0 items-center justify-center gap-2 rounded-md border bg-background px-4 py-2 text-sm font-medium whitespace-nowrap shadow-xs transition-all outline-none hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50\",\n          @class\n        ])\n      }\n      {@rest}\n    >\n      {render_slot(@inner_block)}\n    </button>\n    \"\"\"\n  end\nend\n",
      "path": "alert_dialog.ex"
    }
  ],
  "hooks": [
    {
      "content": "interface AlertDialogHook {\n  el: HTMLElement;\n  mounted(): void;\n  destroyed(): void;\n}\n\nconst FOCUSABLE =\n  'a[href],area[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),[tabindex]:not([tabindex=\"-1\"])';\n\nexport const ShadixAlertDialog = {\n  mounted(this: AlertDialogHook) {\n    const el = this.el;\n    let restoreTo: HTMLElement | null = null;\n\n    const open = () => {\n      restoreTo = document.activeElement as HTMLElement | null;\n      document.body.style.overflow = \"hidden\";\n      window.requestAnimationFrame(() => {\n        const content = el.querySelector<HTMLElement>('[role=\"alertdialog\"]');\n        const first = content?.querySelector<HTMLElement>(FOCUSABLE);\n        (first ?? content)?.focus();\n      });\n    };\n\n    const close = () => {\n      document.body.style.overflow = \"\";\n      restoreTo?.focus();\n      restoreTo = null;\n    };\n\n    el.addEventListener(\"shadix:alert-dialog-open\", open);\n    el.addEventListener(\"shadix:alert-dialog-close\", close);\n  },\n\n  destroyed(this: AlertDialogHook) {\n    document.body.style.overflow = \"\";\n  },\n};\n",
      "name": "ShadixAlertDialog",
      "path": "alert_dialog.ts"
    }
  ],
  "name": "alert_dialog",
  "npm_deps": [],
  "registry_deps": [
    "cn"
  ]
}