Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions design/canvases/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

File renamed without changes.
17 changes: 14 additions & 3 deletions design/incoming/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion documentation/architecture/ROUTE_MAP.md

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions documentation/architecture/decisions/D607.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
## D607 — The Ops canvas graduates, and the Account center's sections are one menu on a phone

**Issue #1165, slot S01, 2026-10-06.** Frontend and design files only. No
Worker change, no migration and no new route.

The owner sent five links on #1165 on 2026-10-06. They are one canvas, the
Ops canvas D566 landed: four carry the same `Ops Page`, and the fifth
(`XACu9r2t6YmMSXRcdLrJDr`) is the export before it. The board shells differ
only in which row of artboards opens first.

### What was true on main

- **`Ops · Boards.dc.html`, `Ops Page.dc.html` and `ops-data.js` sat in
`design/incoming/`**, although all five areas they draw were built: the
event organiser (D567), the Account center (D568), partner earnings (D569),
Wellbeing (D570) and the advisor's practice (D571). The intake README still
graded four of the areas "not yet triaged".
- **On a phone, the Account center's section list was a row of chips that
scrolled sideways.** D568 built it to the export of that day, whose phone
rule was `.op-secnav{flex-direction:row;overflow-x:auto}`.

### What the newer export changes

One thing. On a phone, `.op-secnav` is hidden and a new `.op-secdrop` row
takes its place: the word "Section", the open section's name, and ▾. It is
44px tall and drawn as a card with a hairline border. The page's script names
the open section as `acCurrent`, from the screen's heading. Its font link adds
Space Grotesk's 800 weight, which the 24px titles use. Nothing else in the
page moved, and the board file did not change.

### The change

- **The canvas graduates.** The three files move to
`design/canvases/integrated/`, and the newer export replaces `Ops Page`
there. Only its three serialisation differences from the committed file
(`<html><head>`, `data-dc-script=""` and `</body></html>`) were kept in the
committed form, so the diff shows the design's change and nothing else.
`integrated/` goes 77 → 79 in both READMEs, and the intake README's row for
the four areas names their routes and decisions.
- **Below `lg`, `AccountSectionNav` is one Section row**
(`pages/account/AccountCenter.jsx`). It draws the canvas's row and lays a
transparent native `<select>` over it, labelled "Account section". The
phone opens its own picker, and the keyboard and a screen reader get a real
control; the row shows a focus ring when the select has focus. The options
are the same sections in the same order: the canvas's six first, then the
four kept ones in a "More" group. A section id the list does not hold names
the first section rather than a blank row.
- **From `lg` it is unchanged:** the column beside the content, with "More"
over the four kept sections.
- **The chip row is gone**, and with it the effect that scrolled the active
chip into view.

### Why a native select

The canvas draws the closed row only. Before D568 this page had a custom
dropdown: a button over a `role="listbox"` list with no arrow-key handling. A
native select needs no focus management or outside-click code, and on a phone
it is the picker the phone already knows. The drawing stays the canvas's,
because the select is transparent and fills the row.

### Tests

`frontend/test/account_center_d568.test.mjs`:

- The section list test checks that the column is the `lg` shape.
- A new test checks the phone row:
- it is hidden from `lg` and is 44px tall;
- it reads "Section Billing and orders ▾" with that section open;
- its options are in the list's order, with the kept ones under "More" and
the open one selected;
- a pick reaches `onSelect`;
- a stray id names the first section;
- the sideways row is gone.
58 changes: 38 additions & 20 deletions frontend/src/pages/account/AccountCenter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,25 +173,22 @@ export function Chips({ items, label }) {
/**
* The section list on the left of the content — the canvas's `acNav`.
*
* ONE ELEMENT, TWO SHAPES, which is the canvas's own phone rule
* (`.op-secnav{flex-direction:row;overflow-x:auto}`): a column beside the
* content from `lg`, a row of chips that scrolls sideways below it. The six
* canvas sections come first in the canvas's order; the four the old page had
* and the canvas does not draw follow under "More", kept rather than dropped.
* TWO SHAPES, which is the canvas's own phone rule (D607, the 2026-10-06
* export: `.op-secnav{display:none}.op-secdrop{display:flex}`). From `lg` it
* is a column beside the content. Below `lg` it is one row, "Section", the
* open section's name and ▾, and the list opens from it. The six canvas
* sections come first in the canvas's order; the four the old page had and
* the canvas does not draw follow under "More", kept rather than dropped.
*
* The phone row is a native select under the canvas's drawing, so the phone
* opens its own picker, and the keyboard and a screen reader get a real
* control. D568 drew a row of chips that scrolled sideways here, after the
* export it was built from; the newer export replaced that row.
*/
export function AccountSectionNav({ sections, active, onSelect }) {
const rowRef = useRef(null);
const main = sections.filter((s) => s.group !== 'more');
const more = sections.filter((s) => s.group === 'more');

// On a phone the active chip can sit off the edge of the row. Scroll the
// row itself, never the page.
useEffect(() => {
const row = rowRef.current;
if (!row || row.scrollWidth <= row.clientWidth) return;
const btn = row.querySelector('[aria-current="page"]');
if (btn) row.scrollLeft = btn.offsetLeft - (row.clientWidth - btn.clientWidth) / 2;
}, [active]);
const current = sections.find((s) => s.id === active) || main[0] || sections[0];

const item = (s) => {
const on = s.id === active;
Expand All @@ -202,7 +199,7 @@ export function AccountSectionNav({ sections, active, onSelect }) {
onClick={() => onSelect(s.id)}
aria-current={on ? 'page' : undefined}
data-testid={`account-nav-${s.id}`}
className={`shrink-0 whitespace-nowrap rounded-axal-sm px-3 py-2 text-left text-[13px] transition-colors lg:whitespace-normal ${
className={`rounded-axal-sm px-3 py-2 text-left text-[13px] transition-colors ${
on
? 'bg-axal-lavender font-semibold text-axal-violet-deep dark:bg-violet-900/30 dark:text-violet-300'
: 'font-medium text-axal-muted hover:bg-axal-ground hover:text-axal-ink dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'
Expand All @@ -213,20 +210,41 @@ export function AccountSectionNav({ sections, active, onSelect }) {
);
};

const option = (s) => <option key={s.id} value={s.id}>{s.label}</option>;

return (
<nav aria-label="Account sections" data-testid="account-section-nav" className="min-w-0 lg:sticky lg:top-4 lg:self-start">
<Card padding="none" className="p-1.5">
<div ref={rowRef} className="flex gap-0.5 overflow-x-auto lg:flex-col lg:overflow-visible">
<Card padding="none" className="hidden p-1.5 lg:block">
<div className="flex flex-col gap-0.5">
{main.map(item)}
{more.length > 0 && (
<>
<span aria-hidden="true" className="mx-1 w-px shrink-0 self-stretch bg-axal-hairline dark:bg-gray-700 lg:hidden" />
<SectionLabel className="hidden px-3 pb-1 pt-3 lg:block">More</SectionLabel>
<SectionLabel className="px-3 pb-1 pt-3">More</SectionLabel>
{more.map(item)}
</>
)}
</div>
</Card>
<div
data-testid="account-section-menu"
className="relative flex min-h-[44px] items-center justify-between gap-2.5 rounded-axal-lg border border-axal-hairline bg-white px-3.5 focus-within:ring-2 focus-within:ring-axal-violet/40 dark:border-gray-800 dark:bg-gray-900 lg:hidden"
>
<span aria-hidden="true" className="flex min-w-0 items-center gap-2">
<span className="shrink-0 text-[11px] font-semibold uppercase tracking-[0.08em] text-axal-muted dark:text-gray-400">Section</span>
<span className="truncate text-sm font-semibold text-axal-ink dark:text-gray-100">{current?.label}</span>
</span>
<span aria-hidden="true" className="shrink-0 text-axal-muted dark:text-gray-400">▾</span>
<select
aria-label="Account section"
value={current?.id ?? ''}
onChange={(e) => onSelect(e.target.value)}
data-testid="account-section-select"
className="absolute inset-0 h-full w-full cursor-pointer appearance-none opacity-0"
>
{main.map(option)}
{more.length > 0 && <optgroup label="More">{more.map(option)}</optgroup>}
</select>
</div>
</nav>
);
}
Expand Down
Loading
Loading