docs(modals): codify URL-addressable modal template convention

Architectural problem flagged after the pilot: "the template renders both
as a standalone page AND as a modal fragment" contract is non-obvious for
future template authors. Task 2 originally used <dialog>, Task 8.5 had to
undo that because nested <dialog> collapses to 2px. The convention is now
in two places:

1. AGENTS.md gains a "URL-addressable modal templates" section under
   Non-Negotiable Constraints listing: outer element must be <div>, close
   buttons use data-modal-dismiss, form actions need #modal-content-scoped
   document delegation, modal chrome CSS is owned by the outer slot.
2. _modal_partial.html (the file template authors will most likely open
   when wondering "what's this layout?") carries a Jinja comment header
   summarising the rule + linking to AGENTS.md for the full convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-17 14:09:05 +02:00
parent 712ccc9861
commit 74fd906cf4
No known key found for this signature in database
2 changed files with 18 additions and 0 deletions

View file

@ -23,6 +23,16 @@ Do not invent architecture outside these plans unless explicitly requested.
- Do not use git worktrees. - Do not use git worktrees.
- Repo is a uv workspace; Python is pinned to 3.13 via `.python-version`. After fresh checkout: install `uv` (`brew install uv` / `curl -LsSf https://astral.sh/uv/install.sh | sh`), then `direnv allow` (or `uv sync` directly). See README **Local development** for details. - Repo is a uv workspace; Python is pinned to 3.13 via `.python-version`. After fresh checkout: install `uv` (`brew install uv` / `curl -LsSf https://astral.sh/uv/install.sh | sh`), then `direnv allow` (or `uv sync` directly). See README **Local development** for details.
### URL-addressable modal templates
A template that renders **both** as a full standalone page AND as a modal fragment (i.e. `{% extends base_layout %}`, where `base_layout` resolves to `_modal_partial.html` for modal-mode requests and `base.html` otherwise — driven by the `HX-Modal: 1` header in `app.py:inject_base_layout`) MUST follow these conventions:
- **The outermost element of `{% block content %}` is a `<div>`, NOT a `<dialog>`.** The persistent `<dialog id="modal-container">` slot in `base.html` provides top-layer + backdrop + focus-trap + Esc-to-close semantics. Nested `<dialog>` elements collapse to 2 px in every browser — Task 8.5 of the modals pilot fixed this the hard way; do not re-introduce it. In standalone mode, the content renders flat under `<main>`, which is the intended "this URL is a real page, not a modal-over-nothing" UX.
- **Close buttons use `data-modal-dismiss`** (NOT `data-modal-close` — that's the legacy inline-dialog system). `modal-router.js` listens at document level for this attribute and calls `dialog.close()` on the outer slot.
- **Form-bearing content needs document-level event delegation** for submit/save/delete actions, gated on `event.target.closest("#modal-content")`. Direct binding to elements in the swapped-in fragment only works in standalone mode — HTMX-swapped content arrives as fresh DOM nodes with no listeners attached. See `files-overlay.js` lines ~599-641 for the canonical pattern (read `data-*` attributes from the textarea, NOT from JS state set during open).
- **CSS classes targeting modal chrome are scoped to the outer slot**`dialog.modal, div.modal` in `components.css`. The inner content div should NOT carry `class="modal modal-wide"` (that's what painted card-in-a-card during Task 8.5b; the outer dialog owns chrome).
- **Reference:** `docs/superpowers/specs/2026-05-17-url-addressable-modals-design.md` (design + verification matrix) and the plan errata at the top of `docs/superpowers/plans/2026-05-17-url-addressable-modals.md`.
### Dev server and filesystem paths ### Dev server and filesystem paths
- **Production paths (`/var/lib/left4me`, `/usr/local/lib/systemd/system`, `/usr/local/libexec/left4me`, `/etc/left4me`) exist only on Linux deploy hosts.** Never create or write to these on a developer machine. They are referenced in `l4d2host/l4d2host/paths.py` and the spec only as the production layout. - **Production paths (`/var/lib/left4me`, `/usr/local/lib/systemd/system`, `/usr/local/libexec/left4me`, `/etc/left4me`) exist only on Linux deploy hosts.** Never create or write to these on a developer machine. They are referenced in `l4d2host/l4d2host/paths.py` and the spec only as the production layout.

View file

@ -1 +1,9 @@
{# Modal-fragment layout. Templates that extend `base_layout` render through
this when the request carries `HX-Modal: 1` (see `inject_base_layout` in
app.py). The persistent <dialog id="modal-container"> in base.html provides
top-layer + backdrop + focus-trap + Esc-to-close semantics. Templates that
extend base_layout MUST NOT wrap their content in a <dialog> — nested
<dialog> collapses to 2px. Use a <div> root and let the outer slot own
dialog semantics. See AGENTS.md "URL-addressable modal templates" for the
full convention. #}
{% block content %}{% endblock %} {% block content %}{% endblock %}