diff --git a/AGENTS.md b/AGENTS.md index c24232b..a7cf3b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,16 @@ Do not invent architecture outside these plans unless explicitly requested. - 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. +### 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 `
`, NOT a ``.** The persistent `` slot in `base.html` provides top-layer + backdrop + focus-trap + Esc-to-close semantics. Nested `` 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 `
`, 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 - **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. diff --git a/l4d2web/l4d2web/templates/_modal_partial.html b/l4d2web/l4d2web/templates/_modal_partial.html index cb0dbe4..304dc1c 100644 --- a/l4d2web/l4d2web/templates/_modal_partial.html +++ b/l4d2web/l4d2web/templates/_modal_partial.html @@ -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 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 — nested + collapses to 2px. Use a
root and let the outer slot own + dialog semantics. See AGENTS.md "URL-addressable modal templates" for the + full convention. #} {% block content %}{% endblock %}