Header, right side: avatar, full name and theme switch only; Sign out moves into an account menu #10

Closed
opened 2026-09-05 12:13:25 +00:00 by tiagoagueda · 1 comment
Owner

Observation

the top nav bar, on the right should contain only the avatar + full name + theme switch. the logout option should show only on a dedicated dropdown menu that opens when the user clicks on the avatar or the name

What exists today

The right cluster of the header (src/postulo/templates/base.html, line 57 onwards) holds four things: Capture (secondary button), Record (primary button), the display name as a link to Your details, and a Sign out form with its own button. No avatar, no switch, no menu.

display_name (accounts/models.py, line 71) is the full name when there is one and the local part of the email address otherwise; #2 makes the full name obligatory, after which the header always shows a name.

Two facts shape the implementation:

  • Sign out is a POST. django-allauth's logout view refuses GET by default (ACCOUNT_LOGOUT_ON_GET is not set anywhere in config/settings/), and rightly so: a GET logout is a one-line CSRF. The menu entry therefore stays a form with a button, styled as a menu item; it does not become a link.
  • No inline script, no Alpine. The CSP is script-src 'self'; the project's client-side behaviour is one delegated-events file (static/js/app.js). A menu has to fit that pattern.

Shape

1. The cluster becomes exactly three things, in the order given:

[ (avatar) Alex Morgan ▾ ]  [ ☾ ]
  • Avatar and full name together are one trigger, not two: a single control containing the picture (#7; its initials fallback is enough) and the name (display_name until #2), with a small chevron (#8) so it reads as a menu and not as a link. On narrow screens the name hides and the avatar alone remains the trigger.
  • The theme switch from #9, at the far right, outside the menu. It stays visible in the bar because that is the point of it.

2. The account menu, opened by the trigger:

  • Your details — the link the name used to be
  • not Invitations: inviting people is user management, and it lives under Server settings → People (#24), decided 2026-09-05. The staff-only item in the main navigation (base.html, line 47) goes away with it, which shortens the bar. Administrators reach Server settings from this menu (#24), so nothing is further away than before.
  • Export everything — today reachable from Insights and the dashboard; an account menu is its natural home as well. Optional, same reasoning.
  • a rule
  • Sign out — the POST form, styled as the last item

3. Capture and Record leave the header. "Only" means only. Both already exist as page-level buttons where they belong: Record an application on the Applications list (twice), the Board and Insights; Capture a posting on the captures list. The one page that relies on the header buttons is the dashboard, which has neither of its own (core/dashboard.html links to export and nothing else in that family). So the dashboard gains its own two actions in its page header, which is where a day starts anyway. Nothing else loses a button.

How the menu is built

Proposal: <details> and <summary>, which the project already uses (jobs/capture_form.html, line 22). It opens and closes with no script, the trigger is focusable and keyboard-operable out of the box, and assistive technology announces the expanded state. This is the native form of the WAI-ARIA disclosure navigation menu pattern, which is the recommended one for site navigation (not role="menu", which is for application menus and brings its own keyboard contract).

What the native element does not do is close on a click elsewhere or on Escape. That is a few lines in app.js, delegated from the document like data-autosubmit: any open details[data-menu] closes when the click lands outside it or Escape is pressed, and focus returns to the trigger. Markup swapped by htmx keeps working, because nothing is bound to an element.

The alternative is the Popover API (popover on the panel, popovertarget on the button), which gives light dismiss and Escape natively and needs no JS at all. Its drawback is placement: a popover lands in the top layer centred in the viewport by default, so putting it under the trigger needs CSS anchor positioning or, again, a few lines of JS. Either is acceptable; <details> is the one already in the codebase.

Details that matter either way:

  • The panel is positioned relative to the cluster, right-aligned, on top of page content, and it works in both themes (card-like surface: border-ink-200 bg-white dark:border-ink-800 dark:bg-ink-900).
  • The trigger has an accessible name that includes the person's name ("Account menu, Alex Morgan").
  • The rendered header must stay a plain, cacheable template: no per-request JS, no state beyond the open attribute.

Classification

Enhancement. Not breaking: no schema, no setting, no URL changes; the only behaviour that moves is where two buttons and one form live.

Depends on

  • #7 for the avatar (the initials fallback is enough to start)
  • #8 for the chevron, and the icons beside the menu items if wanted
  • #9 for the theme switch that shares the cluster
  • #2 makes the name reliable; not a blocker

Open questions

  1. Does Export everything move into the menu (proposal: yes), or does the menu hold Your details, Settings (#22) and Sign out only? Invitations is decided: Server settings (#24).
  2. Should the dashboard's new Record and Capture be its page-header actions (proposal) or a card of its own ("Start something")?
  3. Whether the main navigation gets a compact form on narrow screens in the same change, now that the right side is small enough to fit beside it. Probably its own issue.
## Observation > the top nav bar, on the right should contain only the avatar + full name + theme switch. the logout option should show only on a dedicated dropdown menu that opens when the user clicks on the avatar or the name ## What exists today The right cluster of the header (`src/postulo/templates/base.html`, line 57 onwards) holds four things: *Capture* (secondary button), *Record* (primary button), the display name as a link to *Your details*, and a *Sign out* form with its own button. No avatar, no switch, no menu. `display_name` (`accounts/models.py`, line 71) is the full name when there is one and the local part of the email address otherwise; #2 makes the full name obligatory, after which the header always shows a name. Two facts shape the implementation: - **Sign out is a POST.** django-allauth's logout view refuses GET by default (`ACCOUNT_LOGOUT_ON_GET` is not set anywhere in `config/settings/`), and rightly so: a GET logout is a one-line CSRF. The menu entry therefore stays a form with a button, styled as a menu item; it does not become a link. - **No inline script, no Alpine.** The CSP is `script-src 'self'`; the project's client-side behaviour is one delegated-events file (`static/js/app.js`). A menu has to fit that pattern. ## Shape **1. The cluster becomes exactly three things, in the order given:** ``` [ (avatar) Alex Morgan ▾ ] [ ☾ ] ``` - **Avatar and full name together are one trigger**, not two: a single control containing the picture (#7; its initials fallback is enough) and the name (`display_name` until #2), with a small chevron (#8) so it reads as a menu and not as a link. On narrow screens the name hides and the avatar alone remains the trigger. - **The theme switch** from #9, at the far right, outside the menu. It stays visible in the bar because that is the point of it. **2. The account menu**, opened by the trigger: - *Your details* — the link the name used to be - not *Invitations*: inviting people is user management, and it lives under *Server settings → People* (#24), decided 2026-09-05. The staff-only item in the main navigation (`base.html`, line 47) goes away with it, which shortens the bar. Administrators reach Server settings from this menu (#24), so nothing is further away than before. - *Export everything* — today reachable from *Insights* and the dashboard; an account menu is its natural home as well. Optional, same reasoning. - a rule - **Sign out** — the POST form, styled as the last item **3. Capture and Record leave the header.** "Only" means only. Both already exist as page-level buttons where they belong: *Record an application* on the Applications list (twice), the Board and Insights; *Capture a posting* on the captures list. The one page that relies on the header buttons is the **dashboard**, which has neither of its own (`core/dashboard.html` links to export and nothing else in that family). So the dashboard gains its own two actions in its page header, which is where a day starts anyway. Nothing else loses a button. ## How the menu is built **Proposal: `<details>` and `<summary>`**, which the project already uses (`jobs/capture_form.html`, line 22). It opens and closes with no script, the trigger is focusable and keyboard-operable out of the box, and assistive technology announces the expanded state. This is the native form of the WAI-ARIA *disclosure navigation menu* pattern, which is the recommended one for site navigation (not `role="menu"`, which is for application menus and brings its own keyboard contract). What the native element does not do is close on a click elsewhere or on Escape. That is a few lines in `app.js`, delegated from the document like `data-autosubmit`: any open `details[data-menu]` closes when the click lands outside it or Escape is pressed, and focus returns to the trigger. Markup swapped by htmx keeps working, because nothing is bound to an element. **The alternative** is the Popover API (`popover` on the panel, `popovertarget` on the button), which gives light dismiss and Escape natively and needs no JS at all. Its drawback is placement: a popover lands in the top layer centred in the viewport by default, so putting it under the trigger needs CSS anchor positioning or, again, a few lines of JS. Either is acceptable; `<details>` is the one already in the codebase. **Details that matter either way:** - The panel is positioned relative to the cluster, right-aligned, on top of page content, and it works in both themes (`card`-like surface: `border-ink-200 bg-white dark:border-ink-800 dark:bg-ink-900`). - The trigger has an accessible name that includes the person's name ("Account menu, Alex Morgan"). - The rendered header must stay a plain, cacheable template: no per-request JS, no state beyond the `open` attribute. ## Classification Enhancement. Not breaking: no schema, no setting, no URL changes; the only behaviour that moves is where two buttons and one form live. ## Depends on - #7 for the avatar (the initials fallback is enough to start) - #8 for the chevron, and the icons beside the menu items if wanted - #9 for the theme switch that shares the cluster - #2 makes the name reliable; not a blocker ## Open questions 1. Does *Export everything* move into the menu (proposal: yes), or does the menu hold *Your details*, *Settings* (#22) and *Sign out* only? *Invitations* is decided: Server settings (#24). 2. Should the dashboard's new *Record* and *Capture* be its page-header actions (proposal) or a card of its own ("Start something")? 3. Whether the main navigation gets a compact form on narrow screens in the same change, now that the right side is small enough to fit beside it. Probably its own issue.
tiagoagueda added this to the 0.2.0 milestone 2026-09-05 12:13:25 +00:00
Author
Owner

Shipped in 6f283b7 (with a follow-up in 6f32f55). The trigger uses the initials tile as its avatar; #7 adds the picture on top of it and no longer blocks this. The dependency is removed so the issue can close.

Shipped in 6f283b7 (with a follow-up in 6f32f55). The trigger uses the initials tile as its avatar; #7 adds the picture on top of it and no longer blocks this. The dependency is removed so the issue can close.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
Postulo/postulo#10
No description provided.