Rebuild the masthead on the component layer: an avatar-only account button, a + menu, and a main navigation made for a phone #282
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Postulo/postulo#282
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The masthead is the one element on every page, and it is the oldest thing in the interface: a flex row that has had a control added to it each time one was needed — the wordmark, seven links, a
<details>that hides them under Menu below 768 pixels, a search box, an account disclosure with a name in it. #262 and #263 since gave the project a component layer (Basecoat structure, Postulo paint, composed withdjango-cotton), and the header has had none of it. This is the rebuild.Four things, in the order they were asked for:
+menu beside it, to start a listing, a document, a company or an event from anywhere.What the masthead is today
src/postulo/templates/base.html:94-199, one<header class="sticky top-0 z-30">holding a singleflex flex-wraprow::105-113) — logo and instance name; takes over as the dashboard link, with the active style and its own accessible name, when Dashboard is hidden (#23).partials/nav_links.htmlrendered into a<details class="md:hidden">panel (:129-138) and again into a<nav class="hidden md:flex">(:139-141). Items come fromcore/navigation.py:ITEMS, which is seven of them; the comment at:116records that six already measured 635 pixels and made every page in Postulo scroll sideways at 320 — an AA Reflow failure, fixed by the disclosure (#113).:145-152) —class="hidden md:block". There is no search on a phone at all, which is worth stating plainly because it is invisible in the markup's intent.:158-192) — a<details class="popover" data-menu>whose<summary>is{% avatar user %}+{{ user.display_name }}(hidden sm:inline) + a chevron. Inside: Your details, Settings, Server settings for staff, a separator, the theme switch as a menu row (#197), and Sign out.Deliberately not
role="menu": the comment at:153records the reasoning — this is navigation, so it is a disclosure where Tab moves between rows; the people table's row actions are genuine actions and get the menu role (#262). Whatever replaces it keeps that distinction.1. The avatar as the button
{% avatar user %}iscore/templatetags/postulo.py:206. It draws the picture, or an initials tile coloured by a stable hash of the display name.It is decorative on purpose —
alt=""on the picture,aria-hidden="true"on the tile — and the docstring gives the reason: "it always stands beside the person's name." Taking the name out of the trigger makes that sentence false. Two things follow, and neither is optional:<summary>already carriesaria-label="Account menu, {{ name }}"(:160), so the control stays named. That label becomes the only name, so it is now load-bearing, and the tag's docstring should stop claiming a name is always beside it.size-7— 28 pixels. On its own that clears WCAG 2.2 AA's 24 (tests/e2e/test_target_size.py, measured not read) with four pixels to spare, and is nowhere near the 44 that is comfortable for a thumb. It needs padding around it, not a bigger avatar.2. The name inside the menu
A header row at the top of the panel — avatar, display name, and the email underneath it if it earns the space. Signed in as … reads as a security statement and is the right phrasing on a multi-person instance (#268 is the issue for those); the name alone is quieter. Either is fine; it should be decided once and written down, not left to the template.
Not a
menuitem— it is a label, not a destination, unless it becomes the Your details link with the name as its text.3. The
+menuFour things were asked for. Three are a link to a page that exists; one is not, and it is the reason this section needs a decision before any markup:
listings:create— andjobs:capture_create, which is how a listing usually starts (paste a URL)documents:cv_create,documents:letter_create,documents:upload_createjobs:company_createapplications:event_createtakes an application's pk. An event belongs to an application; there is no route that makes one from nowhere.applications:reminder_createis global and is the thing somebody actually wants from a masthead), or an event gains a standalone create flow whose first field picks the application. The second is a feature, not a menu entry, and would be better as its own issue than smuggled into this one. Recommendation: ship Reminder, and open the standalone-event question separately. #238 (deadlines and reminders) is adjacent.applications:create) and was not in the list. Worth deciding rather than omitting silently.<c-dropdown-menu>(templates/cotton/dropdown_menu.html), which is exactly this:<details>, Basecoat's popover box,role="menu"withmenuitemrows, and the arrow-key handlingapp.js:911-950already adds. These are actions, so the menu role is right here — unlike the account disclosure beside it.static/icons/hasplus.4. The main navigation
The present design is one row, doubled, with a breakpoint switching which copy is in the layout. It works and it is dull, and the three adjectives asked for point at different things:
app.js:757measures it into--header-heightfor everything that has to clear it. Options worth weighing: icons beside labels so the row survives translation; an overflow More menu once the row runs out; grouping seven items into four (Search / Pipeline / Documents / Records). Whatever is chosen,--header-heightmust keep working — the sticky sidebars, the skip link andscroll-padding-topall read it.hidden_nav_itemson the profile,navigation.HIDEABLE, and the switches inAppearanceForm(accounts/forms.py:424). What is missing is order — a person can hide Calendar but cannot put Applications first. If reordering goes in, the stored shape changes from a list of hidden keys to something that also carries position, andnavigation.py's docstring explains why hidden-not-shown was chosen (a new item in a later release must appear for everybody); the same care applies to whatever replaces it. Note that #281 proposes an Accessibility settings section, which may move where these switches live.What the component layer actually offers, and its two hard limits
basecoat-css@1.0.2ships 39 component stylesheets. Three are imported (assets/css/app.css:24-26):button,popover,dropdown-menu. Unimported and relevant here:avatar,sidebar,drawer,command,dialog,tooltip,button-group,item,badge.Before anyone reaches for them:
cotton/dropdown_menu.htmlstates it: Basecoat's own dropdown is a<button>plus a panel its script reveals, "and a menu somebody cannot open with scripts off is not one this project ships" — hence<details>/<summary>, withapp.jsadding only what the platform lacks.drawer.js,sidebar.jsandcommand.jsare all script-first. Their stylesheets may still be usable; their behaviour has to be rebuilt on a disclosure, or left out.tests/test_stylesheet.pyrefuses any Basecoat import whose class namesapp.cssalso defines. The bundle would bring a.cardand an.alertunder names this project owns, and the cascade — not the author — would decide which won. Any new import is a deliberate, tested decision.A command palette (
command.css) is tempting for "fluid", and would answer the missing phone search besides. It is also the single most script-dependent thing in the list. If it appears, it appears on top of a search box that works without it, never instead of one.Boundary with #73
#73 (Readable and usable on a phone, 0.6.0) owns the page bodies: tables, the board, touch targets, form
inputmode, the dashboard, and adding a phone viewport to the browser suite. It takes the navigation as given — "the navigation collapses" is listed there as an existing fact.This issue owns the masthead: the account control, the
+menu, and what the main navigation is. The two meet at the phone viewport #73 adds to the suite, which is what would hold this work in place afterwards. Neither blocks the other; doing this one first means #73's phone pass is not fighting a header it also wants to change.What will constrain the work
Four browser tests already fence this area, and a revamp has to keep them green rather than edit them into agreement:
tests/e2e/test_reflow.py::test_the_navigation_becomes_a_menu_and_still_works— 320 pixels, and the reason the disclosure exists at all.tests/e2e/test_target_size.py— 24×24 measured, with the spacing exception; the avatar-only trigger meets it by four pixels.tests/e2e/test_sticky_header.py— the header is still there after scrolling and an anchor clears it.tests/e2e/test_keyboard_and_focus.py,test_people_menu.py— Escape, arrow keys, and focus not being lost when a menu closes (#227).Also: the browser suite reads the live tree, so no template or stylesheet edit while it runs;
npm run build:csswhenever the templates gain classes, since the compiled CSS is committed and CI checks it; and three catalogues (fr-fr,pt-pt,pt-br) filleddraftfor every new string, the other 36 at the release sweep.Suggested order
The four parts are independent and the first three are small. If this is split, 1 + 2 + 3 land together as one commit (they are one control and the button beside it) and 4 is its own issue once its shape has been prototyped — it is the only part where the right answer is not already visible from the code.
Parts 1–3 are in (
01d799c80); part 4 is spun out as #299, per the suggested order —it is the one part whose right answer is not visible from the code, and
test_reflow.py::test_the_navigation_becomes_a_menu_and_still_workspins the currentrow-plus-disclosure that this issue says to keep green rather than edit into agreement.
1. The profile button is the avatar and nothing else. The
<summary>drops the namespan and the chevron's job is noted in the template; the
aria-label("Account menu, …")is now the control's only name, so it stays load-bearing. The target grows from 28 to a
44-pixel box with
p-2around the unchangedsize-7avatar — padding, not a biggeravatar — which clears the 24 measured in
test_target_size.pywith room for a thumb.2. The name inside the menu. A header row at the top of the panel: avatar, display
name, and the email under it, truncating inside the popover's 224px. The decision the
issue asked for is made once and written down in the template: the name on its own, not
Signed in as … (quieter; the security-statement form is not needed on a
single-person instance). It is a label, not a destination — Your details below remains
the link.
3. A + menu beside it.
<c-dropdown-menu>with aplustrigger: Listing (thecapture, which is how a listing usually starts), Application, Company, Reminder,
then a Documents group of CV / Cover letter / Upload a file. Reminder is in for
event (an event has no route that makes one from nowhere); Application is the
obvious fifth, included rather than omitted silently. The group is a
role="group"label, not a heading: a heading in the masthead would land in the document before every
page's
<h1>, and the pages are held to one-h1-first. All seven destination routesalready existed; the one new string (Listing, singular) is filled
draftinfr-fr / pt-pt / pt-br.
Part 4 — the main navigation rethought for a phone — is #299 now, per the suggested
order: it is the one part whose right answer is not visible from the code, and
test_reflow.py::test_the_navigation_becomes_a_menu_and_still_workspins the currentrow-plus-disclosure, so that work rewrites that fence with intent. The standalone-event
question is parked there as well.
Kept green without editing them: the reflow, target-size, sticky-header,
keyboard-and-focus and people-menu browser tests.
tests/test_header.pywas updated tohold the new shape (the + menu's routes, and the name being in the menu rather than the
trigger).