Anchor the account menu's popover to its button
Assisted-by: GLM-5.3 via pi
diff --git a/lib/pro_tacts/admin/layout.rb b/lib/pro_tacts/admin/layout.rb
index 54de6e5..c573893 100644
--- a/lib/pro_tacts/admin/layout.rb
+++ b/lib/pro_tacts/admin/layout.rb
@@ -131,9 +131,11 @@ module ProTacts
# a book's life (docs/plans/2026-09-21-import-a-vcf.md),
# and device setup. A popover by the Popover API, like
# every floated layer here; the list sits beside its
- # button in the header so it can be positioned from it
- # (admin.css) while the Popover API still renders it
- # above everything when open.
+ # button in the header because a promoted popover is
+ # positioned against the viewport, not the DOM beside
+ # its button, so admin.css anchors it to the wrapper by
+ # name — while the Popover API still renders it above
+ # everything when open.
div(class: "user-menu") do
button(type: "button", data_size: "sm", popovertarget: MENU) do
span { @login }
diff --git a/public/admin.css b/public/admin.css
index aec5141..397f121 100644
--- a/public/admin.css
+++ b/public/admin.css
@@ -674,26 +674,30 @@ input[type="search"]::-webkit-search-cancel-button {
}
/* The account menu, the header's third region: the button naming the
- login and the popover it opens. The wrapper is the popover's
- positioning context — a popover is promoted to the top layer on
- opening but keeps its DOM position for `position: absolute`, so
- the list hangs below the button at the header's right edge with
- no script and no anchor positioning; closed, the Popover API
- itself keeps it out of the layout and the tab order. */
+ login and the popover it opens. The wrapper names itself the list's
+ anchor because a popover promoted to the top layer is positioned
+ against the viewport, not the boxes beside it in the DOM — a
+ `position: relative` wrapper with an `absolute` list cannot hang
+ the list below the button and instead opens it a full viewport
+ down, at the page's bottom right. Closed, the Popover API itself
+ keeps it out of the layout and the tab order. */
.user-menu {
- position: relative;
+ anchor-name: --user-menu;
display: flex;
}
/* The UA popover styles center (`position: fixed; inset: 0; margin:
- auto`), overridden to open off the button. A card's construction —
- surface, hairline, the one --gl-shadow-float a floated layer
- carries (Gloss's dialog, here) — at menu size; the rows take the
- quiet button's own hover tint. */
+ auto`), overridden to open off the anchor: below the button's
+ bottom edge and inline-end-aligned with it, `anchor()` reading
+ the button's box wherever the header's right edge falls. A card's
+ construction — surface, hairline, the one --gl-shadow-float a
+ floated layer carries (Gloss's dialog, here) — at menu size; the
+ rows take the quiet button's own hover tint. */
.user-menu ul {
- position: absolute;
+ position: fixed;
+ position-anchor: --user-menu;
inset: auto;
- inset-block-start: calc(100% + var(--gl-space-2xs));
- inset-inline-end: 0;
+ inset-block-start: calc(anchor(--user-menu bottom) + var(--gl-space-2xs));
+ inset-inline-end: anchor(--user-menu right);
margin: 0;
padding: var(--gl-space-2xs);
background: var(--gl-color-surface-card);
@@ -701,6 +705,15 @@ input[type="search"]::-webkit-search-cancel-button {
border-radius: var(--gl-radius-md);
box-shadow: var(--gl-shadow-float);
}
+/* Without anchor positioning the list cannot find the button at all,
+ so it parks at the page's top right — under the header, where the
+ button lives — rather than the UA popover's dead center. */
+@supports not (anchor-name: --a) {
+ .user-menu ul {
+ inset-block-start: calc(var(--gl-space-l) + var(--gl-space-2xs));
+ inset-inline-end: var(--gl-space-m);
+ }
+}
.user-menu ul a {
display: block;
padding: var(--gl-space-2xs) var(--gl-space-xs);