@ghub/identity-chrome 0.1.0 → 0.3.0

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.
@@ -0,0 +1,1248 @@
1
+ "use strict";
2
+ /**
3
+ * `<gp-account>`, the account popup (GPlatform Identity, sheet 32, option A).
4
+ *
5
+ * An avatar at the right end of a product's top bar and, under it, a card on a
6
+ * tinted ground: who is signed in, where their Identity account is, which
7
+ * organisation they are acting in, and how to sign out of this product and
8
+ * only this product (D21).
9
+ *
10
+ * **Most of what it shows is the product's.** The name, the address, the
11
+ * organisations and both actions are properties the product sets from its own
12
+ * session. Identity adds the picture, the tint, its own two links and the
13
+ * footer, through the same core as `<gp-apps>`, so the popup still signs
14
+ * somebody out while Identity cannot be reached.
15
+ *
16
+ * **What it is to assistive technology:**
17
+ *
18
+ * - The trigger is a `button` with `aria-haspopup="dialog"`, named "Account:
19
+ * Mira Kessler", with the same words as a tooltip on hover and on focus after
20
+ * half a second, which Escape dismisses without closing anything else (1.4.13).
21
+ * - The popup is a `dialog` labelled by the person's name and described by their
22
+ * address, not modal. Below 600 wide it is a modal sheet with a scrim and a
23
+ * contained focus loop, as the picker is.
24
+ * - Focus opens on the pill (Manage your Identity account, or Set up your
25
+ * Identity), else the active organisation, else Sign out. The close button is
26
+ * a stop and never the first one.
27
+ * - The organisations are a `radiogroup`, one tab stop, Up and Down moving
28
+ * between them. Choosing is Space or Enter and never an arrow, because
29
+ * choosing switches the product: it closes the popup and announces
30
+ * "Switched to ...". Five show; "Show all" opens the rest in place, scrolling
31
+ * inside, and focus lands on the row that took its place.
32
+ * - Signing out goes busy in place. A failure keeps the popup open, says in one
33
+ * sentence that the person is still signed in, and turns the row into Try
34
+ * again. It never claims to have signed out.
35
+ *
36
+ * **In a sidebar** (`layout="row"`, `placement="top"`): the trigger is a full
37
+ * width row, the picture with the name over the address, and the popup opens
38
+ * upward from it. A product with a sidebar puts `<gp-apps>` at the row's end.
39
+ *
40
+ * **The product's own items** (`items`) sit after the organisations, as a group
41
+ * headed with the product's name. Until the product signs in through Identity,
42
+ * Sign out is the last row of that same group, because it is one of the
43
+ * product's own actions; once it does (`viaIdentity`), signing out ends both
44
+ * sessions and stands on its own again.
45
+ *
46
+ * **Hidden for now (D20):** the other accounts and "Add another account" are
47
+ * not drawn until Identity holds several sessions in one browser and products
48
+ * sign in through it, and no product can switch them on.
49
+ *
50
+ * The words are English by default and every one can be replaced (`labels`).
51
+ */
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.ACCOUNT_CSS = exports.ACCOUNT_LABELS = void 0;
54
+ exports.iconOf = iconOf;
55
+ exports.defineAccountElement = defineAccountElement;
56
+ exports.createAccountElementClass = createAccountElementClass;
57
+ const tint_js_1 = require("../tint.js");
58
+ const validate_js_1 = require("../validate.js");
59
+ const contract_js_1 = require("./contract.js");
60
+ const dom_js_1 = require("./dom.js");
61
+ const sentence = (words) => `${words.charAt(0).toUpperCase()}${words.slice(1)}`;
62
+ exports.ACCOUNT_LABELS = {
63
+ trigger: (name) => `Account: ${name}`,
64
+ signedInTo: (product) => `Signed in to ${product}`,
65
+ close: "Close",
66
+ changePicture: "Change your picture",
67
+ manage: "Manage your Identity account",
68
+ setUp: "Set up your Identity",
69
+ organisations: "Organisations",
70
+ active: "Active",
71
+ showAll: (count) => `Show all ${String(count)}`,
72
+ switched: (organisation) => `Switched to ${organisation}`,
73
+ switchFailed: (organisation) => `Could not switch to ${organisation}. Try again.`,
74
+ signOut: (product) => `Sign out of ${product}`,
75
+ signOutBoth: (product) => `Sign out of ${product} and Identity`,
76
+ signingOut: (product) => `Signing out of ${product}`,
77
+ signOutFailed: (product) => `You are still signed in. ${sentence(product)} did not answer. Try again, or close the browser to end this session.`,
78
+ retry: "Try again",
79
+ thisProduct: "this product",
80
+ terms: "Terms of Service",
81
+ privacy: "Privacy policy",
82
+ };
83
+ /** A view asked again on opening when it is older than this. */
84
+ const FRESH_MS = 60_000;
85
+ const SHEET_QUERY = "(max-width: 599px)";
86
+ /** The organisations shown before "Show all". */
87
+ const SHOWN = 5;
88
+ const TIP_DELAY_MS = 500;
89
+ /** Long enough to cross from the trigger to its tooltip without losing it. */
90
+ const TIP_LINGER_MS = 150;
91
+ const tintRules = tint_js_1.AVATAR_TINTS.map((tint, index) => `.t${String(index)} { background: ${tint}; }`).join("\n ");
92
+ exports.ACCOUNT_CSS = `${contract_js_1.CONTRACT_CSS}
93
+ .trigger {
94
+ width: 40px; height: 40px; padding: 0; margin: 0; border: 0; border-radius: 9999px;
95
+ display: grid; place-items: center; background: transparent; color: inherit; cursor: pointer;
96
+ }
97
+ .trigger:hover { background: var(--_hover); }
98
+ .trigger:active { background: var(--_active); }
99
+ .trigger[aria-expanded="true"] { box-shadow: 0 0 0 2px var(--_accent); }
100
+ .trigger:focus-visible { outline: 2px solid var(--_ring); outline-offset: 2px; }
101
+ @media (pointer: coarse) { :host(:not([layout="row"])) .trigger { box-sizing: content-box; padding: 2px; } }
102
+ :host([layout="row"]) { display: block; }
103
+ :host([layout="row"]) .trigger {
104
+ box-sizing: border-box; width: 100%; height: auto; min-height: 44px; padding: 6px 8px; border-radius: 10px;
105
+ display: flex; align-items: center; gap: 10px; text-align: left; font: inherit;
106
+ }
107
+ :host([layout="row"]) .trigger[aria-expanded="true"] { background: var(--_hover); box-shadow: inset 0 0 0 1px var(--_accent); }
108
+ :host([layout="row"]) .trigger:focus-visible { outline-offset: 0; }
109
+ .who { flex: 1; min-width: 0; display: flex; flex-direction: column; }
110
+ .who-name { font-size: 14px; line-height: 19px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
111
+ .who-mail { font-size: 12px; line-height: 16px; color: var(--_ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
112
+
113
+ .avatar {
114
+ position: relative; display: grid; place-items: center; flex-shrink: 0; overflow: hidden;
115
+ border-radius: 9999px; color: ${tint_js_1.AVATAR_INK}; font-weight: 600; line-height: 1;
116
+ }
117
+ .avatar img { display: block; width: 100%; height: 100%; object-fit: cover; }
118
+ .s32 { width: 32px; height: 32px; font-size: 14px; }
119
+ .s72 { width: 72px; height: 72px; font-size: 30px; }
120
+ ${tintRules}
121
+
122
+ .tip {
123
+ position: absolute; top: calc(100% + 6px); right: 0; z-index: 2147483001; box-sizing: border-box;
124
+ width: max-content; max-width: min(320px, calc(100vw - 32px)); padding: 5px 9px; border-radius: 8px;
125
+ background: var(--_ink); color: var(--_overlay-solid); box-shadow: 0 0 0 1px var(--_hairline);
126
+ font-size: 12px; line-height: 16px; overflow-wrap: anywhere;
127
+ }
128
+ .tip[hidden], .popup[hidden], .scrim[hidden] { display: none; }
129
+
130
+ .popup {
131
+ position: absolute; top: calc(100% + 8px); right: 0; z-index: 2147483000;
132
+ box-sizing: border-box; width: 384px; max-width: calc(100vw - 32px); max-height: calc(100dvh - 96px);
133
+ overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column;
134
+ padding: 8px 8px 10px; border-radius: 28px;
135
+ background: var(--_tint); -webkit-backdrop-filter: var(--_backdrop); backdrop-filter: var(--_backdrop);
136
+ box-shadow: var(--_shadow); color: var(--_ink); animation: rise 120ms ease-out;
137
+ }
138
+ :host([align="start"]) .popup, :host([align="start"]) .tip { right: auto; left: 0; }
139
+ :host([placement="top"]) .popup:not(.sheet) { top: auto; bottom: calc(100% + 8px); }
140
+ :host([placement="top"]) .tip { top: auto; bottom: calc(100% + 6px); }
141
+ .popup.sheet {
142
+ position: fixed; top: auto; right: 0; bottom: 0; left: 0; width: 100%; max-width: none;
143
+ max-height: calc(100dvh - 48px); border-radius: 20px 20px 0 0;
144
+ padding-bottom: calc(20px + env(safe-area-inset-bottom, 0px)); animation: slide 200ms ease-out;
145
+ }
146
+ .scrim { position: fixed; inset: 0; z-index: 2147482999; background: oklch(0 0 0 / 0.45); }
147
+ @keyframes rise { from { opacity: 0; transform: translateY(4px); } }
148
+ @keyframes slide { from { transform: translateY(100%); } }
149
+ @keyframes spin { to { transform: rotate(360deg); } }
150
+
151
+ .panel { background: var(--_panel); }
152
+ button.panel, .org { -webkit-tap-highlight-color: transparent; }
153
+ button.panel:hover, .org:hover { background-image: linear-gradient(var(--_hover), var(--_hover)); }
154
+
155
+ .top { display: flex; align-items: center; min-height: 40px; padding-left: 44px; }
156
+ .context { flex: 1; min-width: 0; margin: 0; text-align: center; font-size: 13px; line-height: 18px; color: var(--_ink-2); }
157
+ .close {
158
+ display: grid; place-items: center; flex-shrink: 0; width: 40px; height: 40px; padding: 0; border: 0;
159
+ border-radius: 9999px; background: transparent; color: var(--_ink-2); cursor: pointer;
160
+ }
161
+ .close:hover { background: var(--_hover); }
162
+ .close:focus-visible { outline: 2px solid var(--_ring); outline-offset: -2px; }
163
+
164
+ .card {
165
+ position: relative; display: flex; flex-direction: column; align-items: center; gap: 2px;
166
+ padding: 22px 20px 20px; border-radius: 24px;
167
+ }
168
+ .portrait { position: relative; width: 72px; height: 72px; margin-bottom: 12px; }
169
+ .pencil {
170
+ position: absolute; right: -4px; bottom: -2px; box-sizing: border-box; width: 28px; height: 28px;
171
+ display: grid; place-items: center; border-radius: 9999px; color: var(--_ink);
172
+ background: var(--_overlay-solid); box-shadow: 0 0 0 2px var(--_tint-solid), inset 0 0 0 1px var(--_control);
173
+ }
174
+ .pencil::after { content: ""; position: absolute; inset: -8px; }
175
+ .pencil:focus-visible { outline: 2px solid var(--_ring); outline-offset: 2px; }
176
+ .name {
177
+ max-width: 100%; margin: 0; font-size: 20px; line-height: 26px; font-weight: 600; letter-spacing: -0.015em;
178
+ text-align: center; overflow-wrap: anywhere;
179
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
180
+ }
181
+ .email {
182
+ display: flex; justify-content: center; max-width: 100%; margin: 0; overflow: hidden; white-space: nowrap;
183
+ font-size: 14px; line-height: 20px; color: var(--_ink-2);
184
+ }
185
+ .local { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
186
+ .domain { flex-shrink: 0; }
187
+ .pill {
188
+ display: inline-flex; align-items: center; gap: 10px; box-sizing: border-box; max-width: 100%;
189
+ min-height: 44px; margin-top: 16px; padding: 0 20px 0 14px; border: 1px solid var(--_control); border-radius: 9999px;
190
+ color: var(--_ink); font-size: 14px; line-height: 20px; font-weight: 500; text-decoration: none;
191
+ }
192
+ .pill img { display: block; flex-shrink: 0; width: 20px; height: 20px; }
193
+ .pill:hover { background: var(--_hover); }
194
+ .pill.invite { background: var(--_selected); }
195
+ .pill:focus-visible { outline: 2px solid var(--_ring); outline-offset: 2px; }
196
+
197
+ .section { display: flex; flex-direction: column; gap: 2px; margin-top: 6px; }
198
+ .heading { margin: 0; padding: 8px 16px 4px; font-size: 13px; line-height: 18px; font-weight: 500; color: var(--_ink-2); }
199
+ .heading.with-mark { display: flex; align-items: center; gap: 8px; }
200
+ .heading img { display: block; width: 16px; height: 16px; flex-shrink: 0; }
201
+ .stack { display: flex; flex-direction: column; gap: 2px; }
202
+ .stack > * { border-radius: 4px; }
203
+ .stack > :first-child { border-top-left-radius: 18px; border-top-right-radius: 18px; }
204
+ .stack > :last-child { border-bottom-left-radius: 18px; border-bottom-right-radius: 18px; }
205
+ .item {
206
+ display: flex; align-items: center; gap: 12px; box-sizing: border-box; width: 100%; min-height: 48px; padding: 0 16px;
207
+ border: 0; color: var(--_ink); font: inherit; font-size: 14px; line-height: 20px; font-weight: 500;
208
+ text-align: left; text-decoration: none; cursor: pointer;
209
+ }
210
+ .item svg { flex-shrink: 0; color: var(--_ink-2); }
211
+ .item:focus-visible { outline: 2px solid var(--_ring); outline-offset: -2px; }
212
+ a.item:hover, button.item:hover { background-image: linear-gradient(var(--_hover), var(--_hover)); }
213
+ .wrap, .radios { display: flex; flex-direction: column; gap: 2px; }
214
+ .radios.all {
215
+ max-height: 300px; overflow-y: auto; overscroll-behavior: contain;
216
+ -webkit-mask-image: linear-gradient(180deg, #000 82%, transparent); mask-image: linear-gradient(180deg, #000 82%, transparent);
217
+ }
218
+ .radios.all.end { -webkit-mask-image: none; mask-image: none; }
219
+ .org {
220
+ display: flex; align-items: center; gap: 12px; box-sizing: border-box; min-height: 56px; padding: 8px 16px;
221
+ border-radius: 4px; cursor: pointer;
222
+ }
223
+ .org[aria-checked="true"] { background: var(--_selected); }
224
+ .radios[aria-readonly="true"] .org { cursor: default; }
225
+ .radios[aria-busy="true"] .org { cursor: progress; }
226
+ .org:focus-visible { outline: 2px solid var(--_ring); outline-offset: -2px; }
227
+ .wrap > .radios:last-child > .org:last-child { border-radius: 4px 4px 18px 18px; }
228
+ .org-mark {
229
+ display: grid; place-items: center; flex-shrink: 0; width: 32px; height: 32px; border-radius: 9px;
230
+ color: ${tint_js_1.AVATAR_INK}; font-size: 14px; font-weight: 600;
231
+ }
232
+ .org-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
233
+ .org-name { font-size: 14px; line-height: 20px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
234
+ .org-role { font-size: 12.5px; line-height: 17px; color: var(--_ink-2); }
235
+ .active { display: flex; align-items: center; gap: 6px; flex-shrink: 0; font-size: 12.5px; font-weight: 500; color: var(--_accent-text); }
236
+ .check { width: 5px; height: 10px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: translateY(-2px) rotate(45deg); }
237
+ .more {
238
+ display: flex; align-items: center; min-height: 48px; padding: 0 16px; border: 0; border-radius: 4px 4px 18px 18px;
239
+ color: var(--_accent-text); font: inherit; font-size: 14px; font-weight: 500; text-align: left; cursor: pointer;
240
+ }
241
+ .more:focus-visible, .signout:focus-visible { outline: 2px solid var(--_ring); outline-offset: -2px; }
242
+ .signout {
243
+ display: flex; align-items: center; gap: 12px; box-sizing: border-box; width: 100%; min-height: 52px; padding: 0 16px;
244
+ border: 0; color: var(--_ink); font: inherit; font-size: 14px; font-weight: 500;
245
+ text-align: left; cursor: pointer;
246
+ }
247
+ .signout[aria-busy="true"] { color: var(--_ink-2); cursor: progress; }
248
+ .spinner {
249
+ box-sizing: border-box; width: 14px; height: 14px; margin: 0 2px; flex-shrink: 0; border-radius: 9999px;
250
+ border: 2px solid var(--_hairline); border-top-color: var(--_ink-2); animation: spin 800ms linear infinite;
251
+ }
252
+ .error { margin: 0; padding: 6px 16px 2px; font-size: 13px; line-height: 19px; color: var(--_crit-ink); }
253
+ .legal {
254
+ display: flex; justify-content: center; flex-wrap: wrap; gap: 6px; padding: 12px 0 4px;
255
+ font-size: 12.5px; line-height: 18px; color: var(--_ink-2);
256
+ }
257
+ .legal a { padding: 2px 6px; border-radius: 6px; color: inherit; text-decoration: none; }
258
+ .legal a:hover { text-decoration: underline; }
259
+ .legal a:focus-visible { outline: 2px solid var(--_ring); outline-offset: 2px; }
260
+
261
+ @media (prefers-reduced-motion: reduce) {
262
+ .popup, .popup.sheet { animation: none; }
263
+ .spinner { animation-duration: 2400ms; }
264
+ }
265
+ @media (prefers-reduced-transparency: reduce) {
266
+ .popup { background: var(--_tint-solid); -webkit-backdrop-filter: none; backdrop-filter: none; }
267
+ }
268
+ @media (forced-colors: active) {
269
+ .popup, .panel, .org, .item { border: 1px solid CanvasText; background: Canvas; }
270
+ .org[aria-checked="true"] { outline: 2px solid Highlight; outline-offset: -2px; }
271
+ .avatar, .org-mark { border: 1px solid CanvasText; }
272
+ .pill, .legal a { color: LinkText; }
273
+ }
274
+ `;
275
+ function pencilMark() {
276
+ return (0, dom_js_1.pathIcon)(14, "0 0 24 24", "M4 20l1-5L15.5 4.5a2 2 0 0 1 2.8 0l1.2 1.2a2 2 0 0 1 0 2.8L9 19z");
277
+ }
278
+ function signOutMark() {
279
+ return (0, dom_js_1.pathIcon)(18, "0 0 24 24", "M10 4H5a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h5v-2H6V6h4zM16.6 7.4L15.2 8.8 17.4 11H9v2h8.4l-2.2 2.2 1.4 1.4L21.2 12z");
280
+ }
281
+ /** The first letter a person or an organisation is drawn with, whole even outside the basic plane. */
282
+ function initialOf(words) {
283
+ return [...words.trim()][0]?.toLocaleUpperCase() ?? "?";
284
+ }
285
+ /** A round picture, or the coloured initial while it loads and when it fails. */
286
+ function avatar(face, size) {
287
+ const made = (0, dom_js_1.element)("span", {
288
+ class: `avatar ${size} t${String(face.tint)}`,
289
+ "aria-hidden": "true",
290
+ });
291
+ if (face.picture === null) {
292
+ made.textContent = face.initial;
293
+ return made;
294
+ }
295
+ const image = (0, dom_js_1.element)("img", {
296
+ src: face.picture,
297
+ alt: "",
298
+ decoding: "async",
299
+ referrerpolicy: "no-referrer",
300
+ });
301
+ image.addEventListener("error", () => {
302
+ made.textContent = face.initial;
303
+ }, { once: true });
304
+ made.append(image);
305
+ return made;
306
+ }
307
+ /** The address on one line, cut in the middle so the domain survives: the domain is what tells two apart. */
308
+ function emailLine(email) {
309
+ const line = (0, dom_js_1.element)("p", { id: "email", class: "email", title: email });
310
+ const at = email.lastIndexOf("@");
311
+ if (at <= 0) {
312
+ line.append((0, dom_js_1.element)("span", { class: "local" }, email));
313
+ }
314
+ else {
315
+ line.append((0, dom_js_1.element)("span", { class: "local" }, email.slice(0, at)), (0, dom_js_1.element)("span", { class: "domain" }, email.slice(at)));
316
+ }
317
+ return line;
318
+ }
319
+ /** An address the product gave, absolute or relative to the page, and only ever http or https. */
320
+ function productAddress(value) {
321
+ if (typeof value !== "string" || value === "")
322
+ return null;
323
+ try {
324
+ return (0, validate_js_1.safeUrl)(new URL(value, document.baseURI).href);
325
+ }
326
+ catch {
327
+ return null;
328
+ }
329
+ }
330
+ const ICON_TAGS = new Set([
331
+ "svg",
332
+ "g",
333
+ "path",
334
+ "circle",
335
+ "ellipse",
336
+ "line",
337
+ "polyline",
338
+ "polygon",
339
+ "rect",
340
+ ]);
341
+ const ICON_ATTRIBUTES = new Set([
342
+ "viewBox",
343
+ "d",
344
+ "cx",
345
+ "cy",
346
+ "r",
347
+ "rx",
348
+ "ry",
349
+ "x",
350
+ "y",
351
+ "x1",
352
+ "y1",
353
+ "x2",
354
+ "y2",
355
+ "width",
356
+ "height",
357
+ "points",
358
+ "fill",
359
+ "stroke",
360
+ "stroke-width",
361
+ "stroke-linecap",
362
+ "stroke-linejoin",
363
+ "fill-rule",
364
+ "clip-rule",
365
+ "transform",
366
+ "opacity",
367
+ ]);
368
+ /**
369
+ * A product's icon, kept to shapes: no script, no link, no style and no reference
370
+ * to anything outside it, because it is drawn inside every page the element is on.
371
+ */
372
+ function iconOf(markup) {
373
+ let parsed;
374
+ try {
375
+ parsed = new DOMParser().parseFromString(markup, "image/svg+xml");
376
+ }
377
+ catch {
378
+ return null;
379
+ }
380
+ const root = parsed.documentElement;
381
+ if (root.localName !== "svg" || parsed.getElementsByTagName("parsererror").length > 0)
382
+ return null;
383
+ const copy = (source) => {
384
+ if (!ICON_TAGS.has(source.localName))
385
+ return null;
386
+ const made = document.createElementNS(dom_js_1.SVG_NS, source.localName);
387
+ for (const attribute of [...source.attributes]) {
388
+ if (ICON_ATTRIBUTES.has(attribute.name) && !/url\(|javascript:|&/iu.test(attribute.value)) {
389
+ made.setAttribute(attribute.name, attribute.value);
390
+ }
391
+ }
392
+ for (const child of [...source.children]) {
393
+ const kept = copy(child);
394
+ if (kept !== null)
395
+ made.append(kept);
396
+ }
397
+ return made;
398
+ };
399
+ const svg = copy(root);
400
+ if (svg === null)
401
+ return null;
402
+ svg.setAttribute("width", "18");
403
+ svg.setAttribute("height", "18");
404
+ svg.setAttribute("aria-hidden", "true");
405
+ svg.setAttribute("focusable", "false");
406
+ return svg;
407
+ }
408
+ function itemsOf(value) {
409
+ if (!Array.isArray(value))
410
+ return [];
411
+ const out = [];
412
+ for (const one of value) {
413
+ if (typeof one !== "object" || one === null)
414
+ continue;
415
+ const { id, label, href, onSelect, icon } = one;
416
+ if (typeof id !== "string" || id === "" || typeof label !== "string" || label.trim() === "")
417
+ continue;
418
+ const link = typeof href === "string" ? productAddress(href) : null;
419
+ const select = typeof onSelect === "function" ? onSelect : null;
420
+ if (link === null && select === null)
421
+ continue;
422
+ out.push({
423
+ id,
424
+ label: label.trim(),
425
+ href: link,
426
+ onSelect: select,
427
+ icon: typeof icon === "string" ? iconOf(icon) : null,
428
+ });
429
+ }
430
+ return out;
431
+ }
432
+ function personOf(value) {
433
+ if (typeof value !== "object" || value === null)
434
+ return null;
435
+ const { name, email, id, avatarUrl } = value;
436
+ if (typeof email !== "string" || email.trim() === "")
437
+ return null;
438
+ const shown = typeof name === "string" && name.trim() !== "" ? name.trim() : email.trim();
439
+ return {
440
+ name: shown,
441
+ email: email.trim(),
442
+ ...(typeof id === "string" && id !== "" ? { id } : {}),
443
+ avatarUrl: typeof avatarUrl === "string" ? avatarUrl : null,
444
+ };
445
+ }
446
+ function organisationsOf(value) {
447
+ if (!Array.isArray(value))
448
+ return [];
449
+ const out = [];
450
+ for (const one of value) {
451
+ if (typeof one !== "object" || one === null)
452
+ continue;
453
+ const { id, name, role, active } = one;
454
+ if (typeof id !== "string" || id === "" || typeof name !== "string" || name.trim() === "")
455
+ continue;
456
+ const label = typeof role === "string" && role.trim() !== "" ? role.trim() : null;
457
+ out.push({ id, name: name.trim(), role: label, active: active === true });
458
+ }
459
+ // The active one first: the row says which one you are acting in.
460
+ const active = out.findIndex((one) => one.active);
461
+ if (active > 0)
462
+ out.unshift(...out.splice(active, 1));
463
+ return out;
464
+ }
465
+ /**
466
+ * Defines `<gp-account>` once, where custom elements exist. Safe to import on a
467
+ * server: nothing touches the DOM until this is called in a browser.
468
+ */
469
+ function defineAccountElement(name = "gp-account") {
470
+ if (typeof customElements === "undefined" || customElements.get(name) !== undefined)
471
+ return;
472
+ customElements.define(name, createAccountElementClass());
473
+ }
474
+ /** The class itself, for a caller that registers it under its own name or in a scoped registry. */
475
+ function createAccountElementClass() {
476
+ return class GpAccountElement extends HTMLElement {
477
+ #core = null;
478
+ #unsubscribe = null;
479
+ #labels = exports.ACCOUNT_LABELS;
480
+ #person = null;
481
+ #organisations = [];
482
+ #product = null;
483
+ #viaIdentity = false;
484
+ #onSignOut = null;
485
+ #onSwitch = null;
486
+ #items = [];
487
+ #given = [];
488
+ #open = false;
489
+ #sheet = false;
490
+ #allShown = false;
491
+ #signOut = "idle";
492
+ #switching = null;
493
+ #switchFailed = null;
494
+ #roving = null;
495
+ #tipTimer;
496
+ #quietFocus = false;
497
+ #face = "";
498
+ #root;
499
+ #trigger;
500
+ #tip;
501
+ #popup;
502
+ #scrim;
503
+ #status;
504
+ constructor() {
505
+ super();
506
+ this.#root = this.attachShadow({ mode: "open" });
507
+ const style = (0, dom_js_1.element)("style");
508
+ style.textContent = exports.ACCOUNT_CSS;
509
+ this.#trigger = (0, dom_js_1.element)("button", {
510
+ type: "button",
511
+ class: "trigger",
512
+ part: "trigger",
513
+ "aria-haspopup": "dialog",
514
+ "aria-expanded": "false",
515
+ "aria-controls": "popup",
516
+ });
517
+ // The trigger is already named with these words; the tooltip only shows them.
518
+ this.#tip = (0, dom_js_1.element)("div", {
519
+ class: "tip",
520
+ part: "tooltip",
521
+ "aria-hidden": "true",
522
+ hidden: "",
523
+ });
524
+ this.#scrim = (0, dom_js_1.element)("div", { class: "scrim", hidden: "" });
525
+ this.#popup = (0, dom_js_1.element)("div", {
526
+ id: "popup",
527
+ class: "popup",
528
+ part: "popup",
529
+ role: "dialog",
530
+ "aria-labelledby": "who",
531
+ "aria-describedby": "email",
532
+ hidden: "",
533
+ });
534
+ // Outside the popup, so "Switched to ..." is heard after it has closed.
535
+ this.#status = (0, dom_js_1.element)("div", { class: "sr-only", role: "status" });
536
+ this.#root.append(style, this.#trigger, this.#tip, this.#scrim, this.#popup, this.#status);
537
+ this.#trigger.addEventListener("click", () => {
538
+ this.#hideTip();
539
+ if (this.#open)
540
+ this.close(true);
541
+ else
542
+ this.show();
543
+ });
544
+ this.#trigger.addEventListener("focus", () => {
545
+ if (!this.#quietFocus)
546
+ this.#tipSoon();
547
+ });
548
+ this.#trigger.addEventListener("blur", () => this.#hideTip());
549
+ this.addEventListener("pointerenter", () => this.#tipSoon());
550
+ this.addEventListener("pointerleave", () => this.#tipLater());
551
+ this.#scrim.addEventListener("click", () => this.close(true));
552
+ this.#popup.addEventListener("keydown", (event) => this.#keyed(event));
553
+ this.#popup.addEventListener("click", (event) => this.#clicked(event));
554
+ }
555
+ /** The core `<gp-apps>` uses: Identity's half of what is shown. Nothing renders until one is set and enabled. */
556
+ get core() {
557
+ return this.#core;
558
+ }
559
+ set core(next) {
560
+ this.#unsubscribe?.();
561
+ this.#unsubscribe = null;
562
+ this.#core = next;
563
+ if (next !== null) {
564
+ this.#unsubscribe = next.subscribe(() => this.#render());
565
+ if (this.isConnected && next.state().status === "idle")
566
+ void next.load();
567
+ }
568
+ this.#render();
569
+ }
570
+ /** Who is signed in. Nothing renders without one. */
571
+ get person() {
572
+ return this.#person;
573
+ }
574
+ set person(next) {
575
+ const person = personOf(next);
576
+ const same = person !== null &&
577
+ this.#person !== null &&
578
+ person.email === this.#person.email &&
579
+ person.id === this.#person.id;
580
+ if (!same)
581
+ this.#signOut = "idle";
582
+ this.#person = person;
583
+ this.#render();
584
+ }
585
+ get organisations() {
586
+ return this.#organisations;
587
+ }
588
+ /** None, or an empty list, and the section is absent rather than empty. */
589
+ set organisations(next) {
590
+ this.#organisations = organisationsOf(next);
591
+ if (this.#switchFailed !== null &&
592
+ !this.#organisations.some((one) => one.id === this.#switchFailed))
593
+ this.#switchFailed = null;
594
+ this.#render();
595
+ }
596
+ /** The product's own name, read from its catalogue entry. Without it, the current tile's. */
597
+ get product() {
598
+ return this.#product;
599
+ }
600
+ set product(next) {
601
+ this.#product = typeof next === "string" && next.trim() !== "" ? next.trim() : null;
602
+ this.#render();
603
+ }
604
+ /** Whether the product signs in through Identity, so signing out ends both and the row says so (D21). */
605
+ get viaIdentity() {
606
+ return this.#viaIdentity;
607
+ }
608
+ set viaIdentity(next) {
609
+ this.#viaIdentity = next === true;
610
+ this.#render();
611
+ }
612
+ get onSignOut() {
613
+ return this.#onSignOut;
614
+ }
615
+ /** No handler, no Sign out row. */
616
+ set onSignOut(next) {
617
+ this.#onSignOut = typeof next === "function" ? next : null;
618
+ this.#render();
619
+ }
620
+ get onSwitch() {
621
+ return this.#onSwitch;
622
+ }
623
+ /** No handler, and the organisations are listed read only. */
624
+ set onSwitch(next) {
625
+ this.#onSwitch = typeof next === "function" ? next : null;
626
+ this.#render();
627
+ }
628
+ static get observedAttributes() {
629
+ return ["layout"];
630
+ }
631
+ attributeChangedCallback() {
632
+ this.#face = "";
633
+ this.#render();
634
+ }
635
+ get items() {
636
+ return this.#given;
637
+ }
638
+ /** The product's own items. None, and the group is absent. */
639
+ set items(next) {
640
+ this.#given = Array.isArray(next) ? next : [];
641
+ this.#items = itemsOf(next);
642
+ this.#render();
643
+ }
644
+ get labels() {
645
+ return this.#labels;
646
+ }
647
+ /** Any of the words, replaced; the rest stay English. */
648
+ set labels(next) {
649
+ this.#labels = { ...exports.ACCOUNT_LABELS, ...next };
650
+ this.#render();
651
+ }
652
+ get open() {
653
+ return this.#open;
654
+ }
655
+ connectedCallback() {
656
+ // A property set before the element was defined sits on the instance; move it through the setters.
657
+ for (const key of [
658
+ "core",
659
+ "person",
660
+ "organisations",
661
+ "product",
662
+ "viaIdentity",
663
+ "onSignOut",
664
+ "onSwitch",
665
+ "items",
666
+ "labels",
667
+ ]) {
668
+ if (Object.prototype.hasOwnProperty.call(this, key)) {
669
+ const value = this[key];
670
+ delete this[key];
671
+ this[key] = value;
672
+ }
673
+ }
674
+ if (this.#core?.state().status === "idle")
675
+ void this.#core.load();
676
+ this.#render();
677
+ }
678
+ disconnectedCallback() {
679
+ this.close(false);
680
+ this.#hideTip();
681
+ }
682
+ #outside = (event) => {
683
+ if (event.composedPath().includes(this))
684
+ return;
685
+ this.close(false);
686
+ // Focus returns to the trigger unless the click landed on something that takes it.
687
+ setTimeout(() => {
688
+ if (document.activeElement === document.body || document.activeElement === null) {
689
+ this.#returnFocus();
690
+ }
691
+ }, 0);
692
+ };
693
+ #documentKey = (event) => {
694
+ if (event.key === "Escape" && this.#open) {
695
+ event.stopPropagation();
696
+ this.close(true);
697
+ }
698
+ };
699
+ #mediaChanged = () => {
700
+ if (!this.#open)
701
+ return;
702
+ this.#layout();
703
+ this.#render();
704
+ };
705
+ /** Escape takes the tooltip away and reaches nothing else, wherever focus is (1.4.13). */
706
+ #tipKey = (event) => {
707
+ if (event.key !== "Escape")
708
+ return;
709
+ event.stopPropagation();
710
+ this.#hideTip();
711
+ };
712
+ #tipSoon() {
713
+ if (this.#open || this.hidden)
714
+ return;
715
+ clearTimeout(this.#tipTimer);
716
+ this.#tipTimer = setTimeout(() => this.#showTip(), TIP_DELAY_MS);
717
+ }
718
+ /** The pointer left: the tooltip goes shortly unless it comes back, or focus holds it. */
719
+ #tipLater() {
720
+ if (this.#root.activeElement === this.#trigger)
721
+ return;
722
+ clearTimeout(this.#tipTimer);
723
+ this.#tipTimer = setTimeout(() => this.#hideTip(), TIP_LINGER_MS);
724
+ }
725
+ #showTip() {
726
+ if (this.#open || this.hidden)
727
+ return;
728
+ this.#tip.hidden = false;
729
+ window.addEventListener("keydown", this.#tipKey, true);
730
+ }
731
+ #hideTip() {
732
+ clearTimeout(this.#tipTimer);
733
+ this.#tip.hidden = true;
734
+ window.removeEventListener("keydown", this.#tipKey, true);
735
+ }
736
+ #returnFocus() {
737
+ this.#quietFocus = true;
738
+ this.#trigger.focus();
739
+ this.#quietFocus = false;
740
+ }
741
+ /** Opens the popup and puts focus on the pill, else the active organisation, else Sign out. */
742
+ show() {
743
+ const core = this.#core;
744
+ if (core === null || this.hidden || this.#open)
745
+ return;
746
+ this.#open = true;
747
+ this.#hideTip();
748
+ const state = core.state();
749
+ if (state.status !== "loading" &&
750
+ (state.updatedAt === null || Date.now() - state.updatedAt > FRESH_MS)) {
751
+ void core.load();
752
+ }
753
+ this.#layout();
754
+ this.#trigger.setAttribute("aria-expanded", "true");
755
+ this.#popup.hidden = false;
756
+ document.addEventListener("pointerdown", this.#outside, true);
757
+ document.addEventListener("keydown", this.#documentKey);
758
+ window.matchMedia?.(SHEET_QUERY).addEventListener?.("change", this.#mediaChanged);
759
+ this.#render();
760
+ this.#focusStart();
761
+ }
762
+ /** Closes it, and gives focus back to the trigger when asked to. */
763
+ close(returnFocus) {
764
+ if (!this.#open)
765
+ return;
766
+ this.#open = false;
767
+ this.#allShown = false;
768
+ this.#switchFailed = null;
769
+ if (this.#signOut === "failed")
770
+ this.#signOut = "idle";
771
+ this.#trigger.setAttribute("aria-expanded", "false");
772
+ this.#popup.hidden = true;
773
+ this.#scrim.hidden = true;
774
+ document.removeEventListener("pointerdown", this.#outside, true);
775
+ document.removeEventListener("keydown", this.#documentKey);
776
+ window.matchMedia?.(SHEET_QUERY).removeEventListener?.("change", this.#mediaChanged);
777
+ if (returnFocus)
778
+ this.#returnFocus();
779
+ }
780
+ #layout() {
781
+ this.#sheet = window.matchMedia?.(SHEET_QUERY).matches ?? false;
782
+ this.#popup.classList.toggle("sheet", this.#sheet);
783
+ this.#scrim.hidden = !this.#sheet;
784
+ if (this.#sheet)
785
+ this.#popup.setAttribute("aria-modal", "true");
786
+ else
787
+ this.#popup.removeAttribute("aria-modal");
788
+ }
789
+ #keyed(event) {
790
+ if (event.key === "Escape") {
791
+ event.preventDefault();
792
+ event.stopPropagation();
793
+ this.close(true);
794
+ return;
795
+ }
796
+ if (event.key === "Tab") {
797
+ this.#tabbed(event);
798
+ return;
799
+ }
800
+ const radio = event.composedPath()[0]?.closest?.('[role="radio"]');
801
+ if (!(radio instanceof HTMLElement))
802
+ return;
803
+ if (event.key === " " || event.key === "Enter") {
804
+ event.preventDefault();
805
+ const id = radio.dataset["id"];
806
+ if (id !== undefined)
807
+ void this.#choose(id);
808
+ return;
809
+ }
810
+ const radios = this.#radios();
811
+ const index = radios.indexOf(radio);
812
+ let next;
813
+ switch (event.key) {
814
+ case "ArrowDown":
815
+ case "ArrowRight":
816
+ next = Math.min(index + 1, radios.length - 1);
817
+ break;
818
+ case "ArrowUp":
819
+ case "ArrowLeft":
820
+ next = Math.max(index - 1, 0);
821
+ break;
822
+ case "Home":
823
+ next = 0;
824
+ break;
825
+ case "End":
826
+ next = radios.length - 1;
827
+ break;
828
+ default:
829
+ return;
830
+ }
831
+ event.preventDefault();
832
+ const target = radios[next];
833
+ if (target === undefined)
834
+ return;
835
+ this.#roveTo(target);
836
+ target.focus();
837
+ }
838
+ /** Tab between the stops; from the last one it closes, or in a sheet it loops. */
839
+ #tabbed(event) {
840
+ const stops = [
841
+ ...this.#popup.querySelectorAll('button, a[href], [role="radio"][tabindex="0"]'),
842
+ ];
843
+ if (stops.length === 0)
844
+ return;
845
+ const here = stops.indexOf(this.#root.activeElement);
846
+ const last = stops.length - 1;
847
+ if (!event.shiftKey && here === last) {
848
+ event.preventDefault();
849
+ if (this.#sheet)
850
+ stops[0]?.focus();
851
+ else
852
+ this.close(true);
853
+ }
854
+ else if (event.shiftKey && here === 0 && this.#sheet) {
855
+ event.preventDefault();
856
+ stops[last]?.focus();
857
+ }
858
+ }
859
+ #clicked(event) {
860
+ const target = event.composedPath()[0]?.closest?.("[data-action]");
861
+ if (!(target instanceof HTMLElement))
862
+ return;
863
+ switch (target.dataset["action"]) {
864
+ case "close":
865
+ this.close(true);
866
+ break;
867
+ case "more":
868
+ this.#showAll();
869
+ break;
870
+ case "signout":
871
+ void this.#signOutNow();
872
+ break;
873
+ case "org": {
874
+ const id = target.dataset["id"];
875
+ if (id !== undefined)
876
+ void this.#choose(id);
877
+ break;
878
+ }
879
+ case "item":
880
+ this.#chosen(target, event);
881
+ break;
882
+ default:
883
+ break;
884
+ }
885
+ }
886
+ #radios() {
887
+ return [...this.#popup.querySelectorAll('[role="radio"]')];
888
+ }
889
+ #roveTo(radio) {
890
+ for (const other of this.#radios())
891
+ other.tabIndex = other === radio ? 0 : -1;
892
+ this.#roving = radio.dataset["id"] ?? null;
893
+ }
894
+ #byKey(key) {
895
+ return [...this.#popup.querySelectorAll("[data-key]")].find((one) => one.dataset["key"] === key);
896
+ }
897
+ #focusStart() {
898
+ const start = this.#byKey("pill") ??
899
+ this.#popup.querySelector('[role="radio"][tabindex="0"]') ??
900
+ this.#byKey("signout") ??
901
+ this.#byKey("close");
902
+ start?.focus();
903
+ }
904
+ /** The rest open in place, and focus lands on the row that took "Show all"'s spot. */
905
+ #showAll() {
906
+ this.#allShown = true;
907
+ const moved = this.#organisations[SHOWN];
908
+ if (moved !== undefined)
909
+ this.#roving = moved.id;
910
+ this.#render();
911
+ if (moved !== undefined)
912
+ this.#byKey(`org:${moved.id}`)?.focus();
913
+ }
914
+ /** One of the product's items: its handler runs, a link goes where it goes, and the popup closes. */
915
+ #chosen(target, event) {
916
+ const item = this.#items.find((one) => one.id === target.dataset["id"]);
917
+ if (item === undefined)
918
+ return;
919
+ try {
920
+ item.onSelect?.(event);
921
+ }
922
+ catch {
923
+ // The product's fault, and not a reason to leave the popup open over the page.
924
+ }
925
+ this.close(false);
926
+ // Back to the trigger unless the item took focus somewhere, as a dialog it opened would.
927
+ setTimeout(() => {
928
+ if (document.activeElement === document.body || document.activeElement === null) {
929
+ this.#returnFocus();
930
+ }
931
+ }, 0);
932
+ }
933
+ async #choose(id) {
934
+ const handler = this.#onSwitch;
935
+ const organisation = this.#organisations.find((one) => one.id === id);
936
+ if (handler === null ||
937
+ organisation === undefined ||
938
+ organisation.active ||
939
+ this.#switching !== null)
940
+ return;
941
+ this.#switching = id;
942
+ this.#switchFailed = null;
943
+ this.#roving = id;
944
+ this.#render();
945
+ try {
946
+ await handler(id);
947
+ }
948
+ catch {
949
+ this.#switching = null;
950
+ this.#switchFailed = id;
951
+ this.#render();
952
+ return;
953
+ }
954
+ this.#switching = null;
955
+ this.close(true);
956
+ this.#status.textContent = this.#labels.switched(organisation.name);
957
+ }
958
+ async #signOutNow() {
959
+ const handler = this.#onSignOut;
960
+ if (handler === null || this.#signOut === "busy")
961
+ return;
962
+ this.#signOut = "busy";
963
+ this.#render();
964
+ try {
965
+ await handler();
966
+ }
967
+ catch {
968
+ this.#signOut = "failed";
969
+ this.#render();
970
+ return;
971
+ }
972
+ // Signed out. The product navigates; the kept list goes, so a shared device never shows the next person this one's.
973
+ this.#core?.clear();
974
+ }
975
+ #productName(view) {
976
+ return this.#product ?? view?.yours.find((tile) => tile.current)?.displayName ?? null;
977
+ }
978
+ /** Identity's picture if there is an Identity, else the product's, else the coloured initial (D22). */
979
+ #faceOf(view, person) {
980
+ const own = view?.person.hasIdentity === true ? view.person : null;
981
+ return {
982
+ picture: own?.avatarUrl ?? productAddress(person.avatarUrl),
983
+ tint: own?.tint ?? (0, tint_js_1.avatarTint)(person.id ?? person.email.toLowerCase()),
984
+ initial: initialOf(person.name),
985
+ };
986
+ }
987
+ #render() {
988
+ const state = this.#core?.state() ?? null;
989
+ const person = this.#person;
990
+ this.hidden = state === null || state.status === "off" || person === null;
991
+ if (state === null || person === null || this.hidden) {
992
+ this.close(false);
993
+ this.#hideTip();
994
+ return;
995
+ }
996
+ const labels = this.#labels;
997
+ const view = state.view;
998
+ const name = labels.trigger(person.name);
999
+ const row = this.getAttribute("layout") === "row";
1000
+ // In a row the address is on screen too, so the name says it as well (2.5.3, label in name).
1001
+ this.#trigger.setAttribute("aria-label", row ? `${name}, ${person.email}` : name);
1002
+ this.#tip.textContent = name;
1003
+ const face = this.#faceOf(view, person);
1004
+ const signature = `${face.picture ?? ""} ${String(face.tint)} ${face.initial} ${row ? `${person.name} ${person.email}` : ""}`;
1005
+ if (signature !== this.#face) {
1006
+ this.#face = signature;
1007
+ this.#trigger.replaceChildren(avatar(face, "s32"));
1008
+ if (row) {
1009
+ const who = (0, dom_js_1.element)("span", { class: "who", "aria-hidden": "true" });
1010
+ who.append((0, dom_js_1.element)("span", { class: "who-name" }, person.name), (0, dom_js_1.element)("span", { class: "who-mail" }, person.email));
1011
+ this.#trigger.append(who);
1012
+ }
1013
+ }
1014
+ if (!this.#open)
1015
+ return;
1016
+ // What had focus and how far things were scrolled, so a view that swaps in underneath keeps both.
1017
+ const active = this.#root.activeElement;
1018
+ const focused = active?.dataset["key"];
1019
+ const hadFocus = active !== null && this.#popup.contains(active);
1020
+ const scrolled = this.#popup.scrollTop;
1021
+ const listScrolled = this.#popup.querySelector(".radios")?.scrollTop ?? 0;
1022
+ this.#popup.replaceChildren(this.#content(view, person, face));
1023
+ this.#popup.scrollTop = scrolled;
1024
+ const list = this.#popup.querySelector(".radios.all");
1025
+ if (list !== null) {
1026
+ list.scrollTop = listScrolled;
1027
+ const fade = () => {
1028
+ list.classList.toggle("end", list.scrollTop + list.clientHeight >= list.scrollHeight - 2);
1029
+ };
1030
+ list.addEventListener("scroll", fade, { passive: true });
1031
+ fade();
1032
+ }
1033
+ const again = focused === undefined ? undefined : this.#byKey(focused);
1034
+ if (again !== undefined)
1035
+ again.focus();
1036
+ else if (hadFocus)
1037
+ this.#focusStart();
1038
+ }
1039
+ #content(view, person, face) {
1040
+ const labels = this.#labels;
1041
+ const product = this.#productName(view);
1042
+ const out = document.createDocumentFragment();
1043
+ const top = (0, dom_js_1.element)("div", { class: "top" });
1044
+ top.append((0, dom_js_1.element)("p", { class: "context" }, product === null ? "" : labels.signedInTo(product)));
1045
+ const close = (0, dom_js_1.element)("button", {
1046
+ type: "button",
1047
+ class: "close",
1048
+ "data-action": "close",
1049
+ "data-key": "close",
1050
+ "aria-label": labels.close,
1051
+ });
1052
+ close.append((0, dom_js_1.crossMark)());
1053
+ top.append(close);
1054
+ out.append(top);
1055
+ const own = view?.person.hasIdentity === true;
1056
+ const links = view?.identity ?? null;
1057
+ const card = (0, dom_js_1.element)("div", { class: "card panel" });
1058
+ const portrait = (0, dom_js_1.element)("span", { class: "portrait" });
1059
+ portrait.append(avatar(face, "s72"));
1060
+ if (own && links !== null) {
1061
+ const pencil = (0, dom_js_1.element)("a", {
1062
+ class: "pencil",
1063
+ href: links.pictureUrl,
1064
+ "data-key": "pencil",
1065
+ "aria-label": labels.changePicture,
1066
+ });
1067
+ pencil.append(pencilMark());
1068
+ portrait.append(pencil);
1069
+ }
1070
+ card.append(portrait, (0, dom_js_1.element)("h2", { id: "who", class: "name", title: person.name }, person.name), emailLine(person.email));
1071
+ if (links !== null) {
1072
+ const pill = (0, dom_js_1.element)("a", {
1073
+ class: own ? "pill" : "pill invite",
1074
+ href: own ? links.accountUrl : links.setupUrl,
1075
+ "data-key": "pill",
1076
+ });
1077
+ if (links.iconUrl !== null) {
1078
+ pill.append((0, dom_js_1.element)("img", {
1079
+ src: links.iconUrl,
1080
+ alt: "",
1081
+ width: "20",
1082
+ height: "20",
1083
+ decoding: "async",
1084
+ referrerpolicy: "no-referrer",
1085
+ }));
1086
+ }
1087
+ pill.append((0, dom_js_1.element)("span", {}, own ? labels.manage : labels.setUp));
1088
+ card.append(pill);
1089
+ }
1090
+ out.append(card);
1091
+ if (this.#organisations.length > 0)
1092
+ out.append(this.#organisationsSection());
1093
+ out.append(...this.#actionSections(view, product));
1094
+ if (view !== null && view.legal !== null) {
1095
+ const footer = (0, dom_js_1.element)("div", { class: "legal" });
1096
+ footer.append((0, dom_js_1.element)("a", { href: view.legal.termsUrl, "data-key": "terms" }, labels.terms), (0, dom_js_1.element)("span", { "aria-hidden": "true" }, "·"), (0, dom_js_1.element)("a", { href: view.legal.privacyUrl, "data-key": "privacy" }, labels.privacy));
1097
+ out.append(footer);
1098
+ }
1099
+ return out;
1100
+ }
1101
+ #organisationsSection() {
1102
+ const labels = this.#labels;
1103
+ const all = this.#organisations;
1104
+ const cut = !this.#allShown && all.length > SHOWN;
1105
+ const shown = cut ? all.slice(0, SHOWN) : all;
1106
+ const stop = shown.find((one) => one.id === this.#roving) ?? shown.find((one) => one.active) ?? shown[0];
1107
+ const section = (0, dom_js_1.element)("div", { class: "section" });
1108
+ section.append((0, dom_js_1.element)("p", { id: "orgs", class: "heading" }, labels.organisations));
1109
+ const wrap = (0, dom_js_1.element)("div", { class: "wrap" });
1110
+ const radios = (0, dom_js_1.element)("div", {
1111
+ class: this.#allShown && all.length > SHOWN ? "radios all" : "radios",
1112
+ role: "radiogroup",
1113
+ "aria-labelledby": "orgs",
1114
+ });
1115
+ if (this.#onSwitch === null)
1116
+ radios.setAttribute("aria-readonly", "true");
1117
+ if (this.#switching !== null)
1118
+ radios.setAttribute("aria-busy", "true");
1119
+ for (const one of shown) {
1120
+ const row = (0, dom_js_1.element)("div", {
1121
+ class: "org panel",
1122
+ role: "radio",
1123
+ "aria-checked": String(one.active),
1124
+ "aria-label": one.role ? `${one.name}, ${one.role}` : one.name,
1125
+ tabindex: one === stop ? "0" : "-1",
1126
+ "data-action": "org",
1127
+ "data-id": one.id,
1128
+ "data-key": `org:${one.id}`,
1129
+ });
1130
+ const text = (0, dom_js_1.element)("span", { class: "org-text", "aria-hidden": "true" });
1131
+ text.append((0, dom_js_1.element)("span", { class: "org-name", title: one.name }, one.name));
1132
+ if (one.role)
1133
+ text.append((0, dom_js_1.element)("span", { class: "org-role" }, one.role));
1134
+ row.append((0, dom_js_1.element)("span", { class: `org-mark t${String((0, tint_js_1.avatarTint)(one.id))}`, "aria-hidden": "true" }, initialOf(one.name)), text);
1135
+ if (one.active) {
1136
+ const tag = (0, dom_js_1.element)("span", { class: "active", "aria-hidden": "true" });
1137
+ tag.append((0, dom_js_1.element)("span", { class: "check" }), labels.active);
1138
+ row.append(tag);
1139
+ }
1140
+ radios.append(row);
1141
+ }
1142
+ wrap.append(radios);
1143
+ if (cut) {
1144
+ wrap.append((0, dom_js_1.element)("button", { type: "button", class: "more panel", "data-action": "more", "data-key": "more" }, labels.showAll(all.length)));
1145
+ }
1146
+ section.append(wrap);
1147
+ const failed = all.find((one) => one.id === this.#switchFailed);
1148
+ if (failed !== undefined) {
1149
+ section.append((0, dom_js_1.element)("p", { class: "error", role: "alert" }, labels.switchFailed(failed.name)));
1150
+ }
1151
+ return section;
1152
+ }
1153
+ /**
1154
+ * The product's items and Sign out. Until the product signs in through
1155
+ * Identity, Sign out is the last row of the items' group, one of the
1156
+ * product's own actions; once it does, it ends both sessions and stands apart.
1157
+ */
1158
+ #actionSections(view, product) {
1159
+ const labels = this.#labels;
1160
+ const name = product ?? labels.thisProduct;
1161
+ const sections = [];
1162
+ const joined = !this.#viaIdentity;
1163
+ let stack = null;
1164
+ let section = null;
1165
+ if (this.#items.length > 0) {
1166
+ section = (0, dom_js_1.element)("div", { class: "section" });
1167
+ if (product !== null) {
1168
+ const heading = (0, dom_js_1.element)("p", { class: "heading with-mark" });
1169
+ const mark = view?.yours.find((tile) => tile.current)?.iconUrl ?? null;
1170
+ if (mark !== null) {
1171
+ heading.append((0, dom_js_1.element)("img", {
1172
+ src: mark,
1173
+ alt: "",
1174
+ width: "16",
1175
+ height: "16",
1176
+ referrerpolicy: "no-referrer",
1177
+ }));
1178
+ }
1179
+ heading.append(product);
1180
+ section.append(heading);
1181
+ }
1182
+ stack = (0, dom_js_1.element)("div", { class: "stack" });
1183
+ for (const item of this.#items)
1184
+ stack.append(this.#itemRow(item));
1185
+ section.append(stack);
1186
+ sections.push(section);
1187
+ }
1188
+ if (this.#onSignOut === null)
1189
+ return sections;
1190
+ const button = this.#signOutButton(name);
1191
+ if (joined && stack !== null && section !== null) {
1192
+ stack.append(button);
1193
+ }
1194
+ else {
1195
+ section = (0, dom_js_1.element)("div", { class: "section" });
1196
+ stack = (0, dom_js_1.element)("div", { class: "stack" });
1197
+ stack.append(button);
1198
+ section.append(stack);
1199
+ sections.push(section);
1200
+ }
1201
+ if (this.#signOut === "failed") {
1202
+ button.setAttribute("aria-describedby", "signout-failed");
1203
+ section.append((0, dom_js_1.element)("p", { id: "signout-failed", class: "error", role: "alert" }, labels.signOutFailed(name)));
1204
+ }
1205
+ return sections;
1206
+ }
1207
+ #itemRow(item) {
1208
+ const row = item.href === null
1209
+ ? (0, dom_js_1.element)("button", { type: "button" })
1210
+ : (0, dom_js_1.element)("a", { href: item.href });
1211
+ row.className = "item panel";
1212
+ row.dataset["action"] = "item";
1213
+ row.dataset["id"] = item.id;
1214
+ row.dataset["key"] = `item:${item.id}`;
1215
+ if (item.icon !== null)
1216
+ row.append(item.icon.cloneNode(true));
1217
+ row.append((0, dom_js_1.element)("span", {}, item.label));
1218
+ return row;
1219
+ }
1220
+ #signOutButton(name) {
1221
+ const labels = this.#labels;
1222
+ const busy = this.#signOut === "busy";
1223
+ const failed = this.#signOut === "failed";
1224
+ const button = (0, dom_js_1.element)("button", {
1225
+ type: "button",
1226
+ class: "signout panel",
1227
+ "data-action": "signout",
1228
+ "data-key": "signout",
1229
+ });
1230
+ if (busy) {
1231
+ button.setAttribute("aria-busy", "true");
1232
+ button.append((0, dom_js_1.element)("span", { class: "spinner", "aria-hidden": "true" }));
1233
+ }
1234
+ else {
1235
+ button.append(signOutMark());
1236
+ }
1237
+ button.append((0, dom_js_1.element)("span", {}, busy
1238
+ ? labels.signingOut(name)
1239
+ : failed
1240
+ ? labels.retry
1241
+ : this.#viaIdentity
1242
+ ? labels.signOutBoth(name)
1243
+ : labels.signOut(name)));
1244
+ return button;
1245
+ }
1246
+ };
1247
+ }
1248
+ //# sourceMappingURL=account.js.map