Anchor the account menu's popover to its button
Assisted-by: GLM-5.3 via pi
change urxzwsmxkkmtyuuvolvwrryttqlvsmln
commit 6e1f40eea4596572a3b02c45e388d8bec5d1ca60
author Alpha Chen <alpha@kejadlen.dev>
date
parent 6020900d
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);