Files
cgalo5758 660485d9b8 Make status/explorations a local notebook git ignores
The exploration directory held 245 tracked files, and one week's
notebook had grown to 28M of captures, per-run CSS copies and model
logs, so every change ended with an argument about what to prune. It is
now a local lab notebook: git ignores it, its tracked files are
untracked, and no tracked file cites a path under it, because another
clone does not have it.

Every live citation is rewritten so the fact stands in the sentence:
the docs pages, AGENTS.md, four main specs, one Go comment, issues.md
and milestones.md. The operator IA page gains the landed IA revisions it
used to point at, and the UX rubric moves to docs/first-contact-rubric.md
because the process requires it. Archived changes, status/archive,
status/log and design/ keep their old paths as frozen history; the
design/ ones are logged as an upstream issue. MAINTAINING.md and the
status README describe the notebook and the harvest that replaces
pruning.
2026-09-19 19:46:52 -05:00

19 KiB

chrome-conventions Specification

Purpose

Cross-cutting contract for page chrome outside forms (UX-13): location trails, row navigation, action weight, member navigation structure, and the identity-provider handoff.

Requirements

Requirement: One location trail

Every page except the surface's root SHALL carry exactly one location trail: a Bootstrap breadcrumb rendered by the pageHeader part above the title, whose first crumb is the surface's root ("Operator", linking to /operator, on the operator surface; "Home", linking to /, on the member surface), followed by the page's rail entry when the page sits below one, then real intermediate pages only, then the current page, unlinked with aria-current="page". A rail-entry page therefore reads "Operator / Products". The surface roots themselves (the operator overview at /operator, the member dashboard at /) SHALL carry none; error pages SHALL carry none and SHALL link out in their copy instead. The trail is what states the surface: the top bar SHALL NOT carry a surface label. Pages SHALL NOT render "← Back to {parent}" links or any other back affordance, and SHALL NOT vary the trail's position or separator. (Adopted by the maintainer 2026-09-01: below the lg breakpoint the rail is a drawer, so the trail is the only visible statement of where a page sits; every page header gets the same anatomy; the root crumb is the way home.)

Scenario: Trails are uniform

  • WHEN a user views any page other than the surface root
  • THEN the location trail is a breadcrumb above the title reading "{surface root} / {rail entry} / … / {current page}" with the current page unlinked
  • AND no back link renders anywhere on the page

Scenario: A rail-entry page is rooted at the surface

  • WHEN an operator views /operator/products
  • THEN the trail reads "Operator / Products", with "Operator" linking to /operator

Scenario: The root and an error page carry no trail

  • WHEN a user views the surface root, or an error page
  • THEN no trail renders, and an error page's copy links a way out

Every browse table's first column SHALL be the record's human-readable identifier (its name, or its reference number), rendered inside a <th scope="row"> cell — the row's semantic header, semibold via app.css so it stands out from body text without repeating the column headers' full weight (maintainer, 2026-08-30) — as a plain text link to the page that holds the record: the record's own detail page when it has one, or, when the record's home is a section of another page, that page with a fragment anchor on the section (a grant's home is its organization's composite, so a grants row links to /operator/organizations/{orgID}#plan-grants-panel); a record with neither renders it as plain text. Tables SHALL NOT render navigation as buttons ("View", "Manage"): a button in a table is a verb that changes or discloses something, and rows without such verbs carry no Actions column. Real verbs render in a trailing Actions column, at most two visible; a row that carries no verb while its siblings do SHALL render an empty Actions cell, never explanatory text in the verb's place. Whole rows SHALL NOT be click targets. A cell naming another record links to that record as plain text.

Scenario: A row navigates through its identifier

  • WHEN a browse table lists records that have detail pages
  • THEN each row's first-column identifier renders in a <th scope="row"> cell as a text link to its detail page
  • AND no "View" or "Manage" button renders anywhere in the table
  • WHEN a browse table lists records whose home is a section of another page (grants on the organization composite)
  • THEN each row's first-column identifier links to that page with the section's fragment anchor
  • WHEN a row carries a real action (revoke, extend, delete)
  • THEN the action renders as a verb-labeled control in the trailing Actions column, separate from the row's navigation link

Scenario: A verb-less row leaves its Actions cell empty

  • WHEN a table carries an Actions column and a row has no action (a system-managed grant, an operator-root domain claim)
  • THEN that row's Actions cell renders nothing, and the reason lives in the row's other columns

Requirement: One dominant primary action per screen

A screen SHALL present at most one button in the primary (filled) style; additional actions render in subordinate styles (outline or link). Two equal-weight primary buttons SHALL NOT appear on one screen.

Scenario: Detail pages demote secondary actions

  • WHEN a user views a page with more than one available action
  • THEN exactly one action carries the primary style

Requirement: The identity-provider handoff is marked subtly

The "Identity and Access" control, which renders inside the account menu on both surfaces, SHALL carry a subtle external-destination marker and a hover tooltip in plain words (no "identity provider" jargon) stating that it manages the account and opens in a new tab, so the externally-branded console that follows is expected rather than alarming; inline explanation copy SHALL NOT render beside the label (maintainer decision 2026-08-23: explanations do not belong in navigation, and nothing may widen the rail). The marker SHALL be a small monochrome inline icon that inherits the link's color and renders on the same line as the label — not an emoji or a character with emoji presentation (maintainer decision 2026-08-23: U+2197 rendered as an emoji and wrapped to its own line), and not separable from the label by line wrapping.

Scenario: Handoff is hinted, not explained inline

  • WHEN a user opens the account menu and views the Identity and Access control
  • THEN it carries a small monochrome external-destination icon on the same line as the label and a plain-words hover tooltip
  • AND no inline explanation copy renders in the menu
  • AND the marker is not an emoji or emoji-presentable character

Requirement: The sidebar has a fixed width

The application sidebar SHALL hold a fixed width on large screens; entry content SHALL NOT widen the rail — a label too long for the rail wraps instead.

Scenario: Long content cannot widen the sidebar

  • WHEN a sidebar entry's content is wider than the rail
  • THEN the content wraps within the fixed width and the sidebar's width is unchanged

Requirement: One shell for both surfaces

The member surface (dashboard, products, billing) and the operator surface SHALL render one application shell: the same top bar, the same rail geometry, the same account menu, and the same responsive behaviour, produced from one shared template rather than per-page copies. The top bar SHALL carry the brand (the configured deployment name, linking to / on both surfaces) as its left-most element on both surfaces at every width, and one control at its right end: the account menu at lg and up, the menu toggler below lg. The top bar SHALL NOT carry section links or a surface label on either surface at any width; the trail's root crumb states the surface. At lg and up the rail SHALL be visible without any interaction and SHALL carry task destinations only (Dashboard, Products, Billing on the member surface; the operator sections on the operator surface) plus the surface switch, and no session or account control. Below lg the rail SHALL be a drawer, opened from the top bar's toggler and closed by choosing an entry, by its close control, or by the backdrop, and it SHALL be the shell's only menu: the task destinations and the surface switch in one list, then the account menu's items as a second, separately named list at the drawer's foot. The two SHALL be told apart by distance first: the account group SHALL sit at the foot of the drawer whatever the surface's section count, and SHALL carry one divider and no other rule of its own. Navigation SHALL NOT be hidden behind a toggler at lg and up.

Scenario: The top bar names no surface

  • WHEN an operator views any operator page at any viewport width
  • THEN the top bar renders the brand and its one control, and no surface label and no Dashboard, Products, Billing, or Operator links

Scenario: The member top bar matches the operator top bar

  • WHEN a member views the dashboard, products, or billing page
  • THEN the top bar renders the brand and its one control in the same positions as on the operator surface

Scenario: One menu reaches the sections and the account

  • WHEN an operator views an operator page below the lg breakpoint and opens the bar's toggler
  • THEN one drawer opens listing the eight operator sections and the surface switch, and, at its foot under a single divider, the account menu's items headed by the person's name and email
  • AND the account group sits at the same place in the member surface's drawer, which lists three sections
  • AND choosing a section navigates and closes the drawer, leaving the page scrollable

Scenario: Desktop navigation is never hidden

  • WHEN a person views any shell page at lg or wider
  • THEN the rail is visible without opening anything, it carries no account or session control, and no toggler renders

Requirement: Session controls live in the account menu on both surfaces

Both surfaces SHALL render exactly one account menu at a time, never two: at lg and up a dropdown at the right end of the top bar, triggered by a control labelled with the signed-in person's display name (the username when no display name is known); below lg a named group at the foot of the shell's one drawer, after the rail entries. Whichever renders SHALL contain, in order: a non-interactive header with the person's name and email; the "Identity and Access" control, linking to the identity provider's account console at the configured oidc-idp-account-url; a "Get help" control linking to the configured support-url, present only when that key resolves to a value; the "Sign out" control, last. The dropdown SHALL set Sign out apart with a divider. The drawer's group SHALL NOT draw a divider inside itself: it is separated from the rail's entries by one divider and by the drawer's remaining space, and it is named by the person's name and email, which SHALL be the group's accessible name so that assistive technology can tell it from the list of destinations above it. It SHALL contain nothing else, and no session or account control SHALL render anywhere outside it; help SHALL have no other home in the shell (maintainer, 2026-09-02: the bar has no room for a fourth item at narrow width). "Sign out" SHALL render in the menu's ordinary text colour, not the destructive colour, SHALL always be the last item, and SHALL be a control that posts at both widths. The menu SHALL be driven by declarative attributes so it works under the strict content security policy without page scripts. The items SHALL come from one template shared by both surfaces, so the two surfaces' menus cannot drift apart.

Scenario: The account menu holds exactly the session controls

  • WHEN a signed-in person opens the account menu on either surface at either width
  • THEN it lists the name-and-email header, Identity and Access, Get help when a support URL is configured, and Sign out, in that order, and nothing else

Scenario: The drawer's group is named and set apart by space

  • WHEN a signed-in person opens the drawer below lg on a viewport with room to spare
  • THEN the account items render as their own list at the drawer's foot, under one divider, headed by the person's name and email
  • AND that name and email is the list's accessible name
  • AND no divider renders between the account items themselves

Scenario: Only one account menu renders

  • WHEN a signed-in person views any shell page below lg
  • THEN the account items render in the drawer and no account trigger renders in the top bar
  • WHEN the same person views it at lg or wider
  • THEN the account items render in the top bar's dropdown and no account items render in the rail

Scenario: Get help appears only with a destination

  • WHEN support-url resolves to no value
  • THEN the menu renders no "Get help" item
  • WHEN it resolves to a URL
  • THEN "Get help" opens that URL in a new tab from either surface

Scenario: Sign out is plain, last, posting, and everywhere

  • WHEN a signed-in person views any shell page at any width
  • THEN the only session-ending control is the "Sign out" item at the end of the account menu, rendered without destructive styling, and it posts rather than following a link

Scenario: The trigger names the person

  • WHEN the identity provider supplied a display name and the person views a shell page at lg or wider
  • THEN the account menu's trigger reads that name
  • WHEN it supplied no display name
  • THEN the trigger reads the username

Requirement: The surface switch is a visible, mirrored rail entry

The switch between the member surface and the operator surface SHALL be exactly one rail entry on each side, rendered last among the rail's entries and separated from the task entries by the rail's border-top grouping idiom: "Operator panel" on the member rail, shown only to people who hold the operator role, linking to /operator; "Member panel" on the operator rail, linking to /. The switch SHALL NOT be duplicated in the account menu or the top bar, and SHALL never carry the active state. Below lg, where one drawer carries both the rail entries and the account items, the switch SHALL render once among the rail entries, above the account group's divider, and never inside the account group.

Scenario: Operators see the switch on the member rail

  • WHEN a person with the operator role views a member page
  • THEN the member rail ends with a separated "Operator panel" entry linking to /operator
  • WHEN a person without the operator role views a member page
  • THEN no "Operator panel" entry renders

Scenario: The operator rail leads back

  • WHEN an operator views any operator page
  • THEN the operator rail ends with a separated "Member panel" entry linking to / that is never marked active

Scenario: One place for the switch

  • WHEN a person looks at the top bar, or opens the drawer below lg
  • THEN exactly one switch entry renders, in the rail's list, and none renders in the top bar or in the account group

Requirement: The hypermedia engine is htmx 4 with explicit inheritance

The vendored hypermedia library (internal/embeds/static/htmx.min.js) SHALL be the htmx 4.x line, loaded without the htmx-2-compat extension and without the implicitInheritance config flag. Attribute inheritance SHALL be explicit: an attribute that must reach descendants carries the :inherited modifier (the shell's hx-boost and CSRF hx-headers, and container-level targets whose child controls declare none of their own), and an attribute without the modifier applies only to its own element. hx-disinherit, htmx 2 camelCase event names in first-party scripts, and any htmx-2-compat reference SHALL be lint failures.

Scenario: Inheritance is visible in the markup

  • WHEN a control's request behavior depends on an ancestor's htmx attribute
  • THEN that ancestor attribute carries :inherited
  • AND no element relies on implicit inheritance or guards against it with hx-disinherit

Scenario: The compat shim never ships

  • WHEN member-console lint runs
  • THEN a template or script referencing htmx-2-compat, using hx-disinherit, or listening for an htmx 2 camelCase event name fails the build

Requirement: Handlers that branch on HX-Request declare it with Vary

A handler whose response body or headers differ by the HX-Request request header SHALL set Vary: HX-Request on every response from that branch point, so a shared cache never serves the htmx variant of a URL to a full-page navigation or the reverse. Routes SHALL keep fragments at dedicated /partials/* paths rather than branching page URLs on HX-Request, so a history restore's re-fetch of a pushed URL receives the full page.

Scenario: A back navigation re-fetches and gets the full page

  • WHEN a user filters or pages an operator list (which pushes the list URL) and later presses the browser back button
  • THEN htmx re-fetches the previous URL and the server returns the full page
  • AND the restored view renders inside the intact shell

Scenario: The error renderer varies

  • WHEN RenderErrorPage answers an htmx request with plain text or a navigation request with the full error page
  • THEN both responses carry Vary: HX-Request

Requirement: The shell meets AA contrast

The shared shell's text colours SHALL meet the WCAG AA contrast ratio against the surfaces they sit on: the rail's entry links (rest and active) against the rail background, the account trigger against the bar, and links and buttons inside card headers against the header background (card headers are white, and their links use the console's darker link colour, so the organization composite's tab links and "Issue grant" button pass), so no page inherits a contrast failure from the shell. A shell change SHALL be checked against the accessibility baseline before it is accepted.

Scenario: The rail, the bar, and card headers pass

  • WHEN a Lighthouse accessibility snapshot runs on /operator, /, and an organization composite
  • THEN no color-contrast failure names a rail link, the account trigger, or a card-header link or button

Requirement: The top bar holds the brand and one control

The top bar SHALL hold the brand and exactly one control, and nothing else, ever: the menu toggler below the lg breakpoint, the account menu at lg and up. No surface label, no search, no help button, no utility icons, and never two menus at one width. The brand SHALL be the bar's left-most element at every width and SHALL truncate with an ellipsis rather than wrap when the deployment name is long; the account trigger SHALL keep its width cap where it renders. (The bar was measured at 390px on 2026-09-02 carrying three items — toggler, brand, account trigger — with 13px to spare. The account trigger leaves the bar below lg with this change and the room returns to the deployment name.)

Scenario: Nothing joins the bar

  • WHEN a person views any shell page at 390px wide
  • THEN the bar shows the brand at its left edge and the menu toggler at its right, the document does not scroll horizontally, and no other control sits in the bar

Scenario: The bar swaps its one control at the breakpoint

  • WHEN a person views any shell page at lg or wider
  • THEN the bar shows the brand at its left edge and the account menu's trigger at its right, and no toggler renders

Scenario: A long deployment name truncates

  • WHEN the configured deployment name is longer than the space beside the bar's one control at 390px
  • THEN the brand renders on one line with an ellipsis and the control stays visible