@ghub/identity-chrome 0.3.0 → 0.5.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.
package/README.md CHANGED
@@ -76,7 +76,9 @@ const account = document.querySelector("gp-account")!;
76
76
  account.core = chrome;
77
77
  account.product = "GAdvisory"; // your own name, from your catalogue entry
78
78
  account.person = { name: user.name, email: user.email, id: user.id, avatarUrl: user.picture };
79
- account.organisations = [{ id: "org_1", name: "Hafen Labs", role: "Admin", active: true }];
79
+ account.organisations = [
80
+ { id: "org_1", name: "Hafen Labs", role: "Admin", iconUrl: "/orgs/org_1/avatar", active: true },
81
+ ];
80
82
  account.onSwitch = async (id) => switchOrganisation(id); // reject when it did not switch
81
83
  account.onSignOut = async () => {
82
84
  await signOut(); // reject when it failed: the popup says they are still signed in
@@ -90,7 +92,8 @@ and the account popup renders nothing without a person.
90
92
 
91
93
  **The account popup shows mostly what the product passes**: the name and address from your
92
94
  session, your organisations with the role your product gives them there (its own label, already in the
93
- person's language and shown as given; none, and the row has no second line) and your two actions.
95
+ person's language and shown as given; none, and the row has no second line) and their own picture
96
+ where they have one (`iconUrl`; the initial otherwise, and if it does not load), and your two actions.
94
97
  Identity adds the picture, the colour of the initial, the links to its own account and set up
95
98
  screens and the footer, so the popup still signs somebody out while Identity is down. The
96
99
  picture is Identity's if the person has one, else yours, else a coloured initial whose tint
@@ -104,7 +107,13 @@ Every word can be replaced through `labels`; the rest stay English.
104
107
 
105
108
  ```html
106
109
  <div style="display: flex; align-items: center">
107
- <gp-account layout="row" placement="top" align="start" style="flex: 1"></gp-account>
110
+ <gp-account
111
+ layout="row"
112
+ density="compact"
113
+ placement="top"
114
+ align="start"
115
+ style="flex: 1"
116
+ ></gp-account>
108
117
  <gp-apps placement="top" align="start"></gp-apps>
109
118
  </div>
110
119
  ```
@@ -114,13 +123,14 @@ account.labels = { organisations: "Groups" };
114
123
  account.items = [
115
124
  { id: "profile", label: "Profile", href: "/settings/profile", icon: userSvg },
116
125
  { id: "account", label: "Account", href: "/settings/account", icon: settingsSvg },
117
- { id: "replay", label: "Session recording", onSelect: () => openReplayChoice(), icon: videoSvg },
126
+ { id: "shortcuts", label: "Keyboard shortcuts", onSelect: () => openShortcuts(), icon: keysSvg },
118
127
  ];
119
128
  ```
120
129
 
121
130
  `layout="row"` draws the trigger as a full width row, the picture with the name over the
122
- address; `placement="top"` opens both popups upward. Your items follow the organisations
123
- under your product's name, as links or as buttons; on a link, `onSelect` may call
131
+ address; `density="compact"` draws the account popup a step smaller (320 wide, tighter rows),
132
+ so it does not cover the rail and half the page; `placement="top"` opens both popups upward.
133
+ Your items follow the organisations under your product's name, as links or as buttons; on a link, `onSelect` may call
124
134
  `preventDefault()` and navigate with your own router. An icon is SVG markup on a 24 grid and
125
135
  is kept to its shapes. Until you sign in through Identity, Sign out is the last row of your
126
136
  items' group; with `viaIdentity` it ends both sessions and stands on its own.
@@ -35,6 +35,8 @@
35
35
  * **In a sidebar** (`layout="row"`, `placement="top"`): the trigger is a full
36
36
  * width row, the picture with the name over the address, and the popup opens
37
37
  * upward from it. A product with a sidebar puts `<gp-apps>` at the row's end.
38
+ * `density="compact"` draws the popup a step smaller (320 wide, tighter rows),
39
+ * so it does not cover the rail and half the page.
38
40
  *
39
41
  * **The product's own items** (`items`) sit after the organisations, as a group
40
42
  * headed with the product's name. Until the product signs in through Identity,
@@ -67,6 +69,12 @@ export interface AccountOrganisation {
67
69
  * so the element has no list of its own. None, and the row has no second line.
68
70
  */
69
71
  role?: string | null;
72
+ /**
73
+ * The organisation's own picture, absolute or relative to the page, http or
74
+ * https only. Drawn in place of the initial, which comes back if it does not
75
+ * load.
76
+ */
77
+ iconUrl?: string | null;
70
78
  active: boolean;
71
79
  }
72
80
  /** One of the product's own items in the popup, such as its profile or its settings. */
@@ -36,6 +36,8 @@
36
36
  * **In a sidebar** (`layout="row"`, `placement="top"`): the trigger is a full
37
37
  * width row, the picture with the name over the address, and the popup opens
38
38
  * upward from it. A product with a sidebar puts `<gp-apps>` at the row's end.
39
+ * `density="compact"` draws the popup a step smaller (320 wide, tighter rows),
40
+ * so it does not cover the rail and half the page.
39
41
  *
40
42
  * **The product's own items** (`items`) sit after the organisations, as a group
41
43
  * headed with the product's name. Until the product signs in through Identity,
@@ -227,8 +229,9 @@ exports.ACCOUNT_CSS = `${contract_js_1.CONTRACT_CSS}
227
229
  .wrap > .radios:last-child > .org:last-child { border-radius: 4px 4px 18px 18px; }
228
230
  .org-mark {
229
231
  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;
232
+ overflow: hidden; color: ${tint_js_1.AVATAR_INK}; font-size: 14px; font-weight: 600;
231
233
  }
234
+ .org-mark img { display: block; width: 100%; height: 100%; object-fit: cover; }
232
235
  .org-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
233
236
  .org-name { font-size: 14px; line-height: 20px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
234
237
  .org-role { font-size: 12.5px; line-height: 17px; color: var(--_ink-2); }
@@ -258,6 +261,40 @@ exports.ACCOUNT_CSS = `${contract_js_1.CONTRACT_CSS}
258
261
  .legal a:hover { text-decoration: underline; }
259
262
  .legal a:focus-visible { outline: 2px solid var(--_ring); outline-offset: 2px; }
260
263
 
264
+ /* density="compact": the same popup a step smaller, for a sidebar, where the
265
+ full one covers the rail and half the page. Below 600 it is the same sheet. */
266
+ :host([density="compact"]) .popup:not(.sheet) { width: 320px; padding: 6px 6px 8px; border-radius: 22px; }
267
+ :host([density="compact"]) .top { min-height: 32px; padding-left: 36px; }
268
+ :host([density="compact"]) .context { font-size: 12px; line-height: 16px; }
269
+ :host([density="compact"]) .close { width: 32px; height: 32px; }
270
+ :host([density="compact"]) .card { padding: 14px 14px 14px; border-radius: 18px; gap: 1px; }
271
+ :host([density="compact"]) .portrait { width: 52px; height: 52px; margin-bottom: 8px; }
272
+ :host([density="compact"]) .s72 { width: 52px; height: 52px; font-size: 22px; }
273
+ :host([density="compact"]) .pencil { width: 24px; height: 24px; right: -5px; bottom: -3px; }
274
+ :host([density="compact"]) .name { font-size: 16px; line-height: 21px; }
275
+ :host([density="compact"]) .email { font-size: 13px; line-height: 18px; }
276
+ :host([density="compact"]) .pill {
277
+ min-height: 34px; margin-top: 10px; padding: 0 14px 0 10px; gap: 8px; font-size: 13px; line-height: 18px;
278
+ }
279
+ :host([density="compact"]) .pill img { width: 18px; height: 18px; }
280
+ :host([density="compact"]) .section { margin-top: 4px; }
281
+ :host([density="compact"]) .heading { padding: 6px 12px 2px; font-size: 12px; line-height: 16px; }
282
+ :host([density="compact"]) .heading img { width: 14px; height: 14px; }
283
+ :host([density="compact"]) .stack > :first-child { border-top-left-radius: 14px; border-top-right-radius: 14px; }
284
+ :host([density="compact"]) .stack > :last-child { border-bottom-left-radius: 14px; border-bottom-right-radius: 14px; }
285
+ :host([density="compact"]) .item { min-height: 38px; padding: 0 12px; gap: 10px; font-size: 13px; line-height: 18px; }
286
+ :host([density="compact"]) .item svg, :host([density="compact"]) .signout svg { width: 16px; height: 16px; }
287
+ :host([density="compact"]) .org { min-height: 44px; padding: 5px 12px; gap: 10px; }
288
+ :host([density="compact"]) .wrap > .radios:last-child > .org:last-child { border-radius: 4px 4px 14px 14px; }
289
+ :host([density="compact"]) .org-mark { width: 28px; height: 28px; border-radius: 8px; font-size: 12.5px; }
290
+ :host([density="compact"]) .org-name { font-size: 13px; line-height: 18px; }
291
+ :host([density="compact"]) .org-role { font-size: 11.5px; line-height: 15px; }
292
+ :host([density="compact"]) .active { font-size: 12px; gap: 5px; }
293
+ :host([density="compact"]) .more { min-height: 38px; padding: 0 12px; font-size: 13px; border-radius: 4px 4px 14px 14px; }
294
+ :host([density="compact"]) .signout { min-height: 40px; padding: 0 12px; gap: 10px; font-size: 13px; }
295
+ :host([density="compact"]) .error { padding: 4px 12px 2px; font-size: 12px; line-height: 17px; }
296
+ :host([density="compact"]) .legal { padding: 8px 0 2px; font-size: 11.5px; line-height: 16px; }
297
+
261
298
  @media (prefers-reduced-motion: reduce) {
262
299
  .popup, .popup.sheet { animation: none; }
263
300
  .spinner { animation-duration: 2400ms; }
@@ -316,6 +353,30 @@ function emailLine(email) {
316
353
  }
317
354
  return line;
318
355
  }
356
+ /** An organisation's picture where it has one that loads, else its initial on its tint. */
357
+ function orgMark(one) {
358
+ const initial = initialOf(one.name);
359
+ const mark = (0, dom_js_1.element)("span", {
360
+ class: `org-mark t${String((0, tint_js_1.avatarTint)(one.id))}`,
361
+ "aria-hidden": "true",
362
+ });
363
+ const picture = productAddress(one.iconUrl);
364
+ if (picture === null) {
365
+ mark.textContent = initial;
366
+ return mark;
367
+ }
368
+ const image = (0, dom_js_1.element)("img", {
369
+ src: picture,
370
+ alt: "",
371
+ decoding: "async",
372
+ referrerpolicy: "no-referrer",
373
+ });
374
+ image.addEventListener("error", () => {
375
+ mark.textContent = initial;
376
+ }, { once: true });
377
+ mark.append(image);
378
+ return mark;
379
+ }
319
380
  /** An address the product gave, absolute or relative to the page, and only ever http or https. */
320
381
  function productAddress(value) {
321
382
  if (typeof value !== "string" || value === "")
@@ -450,11 +511,17 @@ function organisationsOf(value) {
450
511
  for (const one of value) {
451
512
  if (typeof one !== "object" || one === null)
452
513
  continue;
453
- const { id, name, role, active } = one;
514
+ const { id, name, role, iconUrl, active } = one;
454
515
  if (typeof id !== "string" || id === "" || typeof name !== "string" || name.trim() === "")
455
516
  continue;
456
517
  const label = typeof role === "string" && role.trim() !== "" ? role.trim() : null;
457
- out.push({ id, name: name.trim(), role: label, active: active === true });
518
+ out.push({
519
+ id,
520
+ name: name.trim(),
521
+ role: label,
522
+ iconUrl: typeof iconUrl === "string" && iconUrl !== "" ? iconUrl : null,
523
+ active: active === true,
524
+ });
458
525
  }
459
526
  // The active one first: the row says which one you are acting in.
460
527
  const active = out.findIndex((one) => one.active);
@@ -1131,7 +1198,7 @@ function createAccountElementClass() {
1131
1198
  text.append((0, dom_js_1.element)("span", { class: "org-name", title: one.name }, one.name));
1132
1199
  if (one.role)
1133
1200
  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);
1201
+ row.append(orgMark(one), text);
1135
1202
  if (one.active) {
1136
1203
  const tag = (0, dom_js_1.element)("span", { class: "active", "aria-hidden": "true" });
1137
1204
  tag.append((0, dom_js_1.element)("span", { class: "check" }), labels.active);
package/dist/react.d.ts CHANGED
@@ -39,11 +39,13 @@ export interface GpAccountProps {
39
39
  labels?: Partial<AccountLabels>;
40
40
  /** `row` for the bottom of a sidebar: the picture with the name over the address, full width. */
41
41
  layout?: "button" | "row";
42
+ /** `compact` for a sidebar: the popup a step smaller. */
43
+ density?: "regular" | "compact";
42
44
  align?: Align;
43
45
  placement?: Placement;
44
46
  className?: string;
45
47
  }
46
48
  /** `<gp-account>`, the account popup. */
47
- export declare function GpAccount({ core, person, organisations, product, viaIdentity, onSignOut, onSwitch, items, labels, layout, align, placement, className, }: GpAccountProps): ReactElement;
49
+ export declare function GpAccount({ core, person, organisations, product, viaIdentity, onSignOut, onSwitch, items, labels, layout, density, align, placement, className, }: GpAccountProps): ReactElement;
48
50
  export {};
49
51
  //# sourceMappingURL=react.d.ts.map
package/dist/react.js CHANGED
@@ -31,7 +31,7 @@ function GpApps({ core, labels, align, placement, className }) {
31
31
  return (0, react_1.createElement)("gp-apps", { ref, className, align, placement });
32
32
  }
33
33
  /** `<gp-account>`, the account popup. */
34
- function GpAccount({ core, person, organisations = null, product = null, viaIdentity = false, onSignOut = null, onSwitch = null, items = null, labels, layout, align, placement, className, }) {
34
+ function GpAccount({ core, person, organisations = null, product = null, viaIdentity = false, onSignOut = null, onSwitch = null, items = null, labels, layout, density, align, placement, className, }) {
35
35
  const ref = (0, react_1.useRef)(null);
36
36
  (0, react_1.useEffect)(() => {
37
37
  (0, index_js_1.defineChromeElements)();
@@ -72,6 +72,6 @@ function GpAccount({ core, person, organisations = null, product = null, viaIden
72
72
  if (ref.current !== null && labels !== undefined)
73
73
  ref.current.labels = labels;
74
74
  }, [labels]);
75
- return (0, react_1.createElement)("gp-account", { ref, className, layout, align, placement });
75
+ return (0, react_1.createElement)("gp-account", { ref, className, layout, density, align, placement });
76
76
  }
77
77
  //# sourceMappingURL=react.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghub/identity-chrome",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "The shared header controls of the Gelhaus Solutions products: the apps picker and the account popup, as custom elements with shadow DOM and as React wrappers. The browser half (state, cache, the last list held for an outage) and the server half (the token calls and a route handler). No dependencies.",
5
5
  "author": "Gelhaus Solutions (Enno Gelhaus) <egelhaus@ennogelhaus.de> (https://ennogelhaus.de)",
6
6
  "homepage": "https://ennogelhaus.de",