# Confirm Modal


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## Where this lives, and why

Per the three-layer architecture (see `cjm-fasthtml-design-system`
bootstrap plan, §8.6), *visual recipes* — CSS class compositions, sizing
tokens — live in `cjm-fasthtml-design-system`, and *layout primitives* —
FT-returning helpers that codify DOM structure and behavior — live in
`cjm-fasthtml-app-core`. `render_confirm_modal` returns a `Dialog` with
structural ordering (Cancel-LEFT, Confirm-RIGHT), an HTMX-target body
`Div`, and behavioral attributes (`formmethod="dialog"`,
`type="button"`, backdrop form). Those are DOM-layer concerns; the
recipe layer is what this helper *consumes*, not where it belongs.

The convention itself is referenced from the design-system doc’s
Conventions Directory, which points at this notebook as the canonical
source. The pattern matches V3 (cursor-mode scrollbar in
`cjm-fasthtml-virtual-scrollbar`) and V5 (step-nav footer in
`cjm-fasthtml-interactions`), where the convention is
design-system-owned but the running code lives in the library that
naturally owns the structural layer.

## Load-bearing decisions (V12 invariants)

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr>
<th style="text-align: left;">Decision</th>
<th style="text-align: left;">Why</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Cancel on <strong>LEFT</strong>, Confirm
on <strong>RIGHT</strong></td>
<td style="text-align: left;">LTR convention; aligns with native macOS /
Windows / DaisyUI confirm dialogs.</td>
</tr>
<tr>
<td style="text-align: left;">Backdrop click-to-dismiss</td>
<td style="text-align: left;">Same outcome as Cancel — safe,
non-destructive escape hatch.</td>
</tr>
<tr>
<td style="text-align: left;">Confirm button <code>type="button"</code>
(defensive)</td>
<td style="text-align: left;">Prevents accidental form-submission if the
modal is ever rendered inside a wrapping <code>&lt;form&gt;</code>.
Costs nothing today; closes a future-bug surface.</td>
</tr>
<tr>
<td style="text-align: left;">Cancel via
<code>formmethod="dialog"</code></td>
<td style="text-align: left;">Native HTML5 dialog dismiss — no server
roundtrip, no JS.</td>
</tr>
<tr>
<td style="text-align: left;">Body populated via HTMX swap into
<code>body_id</code></td>
<td style="text-align: left;">Lets caller inject per-target message text
without re-rendering the modal. Pattern observed in both pre-extraction
consumers (<code>workflow-management</code>,
<code>workflow-session-management</code>).</td>
</tr>
</tbody>
</table>

Each decision has a CI-asserted regression guard in the test cell at the
bottom of this notebook; a future “simplification” that erodes any
invariant trips the test before consumers see it.

## `render_confirm_modal`

Generic destructive-confirm modal. Caller supplies the dialog id, the
body-target id (for HTMX message injection), the title, the labels, and
HTMX attrs for the confirm button.

------------------------------------------------------------------------

### render_confirm_modal

``` python

def render_confirm_modal(
    modal_id:str, # HTML id for the <dialog> element
    body_id:str, # HTML id for the inner Div HTMX targets for message text
    title:str='Confirm Action?', # Modal title (rendered in <h3>)
    confirm_label:str='Confirm', # Right-button label
    confirm_icon:Optional=None, # Optional Lucide icon name (e.g. "trash-2") prefixed to the confirm label
    cancel_label:str='Cancel', # Left-button label
    confirm_attrs:Optional=None, # Caller-supplied attrs for the confirm button (hx_post / hx_vals / hx_swap / etc.)
)->FT: # Dialog element implementing V12

```

*Render a destructive-confirm modal (V12). Cancel-LEFT, Confirm-RIGHT,
backdrop click-to-dismiss, defensive type=button. Body populated via
HTMX swap into body_id.*

## Example

``` python
# Example: a delete-document confirm modal
render_confirm_modal(
    modal_id="delete-doc-modal",
    body_id="delete-doc-body",
    title="Delete Document?",
    confirm_label="Delete",
    confirm_icon="trash-2",
    confirm_attrs={"hx_post": "/manage/documents/delete", "hx_swap": "none"},
)
```

``` html
<dialog id="delete-doc-modal" class="modal">  <div class="modal-box">
    <h3 class="text-lg font-bold">Delete Document?</h3>
    <div id="delete-doc-body" class="my-4"></div>
    <div class="modal-action flex gap-2">
<form enctype="multipart/form-data" method="dialog"><button formmethod="dialog" class="btn btn-ghost btn-sm">Cancel</button></form><button hx-post="/manage/documents/delete" hx-swap="none" type="button" class="btn btn-error btn-sm flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"></path><path d="M3 6h18"></path><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>Delete</button>    </div>
  </div>
<form enctype="multipart/form-data" method="dialog" class="modal-backdrop"><button>close</button></form></dialog>
```

## Tests — V12 regression guards

Each assertion below maps to one decision in the *Load-bearing
decisions* table above. Pattern matches V10
(`'bg-base-200' not in panels.content_card`), V11 (icon-strategy
sentinel), and V1 (anti-coincidence-collapse omissions): assertions
encode design intent, not just current behavior.

``` python
from fasthtml.common import to_xml

# Render a representative modal and serialize to HTML for substring checks.
# Sentinel labels (`__CANCEL_BTN__` / `__CONFIRM_BTN__`) ensure the DOM-order
# index lookups never collide with the title or other body text.
test_modal = render_confirm_modal(
    modal_id="t-modal",
    body_id="t-body",
    title="Remove this item?",
    confirm_label="__CONFIRM_BTN__",
    cancel_label="__CANCEL_BTN__",
    confirm_icon="trash-2",
    confirm_attrs={"hx_post": "/delete", "hx_vals": '{"id":"x"}'},
)
test_html = to_xml(test_modal)

# --- Modal structure invariants ---
assert 'modal-action' in test_html, "modal_action wrapper present"
assert 'modal-backdrop' in test_html, "backdrop form present (click-outside-to-dismiss)"
assert 'modal-box' in test_html, "modal_box wrapper present"
assert 'id="t-modal"' in test_html
assert 'id="t-body"' in test_html

# --- Behavioral invariants ---
# Confirm button has type="button" — defensive against accidental form-submit
# if the caller renders the modal inside a wrapping <form>. Without this, a
# stray Enter keypress in any input field could trigger the destructive action.
assert 'type="button"' in test_html
# Cancel uses formmethod="dialog" — native dismiss without server roundtrip.
assert 'formmethod="dialog"' in test_html

# --- V1 role-composition invariants ---
# destructive_confirm uses btn-error solid (commit-now semantics).
assert 'btn-error' in test_html, "confirm button must use V1 destructive_confirm (btn-error)"
# soft_dismissal uses btn-ghost (terminal exit).
assert 'btn-ghost' in test_html, "cancel button must use V1 soft_dismissal (btn-ghost)"

# --- Positional invariant — Cancel before Confirm in DOM order ---
# Locks in V12's Cancel-LEFT discipline. Future rearrangements (e.g., placing
# the destructive action first) trip this assertion at CI time.
cancel_pos = test_html.index("__CANCEL_BTN__")
confirm_pos = test_html.index("__CONFIRM_BTN__")
assert cancel_pos < confirm_pos, "Cancel must precede Confirm in DOM order (Cancel-LEFT)"

# --- HTMX attrs propagation ---
assert 'hx-post="/delete"' in test_html, "hx_post must propagate to confirm button"
assert 'hx-vals' in test_html, "hx_vals must propagate to confirm button"

# --- Title rendering ---
assert 'Remove this item?' in test_html

# --- No-icon variant: confirm_icon=None must not emit an icon ---
plain_modal = render_confirm_modal(
    modal_id="m2", body_id="b2",
    title="Discard changes?",
    confirm_label="Discard",
)
plain_html = to_xml(plain_modal)
assert 'Discard' in plain_html
# Lucide SVGs include the class "lucide" — absence confirms no icon was emitted.
assert 'lucide' not in plain_html, "confirm_icon=None must render label without an icon"

# --- Caller can override defensive type="button" if they really want to ---
submit_modal = render_confirm_modal(
    modal_id="m3", body_id="b3",
    confirm_label="Submit",
    confirm_attrs={"type": "submit"},
)
submit_html = to_xml(submit_modal)
assert 'type="submit"' in submit_html, "explicit type override must win over defensive default"

# --- Anti-coincidence-collapse note (P11 + P8) ---
# The cancel button's class string is `buttons.soft_dismissal`, NOT `buttons.item_remove`
# or `buttons.modal_disclosure` — even though all three currently render with btn-ghost.
# A future maintainer should NOT "simplify" by merging these roles; the V1 catalog deliberately
# keeps them distinct so they can evolve independently. No equality assertion between them here
# — the omission is the discipline, mirroring the V1 buttons.ipynb test cell.
```
