@boost33/ui 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +83 -120
  2. package/dist/components/AccountMenu/AccountMenu.d.ts +42 -32
  3. package/dist/components/AccountMenu/AccountMenu.js +66 -48
  4. package/dist/components/AccountMenu/AccountMenu.js.map +1 -1
  5. package/dist/components/AccountMenu/AccountMenu.module.css.js +44 -38
  6. package/dist/components/AccountMenu/AccountMenu.module.css.js.map +1 -1
  7. package/dist/components/AccountMenu/StandardAccountMenu.d.ts +87 -0
  8. package/dist/components/AccountMenu/StandardAccountMenu.js +86 -0
  9. package/dist/components/AccountMenu/StandardAccountMenu.js.map +1 -0
  10. package/dist/components/AccountMenu/icons.d.ts +2 -2
  11. package/dist/components/AccountMenu/icons.js +21 -8
  12. package/dist/components/AccountMenu/icons.js.map +1 -1
  13. package/dist/components/AccountMenu/initials.d.ts +11 -0
  14. package/dist/components/AccountMenu/initials.js +8 -0
  15. package/dist/components/AccountMenu/initials.js.map +1 -0
  16. package/dist/components/AccountMenu/labels.d.ts +64 -3
  17. package/dist/components/AccountMenu/labels.js +33 -5
  18. package/dist/components/AccountMenu/labels.js.map +1 -1
  19. package/dist/components/AccountMenu/use-menu.d.ts +34 -0
  20. package/dist/components/AccountMenu/use-menu.js +102 -0
  21. package/dist/components/AccountMenu/use-menu.js.map +1 -0
  22. package/dist/components/TopBar/TopBar.d.ts +8 -1
  23. package/dist/components/TopBar/TopBar.js +19 -16
  24. package/dist/components/TopBar/TopBar.js.map +1 -1
  25. package/dist/index.d.ts +4 -2
  26. package/dist/index.js +40 -36
  27. package/dist/index.js.map +1 -1
  28. package/dist/style.css +1 -1
  29. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -27,9 +27,9 @@ why the entry matters more than the number.
27
27
 
28
28
  ## Unreleased
29
29
 
30
- Three components are built to Stage A on main and are not releasable until
31
- each passes its fidelity gate. Their entries wait here for the release that
32
- ships them.
30
+ Two components are built to Stage A on main and are not releasable until each
31
+ passes its fidelity gate. Their entries wait here for the release that ships
32
+ them.
33
33
 
34
34
  ### Added — `CommandPalette`, Stage A only. **Not releasable: it has not passed its fidelity gate** (B33-495)
35
35
 
@@ -93,123 +93,6 @@ Not in `dist/` and not a consumer's business: the specimen route showing the fou
93
93
  states, their `SPECIMEN_ONLY` records (none of them is screenshot-gated, because
94
94
  a *designed* baseline is the gate's to capture and never an agent's), and
95
95
  `tests/command-palette.test.tsx`.
96
- ### Added — `AccountMenu`, Stage A only, **not yet through its fidelity gate** (B33-493)
97
-
98
- > [!danger] Do not include this component in a release until a human has approved
99
- > it. `AccountMenu` has **not passed its fidelity gate.** Nobody has yet looked at
100
- > it beside a reference and said yes, no baseline exists for it, and no measurement
101
- > it draws with has been written into the spec. `CLAUDE.md` is explicit about what
102
- > that means for the three components the Wallet never rendered: "Before the gate,
103
- > nothing is authoritative", and the gate is where the referent is created rather
104
- > than checked. Until that session has happened — a person opening the specimen
105
- > route, judging the component, capturing the first baseline themselves, and
106
- > recording the approved measurements in the spec in the same pass — every number
107
- > this component renders is provisional and may change in a way that moves pixels
108
- > in your app. **If you are cutting a release, either hold this entry back or get
109
- > the gate done first.**
110
-
111
- The twelfth component. The account menu's **panel**: the signed-in person, the
112
- organisation they are acting in, and the rows into Settings and out of the
113
- session. Exported as `AccountMenu`, with `AccountMenuProps`,
114
- `AccountMenuPerson`, `AccountMenuOrg`, `AccountMenuGroup`, `AccountMenuItem`,
115
- `AccountMenuIcon`, `AccountMenuLabels` and `defaultAccountMenuLabels`.
116
-
117
- **It is the panel and nothing else — no trigger, no open state, no dismissal.**
118
- That is not an unfinished half of trigger-plus-panel; it is the part of this
119
- component the two reference drawings agree about. Paper's 004 Components artboard
120
- and its origin on 103 Identity are identical in every property of the panel, and
121
- they disagree about the anchor above it and about where the panel sits relative to
122
- that anchor. Choosing between them is the gate-holder's, so the component takes no
123
- anchor, does no positioning, and leaves `TopBar` untouched. Nothing in this release
124
- changes `TopBar`.
125
-
126
- **What a consumer does with it today: nothing.** Render it and you get a static
127
- panel. It holds no state, so it cannot be opened or closed, and it wires no
128
- handlers, so `Sign out` is a `<button>` that does nothing when pressed. Wiring it
129
- to `useAnchoredOverlay`, giving the anchor its `aria-expanded`, and deciding what
130
- each row does are Stage B, after the gate (`CLAUDE.md`, "Presentational-first").
131
-
132
- **The shape, so it is legible from here:**
133
-
134
- - `person` is `{ name, email, avatarUrl? }`. With no `avatarUrl` the circle draws
135
- bare, which is what the drawings show; with one, the image is decorative and
136
- takes an empty `alt`, because the person's name is on the line beside it.
137
- - `org` is `{ name, initials, role? }`. The initials are decorative; omitting
138
- `role` draws no pill rather than an empty one.
139
- - `groups` is a list of `{ id, items }`, and **a hairline is drawn above every
140
- group** — which is what separates the identity blocks from the first group as
141
- well as each group from the last. Grouped rather than an `items` array plus a
142
- named `footer`, because "footer" would be this package inventing a meaning for
143
- the second group.
144
- - an item is `{ id, label, description?, href?, icon? }`. **Whether a row
145
- navigates is decided by `href` and nothing else** (ADR-0011): with an `href` the
146
- row is a link and renders through `linkComponent`; without one it is an action
147
- and stays a `<button type="button">`. So the package never has to know which of
148
- Profile, Settings, Switch account and Sign out your product treats as a
149
- navigation. `icon` names one of four glyphs the package draws
150
- (`'person' | 'settings' | 'switch' | 'signOut'`); omit it and the glyph lane
151
- stays, empty, so the row labels still line up in one column.
152
- - `linkComponent` is the ADR-0011 escape hatch, unchanged from `NavItem` and
153
- `AppSwitcher`: an `ElementType` defaulting to `'a'`, receiving `href`,
154
- `className` and the row's `data-*`/`aria-*` through unchanged.
155
- - `labels` is `Partial<AccountMenuLabels>` and holds **one** field, `panel`,
156
- default `"Account"` — the panel's accessible name, which a disclosure panel has
157
- no other way to get (ADR-0010). Everything else a person reads is row data you
158
- supply, and therefore translates by construction.
159
-
160
- **The label contract is the `WalletChip`'s, deliberately and in full.** `labels`
161
- is a `Partial`, a field supplied as `undefined` (what a missed next-intl key looks
162
- like) falls back to English rather than blanking the panel's name, and
163
- `defaultAccountMenuLabels` is frozen with no type annotation so the freeze
164
- survives into the emitted `.d.ts`. That is three of this repo's own fixed bugs
165
- (B33-873, B33-877, B33-916) taken as the starting point rather than repeated.
166
- B33-1007 records `AppSwitcher` still diverging on the last of the three; this
167
- component does not add a fourth dialect, and the switcher's remaining divergence
168
- is unchanged here and still that ticket's.
169
-
170
- **A disclosure, not a menu** (ADR-0010): no widget role on the panel, no role on
171
- the rows, every row the real `<a>` or `<button>` it already is, so Tab walks them
172
- in the document's own order.
173
-
174
- **Three new tokens, all three read off an unapproved drawing.** They reach your
175
- app through `./style.css` and `./theme.css` like every other token, and the
176
- derivation comment on each says in as many words that the value is not settled:
177
-
178
- - `--container-account-menu: 312px` — the panel's width.
179
- - `--spacing-3_5: 14px` — the person block's block padding.
180
- - `--spacing-9_5: 38px` — the person block's avatar, the largest in the package.
181
-
182
- Nothing else in the package reads any of the three, so no existing rendering
183
- moves, and **no baseline moved**: all 27 gated states come back unchanged, because
184
- every token here is additive and no shared value was touched.
185
-
186
- **Four places where this does not reproduce its drawing**, each named at its rule
187
- in `AccountMenu.module.css`, and each because the drawn value is not a step this
188
- package's token set holds and the nearest step is within about a pixel — the trade
189
- `PageHeader` already records for `--leading-tight` against Paper's 30px. The row's
190
- glyph-to-text gap (11px drawn, `--spacing-2_5` here); the person block's
191
- name-to-email gap (3px drawn, `--spacing-0_5`, which is what the rows draw for the
192
- same relationship); two 10px type sizes (`--text-2xs`); and every line-height,
193
- because the drawing sets them in px and this package holds only percentages.
194
-
195
- **One open design question, recorded rather than decided.** This panel picks a row
196
- out with `--color-neutral-50`, where every other popover in the package uses
197
- `--color-surface-muted`, one neutral step darker. Reproduced as drawn and raised
198
- at the gate — the drawing was cloned from an Identity screen that predates the
199
- shared token pass, which is why it is the only popover in its own file still bound
200
- to literals.
201
-
202
- #### Not in `dist/`, and so not your concern
203
-
204
- The specimen gains an `AccountMenu` route with two sections, neither screenshot-gated
205
- and both recorded in `SPECIMEN_ONLY` with reasons: the panel, whose first baseline
206
- is a *designed* baseline only the person at the fidelity gate may capture, and a
207
- row rendered through a stand-in link component, which draws identically to the
208
- default by design. `tests/account-menu.test.tsx` adds 28 cases covering the
209
- structure, the row elements, the hairline-per-group rule, the label fallbacks and
210
- the ADR-0011 pass-through — none of them asserting geometry, which jsdom cannot
211
- see.
212
-
213
96
  ### Added — `NotificationCenter`, Stage A only, **not releasable yet** (B33-494)
214
97
 
215
98
  > [!danger] This component has not passed its fidelity gate and must not be
@@ -299,6 +182,86 @@ most consumers meet first.
299
182
  still gains no `aria-haspopup`, and it still opens nothing. Connecting the two is
300
183
  Stage B.
301
184
 
185
+ ## 0.8.1 — 2026-11-05
186
+
187
+ **Not breaking. Add one element to your top bar.** The account menu, the Hub's,
188
+ for every app: the avatar opens the same menu in all ten products (Jamal, 5 Nov
189
+ 2026). New exports only; an app that passes nothing new renders exactly what
190
+ 0.8.0 rendered.
191
+
192
+ **Held back, as before:** `CommandPalette` and `NotificationCenter` under
193
+ Unreleased have still not passed their fidelity gate.
194
+
195
+ ### Added — `StandardAccountMenu`, the account menu behind every app's avatar, and `topBar.account` (B33-493)
196
+
197
+ The Hub's working menu (boost33-hub `src/app/account-menu.tsx`), moved into the
198
+ kit so every app draws the same one. It passed its fidelity gate on 5 Nov 2026
199
+ against a render of the Hub's own component.
200
+
201
+ What a consumer does:
202
+
203
+ ```tsx
204
+ import { AppShell, StandardAccountMenu } from '@boost33/ui'
205
+
206
+ <AppShell
207
+ topBar={{
208
+ actions: <NotificationButton … />,
209
+ account: (
210
+ <StandardAccountMenu
211
+ person={{ name: claims.name, email: claims.email, avatarUrl }}
212
+ org={orgName ? { name: orgName } : undefined}
213
+ role={role} // 'admin' | 'member' | null
214
+ identityUrl="https://id.boost33.com"
215
+ hubUrl="https://app.boost33.com"
216
+ otherAccountsCount={claims.accounts.length - 1}
217
+ />
218
+ ),
219
+ }}
220
+ …
221
+ />
222
+ ```
223
+
224
+ - **Safe from a Server Component layout.** Every prop is plain data. You pass no
225
+ `linkComponent` and no callback, so nothing crosses the client boundary as a
226
+ function. `AccountMenu` is a client component inside the package;
227
+ `StandardAccountMenu` is not.
228
+ - **Remove your own avatar** from `topBar.actions`. `topBar.account` (new on
229
+ `TopBar` and `AppShell`) draws after `actions`, at the far right of the bar.
230
+ - **The rows, the Hub's, with absolute links**, so they work from any app and
231
+ from the Hub itself:
232
+ - Profile, to `{hubUrl}/settings/profile`
233
+ - Security, to `{identityUrl}/account/security`
234
+ - Settings, to `{hubUrl}/settings/org/general`, described by role
235
+ - Switch account, to `{identityUrl}/choose-account`, only when
236
+ `otherAccountsCount` is above 0, described by `otherAccountNames` when every
237
+ account is named, or else by the count
238
+ - Feedback (`mailto:support@boost33.com`) and Docs (`https://docs.boost33.com`)
239
+ - Sign out, to `{identityUrl}/signout`
240
+ - **The Hub's production URL is `https://app.boost33.com`** (Identity's
241
+ `HUB_URL`). `hub.boost33.com` does not resolve.
242
+ - **The trigger is the Hub's**: a 26px avatar with the person's initials, or their
243
+ photo from `person.avatarUrl`, named "Your account". The panel opens 10px below
244
+ it, aligned to its right edge.
245
+ - **Keyboard and focus are the Hub's `useMenu`**: a WAI-ARIA menu button.
246
+ - Opening moves focus to the first row (ArrowUp on the trigger: the last).
247
+ - The arrows wrap; Home and End jump.
248
+ - Escape closes and returns focus to the avatar; Tab leaves and closes.
249
+ - A press outside closes it, and choosing a row closes it.
250
+ - **Words**: `labels` takes the rows' words (`StandardAccountMenuLabels`, English
251
+ defaults the Hub's); `menuLabels` takes the menu's accessible names.
252
+ - **Lower level**: `AccountMenu` takes `groups` of rows of your own, and
253
+ `standardAccountMenuGroups(options)` returns the standard rows as data to
254
+ extend. Do not add per-app rows without asking: one menu everywhere is the
255
+ point.
256
+
257
+ Changed from the Stage A `AccountMenu` that 0.8.0 exported but did not release:
258
+
259
+ - It now draws its own trigger and opens a `role="menu"`, not a static panel.
260
+ - `person.name`, `org` and `org.initials` are optional.
261
+ - The default accessible name is "Your account", not "Account".
262
+ - Rows are `menuitem`s with `tabIndex={-1}`.
263
+ - `AccountMenuIcon` gains `security`, `feedback` and `docs`.
264
+
302
265
  ## 0.8.0 — 2026-11-05
303
266
 
304
267
  **Breaking. Read the B33-1712 entry before you bump.** The 3 November 2026
@@ -6,13 +6,17 @@ import { AccountMenuLabels } from './labels';
6
6
  * menu reads no session and fetches nothing (CLAUDE.md).
7
7
  */
8
8
  export interface AccountMenuPerson {
9
- /** Their name, e.g. "Sofia Marques". */
10
- name: string;
9
+ /**
10
+ * Their name, e.g. "Sofia Marques". Optional: Identity may hold none. Without
11
+ * it the name line reads "You" (`labels.unnamed`) and the initials come from
12
+ * the email, as the Hub's do.
13
+ */
14
+ name?: string;
11
15
  /** Their email, on the line beneath. */
12
16
  email: string;
13
17
  /**
14
- * An avatar image. Omit it -- the drawing does -- and the circle draws bare, in
15
- * `--color-border-strong`.
18
+ * An avatar image, on the trigger and in the person block. Omit it and both
19
+ * circles carry the person's initials on `--color-neutral-300`, as the Hub's do.
16
20
  *
17
21
  * The image is decorative when it renders: it takes an empty `alt`, because the
18
22
  * person's name is on the line beside it and a name here would make a screen
@@ -27,8 +31,11 @@ export interface AccountMenuPerson {
27
31
  export interface AccountMenuOrg {
28
32
  /** The organisation's name, e.g. "Acme Coaching". */
29
33
  name: string;
30
- /** The initials on the tile beside it, e.g. "AC". Decorative -- the name is next to it. */
31
- initials: string;
34
+ /**
35
+ * The initials on the tile beside it, e.g. "AC". Decorative -- the name is next
36
+ * to it. Omit them and they are taken from the name.
37
+ */
38
+ initials?: string;
32
39
  /**
33
40
  * The role pill at the right of the strip, e.g. "Admin". Omit it and no pill is
34
41
  * drawn -- it is Identity's word for what this person is in this organisation,
@@ -81,8 +88,13 @@ export interface AccountMenuGroup {
81
88
  export interface AccountMenuProps {
82
89
  /** The signed-in person. */
83
90
  person: AccountMenuPerson;
84
- /** The organisation this person is acting in. */
85
- org: AccountMenuOrg;
91
+ /**
92
+ * The organisation this person is acting in. Omit it and no strip is drawn: a
93
+ * person on no membership would otherwise get an empty dark tile where the
94
+ * company goes, which reads as a company whose name failed to load (the Hub's
95
+ * reasoning).
96
+ */
97
+ org?: AccountMenuOrg;
86
98
  /** The row groups, in order. A hairline is drawn above each one. */
87
99
  groups: AccountMenuGroup[];
88
100
  /**
@@ -92,6 +104,11 @@ export interface AccountMenuProps {
92
104
  * looks like), keeps its default.
93
105
  */
94
106
  labels?: Partial<AccountMenuLabels>;
107
+ /**
108
+ * Draw the menu open on first render, without moving focus. For the specimen
109
+ * and for screenshots; an app leaves it unset.
110
+ */
111
+ defaultOpen?: boolean;
95
112
  /**
96
113
  * The element a row with an `href` renders through. Defaults to a plain `a`; a
97
114
  * router-owning consumer passes its own so the rows route client-side with the
@@ -101,31 +118,24 @@ export interface AccountMenuProps {
101
118
  linkComponent?: ElementType;
102
119
  }
103
120
  /**
104
- * The account menu: who you are, which organisation you are in, and the way into
105
- * Settings and out of the session (B33-493).
121
+ * The account menu: the avatar button in the top bar's end group, and the menu it
122
+ * opens -- who you are, which organisation you are in, the way into Profile,
123
+ * Security and Settings, Feedback and Docs, and the way out (B33-493).
106
124
  *
107
- * **This is Stage A, and this component is the panel alone.** It takes no
108
- * trigger, holds no open state and wires nothing. That is not a half-built
109
- * version of trigger-plus-panel -- it is the part of this component that the two
110
- * reference drawings agree about. Paper's 004 "AccountMenu — popover" and
111
- * Identity's 103 · 11 Account menu are identical in every property of the panel,
112
- * and they disagree about the anchor (004 draws a bell on a tile, an unread dot
113
- * and a halo behind the avatar; 103 draws none of the three) and about where the
114
- * panel sits relative to it (004: 8px below, 16px right inset, overhanging the
115
- * avatar by 4px; 103: 10px below, 20px inset, flush). CLAUDE.md puts that choice
116
- * with the person at the fidelity gate, and says plainly that an agent picking
117
- * one has invented a referent. So the anchor is not here, `TopBar` is untouched,
118
- * and the specimen positions the panel in its own stylesheet.
125
+ * **Stage B, and the referent is the Hub's working menu** (boost33-hub
126
+ * src/app/account-menu.tsx), named by Jamal on 5 Nov 2026 so that every app's
127
+ * avatar opens the same menu. The panel is Stage A's, which was drawn from the
128
+ * same Paper artboard the Hub built from (103 · 11); the trigger and the placement
129
+ * are the Hub's: a 26px avatar, the panel 10px below it and aligned to its right
130
+ * edge.
119
131
  *
120
- * A disclosure, like the switcher (ADR-0010): no widget role on the panel, no
121
- * role on the rows, every row the real `<a>` or `<button>` it already is, and Tab
122
- * the document's own order. Dismissal, focus restoration and the trigger's
123
- * `aria-expanded` are the shared anchored-overlay contract's (ADR-0009) and
124
- * arrive with the trigger in Stage B.
132
+ * A menu button, not a disclosure: `role="menu"` on the panel and
133
+ * `role="menuitem"` on every row, with the Hub's keyboard model (./use-menu.ts).
134
+ * Choosing a row closes the menu, so a client-side navigation does not leave it
135
+ * open over the page it just opened.
125
136
  *
126
- * Nothing here is authoritative. Every measurement in the stylesheet is read off
127
- * a drawing that no human has approved, and the gate is where it becomes a
128
- * referent -- at which point the approved baseline and the measurements written
129
- * into the spec are what binds, not Paper.
137
+ * Most apps want `StandardAccountMenu`, which builds the standard rows from an
138
+ * identity URL, a Hub URL and the person's role. This is the lower level: any
139
+ * rows, as groups.
130
140
  */
131
- export declare function AccountMenu({ person, org, groups, labels, linkComponent }: AccountMenuProps): import("react").JSX.Element;
141
+ export declare function AccountMenu({ person, org, groups, labels, defaultOpen, linkComponent }: AccountMenuProps): import("react").JSX.Element;
@@ -1,66 +1,84 @@
1
- import { jsxs as s, jsx as e, Fragment as v } from "react/jsx-runtime";
2
- import { Fragment as u } from "react";
3
- import { ACCOUNT_MENU_ICONS as f } from "./icons.js";
4
- import { resolveAccountMenuLabels as g } from "./labels.js";
5
- import a from "./AccountMenu.module.css.js";
6
- function L({ person: i, org: l, groups: o, labels: d, linkComponent: t }) {
7
- const m = g(d), p = t ?? "a";
8
- function h(n) {
9
- const r = n.description === void 0, N = n.icon === void 0 ? null : f[n.icon](a.icon), c = /* @__PURE__ */ s(v, { children: [
10
- /* @__PURE__ */ e("span", { className: a.lane, "aria-hidden": "true", children: N }),
11
- /* @__PURE__ */ s("span", { className: a.rowText, children: [
12
- /* @__PURE__ */ e("span", { className: a.rowLabel, children: n.label }),
13
- n.description !== void 0 && /* @__PURE__ */ e("span", { className: a.rowDescription, children: n.description })
1
+ "use client";
2
+ import { jsxs as r, jsx as a, Fragment as b } from "react/jsx-runtime";
3
+ import { Fragment as w } from "react";
4
+ import { ACCOUNT_MENU_ICONS as x } from "./icons.js";
5
+ import { initials as m } from "./initials.js";
6
+ import { resolveAccountMenuLabels as U } from "./labels.js";
7
+ import { useMenu as y } from "./use-menu.js";
8
+ import e from "./AccountMenu.module.css.js";
9
+ function P({ person: i, org: l, groups: p, labels: h, defaultOpen: u, linkComponent: N }) {
10
+ const o = U(h), v = N ?? "a", s = y({ defaultOpen: u }), c = m(i.name ?? i.email);
11
+ function g(n) {
12
+ const t = n.description === void 0, f = n.icon === void 0 ? null : x[n.icon](e.icon), d = /* @__PURE__ */ r(b, { children: [
13
+ /* @__PURE__ */ a("span", { className: e.lane, "aria-hidden": "true", children: f }),
14
+ /* @__PURE__ */ r("span", { className: e.rowText, children: [
15
+ /* @__PURE__ */ a("span", { className: e.rowLabel, children: n.label }),
16
+ n.description !== void 0 && /* @__PURE__ */ a("span", { className: e.rowDescription, children: n.description })
14
17
  ] })
15
18
  ] });
16
- return n.href === void 0 ? /* @__PURE__ */ e(
19
+ return n.href === void 0 ? /* @__PURE__ */ a(
17
20
  "button",
18
21
  {
19
22
  type: "button",
20
- className: a.row,
23
+ role: "menuitem",
24
+ tabIndex: -1,
25
+ className: e.row,
21
26
  "data-item": n.id,
22
- "data-single-line": r ? "true" : void 0,
23
- children: c
27
+ "data-single-line": t ? "true" : void 0,
28
+ children: d
24
29
  }
25
- ) : /* @__PURE__ */ e(
26
- p,
30
+ ) : /* @__PURE__ */ a(
31
+ v,
27
32
  {
28
- className: a.row,
33
+ role: "menuitem",
34
+ tabIndex: -1,
35
+ className: e.row,
29
36
  href: n.href,
30
37
  "data-item": n.id,
31
- "data-single-line": r ? "true" : void 0,
32
- children: c
38
+ "data-single-line": t ? "true" : void 0,
39
+ children: d
33
40
  }
34
41
  );
35
42
  }
36
- return /* @__PURE__ */ s("div", { className: a.panel, "aria-label": m.panel, children: [
37
- /* @__PURE__ */ s("div", { className: a.person, children: [
38
- i.avatarUrl === void 0 ? /* @__PURE__ */ e("span", { className: a.personAvatar, "aria-hidden": "true" }) : (
39
- // Empty alt, not a name: the person's name is on the next line in. The
40
- // guard in tests/no-hardcoded-strings.test.ts reads `alt`, which is
41
- // the right thing to read -- an empty one is an absence of copy, not
42
- // untranslated copy.
43
- /* @__PURE__ */ e("img", { className: a.personAvatar, src: i.avatarUrl, alt: "" })
44
- ),
45
- /* @__PURE__ */ s("span", { className: a.personText, children: [
46
- /* @__PURE__ */ e("span", { className: a.personName, children: i.name }),
47
- /* @__PURE__ */ e("span", { className: a.personEmail, children: i.email })
48
- ] })
49
- ] }),
50
- /* @__PURE__ */ s("div", { className: a.org, children: [
51
- /* @__PURE__ */ s("span", { className: a.orgIdentity, children: [
52
- /* @__PURE__ */ e("span", { className: a.orgTile, "aria-hidden": "true", children: l.initials }),
53
- /* @__PURE__ */ e("span", { className: a.orgName, children: l.name })
54
- ] }),
55
- l.role !== void 0 && /* @__PURE__ */ e("span", { className: a.orgRole, children: l.role })
56
- ] }),
57
- o.map((n) => /* @__PURE__ */ s(u, { children: [
58
- /* @__PURE__ */ e("hr", { className: a.divider }),
59
- /* @__PURE__ */ e("ul", { className: a.rows, children: n.items.map((r) => /* @__PURE__ */ e("li", { children: h(r) }, r.id)) })
60
- ] }, n.id))
43
+ return /* @__PURE__ */ r("div", { className: e.root, ...s.rootProps, children: [
44
+ /* @__PURE__ */ a("button", { ...s.triggerProps, type: "button", className: e.trigger, "aria-label": o.trigger, children: i.avatarUrl === void 0 ? /* @__PURE__ */ a("span", { "aria-hidden": "true", children: c }) : /* @__PURE__ */ a("img", { className: e.avatarImage, src: i.avatarUrl, alt: "" }) }),
45
+ s.open && /* @__PURE__ */ r(
46
+ "div",
47
+ {
48
+ ...s.menuProps,
49
+ className: e.panel,
50
+ "aria-label": o.panel,
51
+ onClick: (n) => {
52
+ n.target.closest("[role='menuitem']") && s.close(!1);
53
+ },
54
+ children: [
55
+ /* @__PURE__ */ r("div", { className: e.person, children: [
56
+ i.avatarUrl === void 0 ? /* @__PURE__ */ a("span", { className: e.personAvatar, "aria-hidden": "true", children: c }) : (
57
+ // Empty alt, not a name: the person's name is on the next line in.
58
+ /* @__PURE__ */ a("img", { className: e.personAvatar, src: i.avatarUrl, alt: "" })
59
+ ),
60
+ /* @__PURE__ */ r("span", { className: e.personText, children: [
61
+ /* @__PURE__ */ a("span", { className: e.personName, children: i.name ?? o.unnamed }),
62
+ /* @__PURE__ */ a("span", { className: e.personEmail, children: i.email })
63
+ ] })
64
+ ] }),
65
+ l !== void 0 && /* @__PURE__ */ r("div", { className: e.org, children: [
66
+ /* @__PURE__ */ r("span", { className: e.orgIdentity, children: [
67
+ /* @__PURE__ */ a("span", { className: e.orgTile, "aria-hidden": "true", children: l.initials ?? m(l.name) }),
68
+ /* @__PURE__ */ a("span", { className: e.orgName, children: l.name })
69
+ ] }),
70
+ l.role !== void 0 && /* @__PURE__ */ a("span", { className: e.orgRole, children: l.role })
71
+ ] }),
72
+ p.map((n) => /* @__PURE__ */ r(w, { children: [
73
+ /* @__PURE__ */ a("hr", { className: e.divider }),
74
+ /* @__PURE__ */ a("ul", { className: e.rows, role: "none", children: n.items.map((t) => /* @__PURE__ */ a("li", { role: "none", children: g(t) }, t.id)) })
75
+ ] }, n.id))
76
+ ]
77
+ }
78
+ )
61
79
  ] });
62
80
  }
63
81
  export {
64
- L as AccountMenu
82
+ P as AccountMenu
65
83
  };
66
84
  //# sourceMappingURL=AccountMenu.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AccountMenu.js","sources":["../../../src/components/AccountMenu/AccountMenu.tsx"],"sourcesContent":["// No \"use client\", and that is Stage A's whole shape rather than an oversight.\n// This renders the panel and nothing else: no open/closed state, no handlers, no\n// useAnchoredOverlay, no keyboard handling. So it stays a Server Component, a\n// consumer pays no client bundle for it, and tests/guards.test.ts -- which pins\n// the built package's \"use client\" modules to exactly three -- stays green\n// without being edited. Stage B is where that changes, deliberately and with the\n// guard's expectation updated in the same change.\n\nimport { Fragment } from 'react'\nimport type { ElementType, ReactElement } from 'react'\nimport { ACCOUNT_MENU_ICONS, type AccountMenuIcon } from './icons'\nimport { resolveAccountMenuLabels, type AccountMenuLabels } from './labels'\nimport styles from './AccountMenu.module.css'\n\n/**\n * The signed-in person, at the top of the panel. Supplied by the consumer -- the\n * menu reads no session and fetches nothing (CLAUDE.md).\n */\nexport interface AccountMenuPerson {\n /** Their name, e.g. \"Sofia Marques\". */\n name: string\n /** Their email, on the line beneath. */\n email: string\n /**\n * An avatar image. Omit it -- the drawing does -- and the circle draws bare, in\n * `--color-border-strong`.\n *\n * The image is decorative when it renders: it takes an empty `alt`, because the\n * person's name is on the line beside it and a name here would make a screen\n * reader say who you are twice. Same reasoning as `ProductMark`'s default.\n */\n avatarUrl?: string\n}\n\n/**\n * The organisation strip under the person: which account you are acting in.\n * Identity's data, supplied per render, never looked up here (ADR-0003).\n */\nexport interface AccountMenuOrg {\n /** The organisation's name, e.g. \"Acme Coaching\". */\n name: string\n /** The initials on the tile beside it, e.g. \"AC\". Decorative -- the name is next to it. */\n initials: string\n /**\n * The role pill at the right of the strip, e.g. \"Admin\". Omit it and no pill is\n * drawn -- it is Identity's word for what this person is in this organisation,\n * so a consumer with nothing to say there says nothing rather than guessing.\n */\n role?: string\n}\n\n/**\n * One row in the panel.\n *\n * Whether it navigates is decided by `href` and nothing else, which is ADR-0011's\n * rule applied rather than re-decided: a row with an `href` is a link and renders\n * through `linkComponent`; a row without one is an action and stays a `<button>`.\n * That keeps the component out of the business of knowing which of Profile,\n * Settings, Switch account and Sign out are navigations in a given product --\n * four apps may answer differently, and the consumer already knows.\n */\nexport interface AccountMenuItem {\n /** Stable identity: the React key, and what a test addresses the row by. */\n id: string\n /** The row's name, e.g. \"Profile\". */\n label: string\n /**\n * The line beneath the name. Omit it and the row is a single line, set in the\n * quieter register the drawing gives its Sign out row -- see `.rowLabel` in the\n * stylesheet for why the two travel together.\n */\n description?: string\n /** Where the row goes. Omit it and the row is an action, rendered as a button. */\n href?: string\n /** Which glyph the row carries. Omit it and the glyph lane stays, empty. */\n icon?: AccountMenuIcon\n}\n\n/**\n * A run of rows drawn together. The panel's hairline is drawn above every group,\n * which is what separates the first group from the identity blocks above it as\n * well as each group from the last.\n *\n * Grouped rather than flat because the drawing puts a rule between its three\n * navigations and its one way out, and grouped rather than named (`items` plus a\n * `footer`) because \"footer\" would be this package inventing a meaning for the\n * second group. A consumer that wants one group passes one.\n */\nexport interface AccountMenuGroup {\n /** Stable identity: the React key. */\n id: string\n /** The rows, in the order they are drawn. */\n items: AccountMenuItem[]\n}\n\nexport interface AccountMenuProps {\n /** The signed-in person. */\n person: AccountMenuPerson\n /** The organisation this person is acting in. */\n org: AccountMenuOrg\n /** The row groups, in order. A hairline is drawn above each one. */\n groups: AccountMenuGroup[]\n /**\n * Every user-facing string that is not row data -- which is the panel's\n * accessible name, and today nothing else. Each field defaults to English, and\n * a field left out, or supplied as `undefined` (what a missed next-intl key\n * looks like), keeps its default.\n */\n labels?: Partial<AccountMenuLabels>\n /**\n * The element a row with an `href` renders through. Defaults to a plain `a`; a\n * router-owning consumer passes its own so the rows route client-side with the\n * locale prefix intact (ADR-0011, B33-847). Rows without an `href` are buttons\n * and ignore it.\n */\n linkComponent?: ElementType\n}\n\n/**\n * The account menu: who you are, which organisation you are in, and the way into\n * Settings and out of the session (B33-493).\n *\n * **This is Stage A, and this component is the panel alone.** It takes no\n * trigger, holds no open state and wires nothing. That is not a half-built\n * version of trigger-plus-panel -- it is the part of this component that the two\n * reference drawings agree about. Paper's 004 \"AccountMenu — popover\" and\n * Identity's 103 · 11 Account menu are identical in every property of the panel,\n * and they disagree about the anchor (004 draws a bell on a tile, an unread dot\n * and a halo behind the avatar; 103 draws none of the three) and about where the\n * panel sits relative to it (004: 8px below, 16px right inset, overhanging the\n * avatar by 4px; 103: 10px below, 20px inset, flush). CLAUDE.md puts that choice\n * with the person at the fidelity gate, and says plainly that an agent picking\n * one has invented a referent. So the anchor is not here, `TopBar` is untouched,\n * and the specimen positions the panel in its own stylesheet.\n *\n * A disclosure, like the switcher (ADR-0010): no widget role on the panel, no\n * role on the rows, every row the real `<a>` or `<button>` it already is, and Tab\n * the document's own order. Dismissal, focus restoration and the trigger's\n * `aria-expanded` are the shared anchored-overlay contract's (ADR-0009) and\n * arrive with the trigger in Stage B.\n *\n * Nothing here is authoritative. Every measurement in the stylesheet is read off\n * a drawing that no human has approved, and the gate is where it becomes a\n * referent -- at which point the approved baseline and the measurements written\n * into the spec are what binds, not Paper.\n */\nexport function AccountMenu({ person, org, groups, labels, linkComponent }: AccountMenuProps) {\n const t = resolveAccountMenuLabels(labels)\n const Link: ElementType = linkComponent ?? 'a'\n\n function renderRow(item: AccountMenuItem): ReactElement {\n const singleLine = item.description === undefined\n const glyph = item.icon === undefined ? null : ACCOUNT_MENU_ICONS[item.icon](styles.icon)\n\n // The lane is drawn whether or not the row has a glyph. A row that drops its\n // icon and lets the text slide left would break the column the four names\n // stand in, which is the one alignment a panel of stacked rows has to hold.\n // Same reasoning as NavItem's trailing slot (CONTEXT.md, \"Trailing slot\").\n const content = (\n <>\n <span className={styles.lane} aria-hidden=\"true\">\n {glyph}\n </span>\n <span className={styles.rowText}>\n <span className={styles.rowLabel}>{item.label}</span>\n {item.description !== undefined && (\n <span className={styles.rowDescription}>{item.description}</span>\n )}\n </span>\n </>\n )\n\n // No onClick on either branch: Stage A renders the element and wires nothing.\n // The action row is a real <button type=\"button\"> so that the shape Stage B\n // attaches a handler to is the shape a human approved here.\n return item.href === undefined ? (\n <button\n type=\"button\"\n className={styles.row}\n data-item={item.id}\n data-single-line={singleLine ? 'true' : undefined}\n >\n {content}\n </button>\n ) : (\n <Link\n className={styles.row}\n href={item.href}\n data-item={item.id}\n data-single-line={singleLine ? 'true' : undefined}\n >\n {content}\n </Link>\n )\n }\n\n return (\n <div className={styles.panel} aria-label={t.panel}>\n <div className={styles.person}>\n {person.avatarUrl === undefined ? (\n <span className={styles.personAvatar} aria-hidden=\"true\" />\n ) : (\n // Empty alt, not a name: the person's name is on the next line in. The\n // guard in tests/no-hardcoded-strings.test.ts reads `alt`, which is\n // the right thing to read -- an empty one is an absence of copy, not\n // untranslated copy.\n <img className={styles.personAvatar} src={person.avatarUrl} alt=\"\" />\n )}\n <span className={styles.personText}>\n <span className={styles.personName}>{person.name}</span>\n <span className={styles.personEmail}>{person.email}</span>\n </span>\n </div>\n\n <div className={styles.org}>\n <span className={styles.orgIdentity}>\n <span className={styles.orgTile} aria-hidden=\"true\">\n {org.initials}\n </span>\n <span className={styles.orgName}>{org.name}</span>\n </span>\n {org.role !== undefined && <span className={styles.orgRole}>{org.role}</span>}\n </div>\n\n {/* A Fragment rather than a wrapper element, so the hairline and the rows\n are direct children of the panel's flex column exactly as the drawing\n has them. A wrapper would be a block in between, and the hairline's\n block margins -- which are measured, see `.divider` -- would collapse\n through its edges and move the rule. */}\n {groups.map((group) => (\n <Fragment key={group.id}>\n <hr className={styles.divider} />\n <ul className={styles.rows}>\n {group.items.map((item) => (\n <li key={item.id}>{renderRow(item)}</li>\n ))}\n </ul>\n </Fragment>\n ))}\n </div>\n )\n}\n"],"names":["AccountMenu","person","org","groups","labels","linkComponent","t","resolveAccountMenuLabels","Link","renderRow","item","singleLine","glyph","ACCOUNT_MENU_ICONS","styles","content","jsxs","Fragment","jsx","group"],"mappings":"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"}
1
+ {"version":3,"file":"AccountMenu.js","sources":["../../../src/components/AccountMenu/AccountMenu.tsx"],"sourcesContent":["'use client'\n\n// A client component since Stage B (B33-493): the avatar button holds the menu's\n// open state and its keyboard behaviour (./use-menu.ts). Everything it takes is\n// plain data -- person, org, groups of rows with hrefs, labels -- so a Server\n// Component layout renders it directly and passes no function across the\n// boundary. `linkComponent` is the one exception and it is optional: a client\n// consumer may pass its router's link; a Server Component never needs to.\n// tests/guards.test.ts pins the built package's \"use client\" modules, and this\n// one was added to that list on purpose.\n\nimport { Fragment } from 'react'\nimport type { ElementType, ReactElement } from 'react'\nimport { ACCOUNT_MENU_ICONS, type AccountMenuIcon } from './icons'\nimport { initials } from './initials'\nimport { resolveAccountMenuLabels, type AccountMenuLabels } from './labels'\nimport { useMenu } from './use-menu'\nimport styles from './AccountMenu.module.css'\n\n/**\n * The signed-in person, at the top of the panel. Supplied by the consumer -- the\n * menu reads no session and fetches nothing (CLAUDE.md).\n */\nexport interface AccountMenuPerson {\n /**\n * Their name, e.g. \"Sofia Marques\". Optional: Identity may hold none. Without\n * it the name line reads \"You\" (`labels.unnamed`) and the initials come from\n * the email, as the Hub's do.\n */\n name?: string\n /** Their email, on the line beneath. */\n email: string\n /**\n * An avatar image, on the trigger and in the person block. Omit it and both\n * circles carry the person's initials on `--color-neutral-300`, as the Hub's do.\n *\n * The image is decorative when it renders: it takes an empty `alt`, because the\n * person's name is on the line beside it and a name here would make a screen\n * reader say who you are twice. Same reasoning as `ProductMark`'s default.\n */\n avatarUrl?: string\n}\n\n/**\n * The organisation strip under the person: which account you are acting in.\n * Identity's data, supplied per render, never looked up here (ADR-0003).\n */\nexport interface AccountMenuOrg {\n /** The organisation's name, e.g. \"Acme Coaching\". */\n name: string\n /**\n * The initials on the tile beside it, e.g. \"AC\". Decorative -- the name is next\n * to it. Omit them and they are taken from the name.\n */\n initials?: string\n /**\n * The role pill at the right of the strip, e.g. \"Admin\". Omit it and no pill is\n * drawn -- it is Identity's word for what this person is in this organisation,\n * so a consumer with nothing to say there says nothing rather than guessing.\n */\n role?: string\n}\n\n/**\n * One row in the panel.\n *\n * Whether it navigates is decided by `href` and nothing else, which is ADR-0011's\n * rule applied rather than re-decided: a row with an `href` is a link and renders\n * through `linkComponent`; a row without one is an action and stays a `<button>`.\n * That keeps the component out of the business of knowing which of Profile,\n * Settings, Switch account and Sign out are navigations in a given product --\n * four apps may answer differently, and the consumer already knows.\n */\nexport interface AccountMenuItem {\n /** Stable identity: the React key, and what a test addresses the row by. */\n id: string\n /** The row's name, e.g. \"Profile\". */\n label: string\n /**\n * The line beneath the name. Omit it and the row is a single line, set in the\n * quieter register the drawing gives its Sign out row -- see `.rowLabel` in the\n * stylesheet for why the two travel together.\n */\n description?: string\n /** Where the row goes. Omit it and the row is an action, rendered as a button. */\n href?: string\n /** Which glyph the row carries. Omit it and the glyph lane stays, empty. */\n icon?: AccountMenuIcon\n}\n\n/**\n * A run of rows drawn together. The panel's hairline is drawn above every group,\n * which is what separates the first group from the identity blocks above it as\n * well as each group from the last.\n *\n * Grouped rather than flat because the drawing puts a rule between its three\n * navigations and its one way out, and grouped rather than named (`items` plus a\n * `footer`) because \"footer\" would be this package inventing a meaning for the\n * second group. A consumer that wants one group passes one.\n */\nexport interface AccountMenuGroup {\n /** Stable identity: the React key. */\n id: string\n /** The rows, in the order they are drawn. */\n items: AccountMenuItem[]\n}\n\nexport interface AccountMenuProps {\n /** The signed-in person. */\n person: AccountMenuPerson\n /**\n * The organisation this person is acting in. Omit it and no strip is drawn: a\n * person on no membership would otherwise get an empty dark tile where the\n * company goes, which reads as a company whose name failed to load (the Hub's\n * reasoning).\n */\n org?: AccountMenuOrg\n /** The row groups, in order. A hairline is drawn above each one. */\n groups: AccountMenuGroup[]\n /**\n * Every user-facing string that is not row data -- which is the panel's\n * accessible name, and today nothing else. Each field defaults to English, and\n * a field left out, or supplied as `undefined` (what a missed next-intl key\n * looks like), keeps its default.\n */\n labels?: Partial<AccountMenuLabels>\n /**\n * Draw the menu open on first render, without moving focus. For the specimen\n * and for screenshots; an app leaves it unset.\n */\n defaultOpen?: boolean\n /**\n * The element a row with an `href` renders through. Defaults to a plain `a`; a\n * router-owning consumer passes its own so the rows route client-side with the\n * locale prefix intact (ADR-0011, B33-847). Rows without an `href` are buttons\n * and ignore it.\n */\n linkComponent?: ElementType\n}\n\n/**\n * The account menu: the avatar button in the top bar's end group, and the menu it\n * opens -- who you are, which organisation you are in, the way into Profile,\n * Security and Settings, Feedback and Docs, and the way out (B33-493).\n *\n * **Stage B, and the referent is the Hub's working menu** (boost33-hub\n * src/app/account-menu.tsx), named by Jamal on 5 Nov 2026 so that every app's\n * avatar opens the same menu. The panel is Stage A's, which was drawn from the\n * same Paper artboard the Hub built from (103 · 11); the trigger and the placement\n * are the Hub's: a 26px avatar, the panel 10px below it and aligned to its right\n * edge.\n *\n * A menu button, not a disclosure: `role=\"menu\"` on the panel and\n * `role=\"menuitem\"` on every row, with the Hub's keyboard model (./use-menu.ts).\n * Choosing a row closes the menu, so a client-side navigation does not leave it\n * open over the page it just opened.\n *\n * Most apps want `StandardAccountMenu`, which builds the standard rows from an\n * identity URL, a Hub URL and the person's role. This is the lower level: any\n * rows, as groups.\n */\nexport function AccountMenu({ person, org, groups, labels, defaultOpen, linkComponent }: AccountMenuProps) {\n const t = resolveAccountMenuLabels(labels)\n const Link: ElementType = linkComponent ?? 'a'\n const menu = useMenu({ defaultOpen })\n const letters = initials(person.name ?? person.email)\n\n function renderRow(item: AccountMenuItem): ReactElement {\n const singleLine = item.description === undefined\n const glyph = item.icon === undefined ? null : ACCOUNT_MENU_ICONS[item.icon](styles.icon)\n\n // The lane is drawn whether or not the row has a glyph. A row that drops its\n // icon and lets the text slide left would break the column the names stand\n // in, which is the one alignment a panel of stacked rows has to hold. Same\n // reasoning as NavItem's trailing slot (CONTEXT.md, \"Trailing slot\").\n const content = (\n <>\n <span className={styles.lane} aria-hidden=\"true\">\n {glyph}\n </span>\n <span className={styles.rowText}>\n <span className={styles.rowLabel}>{item.label}</span>\n {item.description !== undefined && (\n <span className={styles.rowDescription}>{item.description}</span>\n )}\n </span>\n </>\n )\n\n // tabIndex -1: the arrow keys move through the rows and Tab leaves the menu\n // (./use-menu.ts). An action row is a real <button>; it carries no handler,\n // because a handler is a function and this component takes none from a\n // Server Component. A consumer with an action to wire renders its own row.\n return item.href === undefined ? (\n <button\n type=\"button\"\n role=\"menuitem\"\n tabIndex={-1}\n className={styles.row}\n data-item={item.id}\n data-single-line={singleLine ? 'true' : undefined}\n >\n {content}\n </button>\n ) : (\n <Link\n role=\"menuitem\"\n tabIndex={-1}\n className={styles.row}\n href={item.href}\n data-item={item.id}\n data-single-line={singleLine ? 'true' : undefined}\n >\n {content}\n </Link>\n )\n }\n\n return (\n <div className={styles.root} {...menu.rootProps}>\n <button {...menu.triggerProps} type=\"button\" className={styles.trigger} aria-label={t.trigger}>\n {person.avatarUrl === undefined ? (\n <span aria-hidden=\"true\">{letters}</span>\n ) : (\n <img className={styles.avatarImage} src={person.avatarUrl} alt=\"\" />\n )}\n </button>\n\n {menu.open && (\n <div\n {...menu.menuProps}\n className={styles.panel}\n aria-label={t.panel}\n // Choosing a row closes the menu. A client-side navigation keeps this\n // component mounted, and the menu would otherwise stay open over the\n // page the row just opened (the Hub's reasoning).\n onClick={(event) => {\n if ((event.target as HTMLElement).closest(\"[role='menuitem']\")) menu.close(false)\n }}\n >\n <div className={styles.person}>\n {person.avatarUrl === undefined ? (\n <span className={styles.personAvatar} aria-hidden=\"true\">\n {letters}\n </span>\n ) : (\n // Empty alt, not a name: the person's name is on the next line in.\n <img className={styles.personAvatar} src={person.avatarUrl} alt=\"\" />\n )}\n <span className={styles.personText}>\n <span className={styles.personName}>{person.name ?? t.unnamed}</span>\n <span className={styles.personEmail}>{person.email}</span>\n </span>\n </div>\n\n {org !== undefined && (\n <div className={styles.org}>\n <span className={styles.orgIdentity}>\n <span className={styles.orgTile} aria-hidden=\"true\">\n {org.initials ?? initials(org.name)}\n </span>\n <span className={styles.orgName}>{org.name}</span>\n </span>\n {org.role !== undefined && <span className={styles.orgRole}>{org.role}</span>}\n </div>\n )}\n\n {/* A Fragment rather than a wrapper element, so the hairline and the\n rows are direct children of the panel's flex column. The lists are\n role=\"none\": inside a menu the rows are menuitems, and list\n semantics between the two would break that ownership. */}\n {groups.map((group) => (\n <Fragment key={group.id}>\n <hr className={styles.divider} />\n <ul className={styles.rows} role=\"none\">\n {group.items.map((item) => (\n <li key={item.id} role=\"none\">\n {renderRow(item)}\n </li>\n ))}\n </ul>\n </Fragment>\n ))}\n </div>\n )}\n </div>\n )\n}\n"],"names":["AccountMenu","person","org","groups","labels","defaultOpen","linkComponent","t","resolveAccountMenuLabels","Link","menu","useMenu","letters","initials","renderRow","item","singleLine","glyph","ACCOUNT_MENU_ICONS","styles","content","jsxs","Fragment","jsx","event","group"],"mappings":"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"}