@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.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # @ghub/identity-chrome
2
2
 
3
3
  The shared header controls of the Gelhaus Solutions products: the apps picker and the
4
- account popup. This version is the part with no markup in it: the browser half (what the
5
- controls know, what they keep, what they do when Identity cannot be reached) and the server
6
- half (a product's own server asking GPlatform Identity). The Web Component and the React
7
- wrapper come with the design.
4
+ account popup. Two custom elements with shadow DOM (`<gp-apps>`, `<gp-account>`) and React
5
+ wrappers for them; the browser half under both (what the controls know, what they keep, what
6
+ they do when Identity cannot be reached); and the server half (a product's own server asking
7
+ GPlatform Identity).
8
8
 
9
9
  Nothing here depends on a product signing in through Identity. A product that has not
10
10
  adopted it uses the same package the same way.
@@ -62,6 +62,101 @@ A one-off push of a product's users (`chrome.seed(emails)`) is refused unless st
62
62
  opened a window for it on Identity's product screen, and `chrome.remove(emails)` says people
63
63
  no longer have access.
64
64
 
65
+ ## The two controls
66
+
67
+ ```ts
68
+ import { createChromeCore, defineChromeElements } from "@ghub/identity-chrome";
69
+
70
+ defineChromeElements(); // <gp-apps> and <gp-account>, each once; a no-op on a server
71
+
72
+ const apps = document.querySelector("gp-apps")!;
73
+ apps.core = chrome; // the core above: one fetch and one cache behind both
74
+
75
+ const account = document.querySelector("gp-account")!;
76
+ account.core = chrome;
77
+ account.product = "GAdvisory"; // your own name, from your catalogue entry
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 }];
80
+ account.onSwitch = async (id) => switchOrganisation(id); // reject when it did not switch
81
+ account.onSignOut = async () => {
82
+ await signOut(); // reject when it failed: the popup says they are still signed in
83
+ location.assign("/"); // you navigate; as this resolves the element forgets the kept list
84
+ };
85
+ ```
86
+
87
+ Everything is a property, because a core, a person and a callback are objects. Each can be
88
+ set before the element is defined. Neither control renders anything while the core is off,
89
+ and the account popup renders nothing without a person.
90
+
91
+ **The account popup shows mostly what the product passes**: the name and address from your
92
+ 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.
94
+ Identity adds the picture, the colour of the initial, the links to its own account and set up
95
+ screens and the footer, so the popup still signs somebody out while Identity is down. The
96
+ picture is Identity's if the person has one, else yours, else a coloured initial whose tint
97
+ Identity picks from its own id; for somebody with no Identity, pass your own stable user id as
98
+ `id` and the same hash (`avatarTint`) picks it. Set `viaIdentity` once you sign in through
99
+ Identity, and Sign out says it ends both. Sign out leaves this product only.
100
+
101
+ Every word can be replaced through `labels`; the rest stay English.
102
+
103
+ ### At the bottom of a sidebar, with your own items
104
+
105
+ ```html
106
+ <div style="display: flex; align-items: center">
107
+ <gp-account layout="row" placement="top" align="start" style="flex: 1"></gp-account>
108
+ <gp-apps placement="top" align="start"></gp-apps>
109
+ </div>
110
+ ```
111
+
112
+ ```ts
113
+ account.labels = { organisations: "Groups" };
114
+ account.items = [
115
+ { id: "profile", label: "Profile", href: "/settings/profile", icon: userSvg },
116
+ { id: "account", label: "Account", href: "/settings/account", icon: settingsSvg },
117
+ { id: "replay", label: "Session recording", onSelect: () => openReplayChoice(), icon: videoSvg },
118
+ ];
119
+ ```
120
+
121
+ `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
124
+ `preventDefault()` and navigate with your own router. An icon is SVG markup on a 24 grid and
125
+ is kept to its shapes. Until you sign in through Identity, Sign out is the last row of your
126
+ items' group; with `viaIdentity` it ends both sessions and stands on its own.
127
+
128
+ ### In React
129
+
130
+ ```tsx
131
+ import { GpAccount, GpApps } from "@ghub/identity-chrome/react";
132
+
133
+ <GpApps core={chrome} />
134
+ <GpAccount core={chrome} product="GAdvisory" person={person} organisations={orgs} onSwitch={switchTo} onSignOut={signOut} />
135
+ ```
136
+
137
+ The wrappers define the elements and set the properties after mounting, so they render on a
138
+ server as the bare tags. React 19 or newer, as an optional peer.
139
+
140
+ ### The host contract
141
+
142
+ Shadow DOM: nothing reaches in and nothing leaks out but the custom properties a page
143
+ declares on its root, which inherit into the shadow. The controls read the published
144
+ `@ghub/tokens` names, so a page on `@ghub/tokens` is styled with no setup, and Aurora's frosted
145
+ menu where the page is on Aurora:
146
+
147
+ | Reads | For |
148
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
149
+ | `--ga-menu`, else `--gm-overlay`; `--ga-frost-menu`; `--ga-shadow-menu`, else `--gm-shadow-overlay` | the picker's surface, its frost and its shadow |
150
+ | `--ga-menu`, else `--gm-sunken` | the account popup's tinted ground |
151
+ | `--ga-track`, else `--gm-overlay` | the account popup's panels |
152
+ | `--gm-plate`, `--gm-ink`, `--gm-ink-2`, `--gm-ink-3`, `--gm-hairline`, `--gm-control`, `--gm-hover`, `--gm-active`, `--gm-selected`, `--gm-accent`, `--gm-accent-text`, `--gm-ring`, `--gm-crit-ink` | everything else |
153
+ | `--font-sans` | the face |
154
+
155
+ A page that declares none of them gets modern light, or modern dark under
156
+ `prefers-color-scheme: dark`. There is no second accent: the controls take the page's. Both
157
+ follow reduced motion, reduced transparency and forced colours, and below 600 wide each is a
158
+ modal bottom sheet. `part="trigger"` and `part="popup"` are there for placement only.
159
+
65
160
  ## A product's own name and mark
66
161
 
67
162
  Identity's catalogue is where every product's name and mark live. Read your own from it for
package/dist/core.js CHANGED
@@ -29,7 +29,7 @@ exports.CACHE_KEY = "gpid.chrome.v1";
29
29
  /** A list older than this is not drawn, however the fetch fares. */
30
30
  exports.CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
31
31
  exports.DEFAULT_TIMEOUT_MS = 5000;
32
- const NO_PERSON = { hasIdentity: false, avatarUrl: null };
32
+ const NO_PERSON = { hasIdentity: false, avatarUrl: null, tint: null };
33
33
  function isRecord(value) {
34
34
  return typeof value === "object" && value !== null && !Array.isArray(value);
35
35
  }
@@ -157,7 +157,15 @@ function createChromeCore(options = {}) {
157
157
  void publicCall
158
158
  .then((pub) => {
159
159
  if (pub !== null && state.status === "loading" && state.view === null) {
160
- set({ view: { person: NO_PERSON, yours: [], more: pub.more, legal: pub.legal } });
160
+ set({
161
+ view: {
162
+ person: NO_PERSON,
163
+ yours: [],
164
+ identity: null,
165
+ more: pub.more,
166
+ legal: pub.legal,
167
+ },
168
+ });
161
169
  }
162
170
  })
163
171
  .catch(() => undefined);
@@ -173,7 +181,7 @@ function createChromeCore(options = {}) {
173
181
  // No personal route configured: the public group is the whole answer.
174
182
  set({
175
183
  status: "ready",
176
- view: { person: NO_PERSON, yours: [], ...publicHalf },
184
+ view: { person: NO_PERSON, yours: [], identity: null, ...publicHalf },
177
185
  updatedAt: now(),
178
186
  error: null,
179
187
  });
@@ -186,7 +194,7 @@ function createChromeCore(options = {}) {
186
194
  else if (publicHalf !== null) {
187
195
  set({
188
196
  status: "error",
189
- view: { person: NO_PERSON, yours: [], ...publicHalf },
197
+ view: { person: NO_PERSON, yours: [], identity: null, ...publicHalf },
190
198
  updatedAt: null,
191
199
  error,
192
200
  });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * `<gp-account>`, the account popup (GPlatform Identity, sheet 32, option A).
3
+ *
4
+ * An avatar at the right end of a product's top bar and, under it, a card on a
5
+ * tinted ground: who is signed in, where their Identity account is, which
6
+ * organisation they are acting in, and how to sign out of this product and
7
+ * only this product (D21).
8
+ *
9
+ * **Most of what it shows is the product's.** The name, the address, the
10
+ * organisations and both actions are properties the product sets from its own
11
+ * session. Identity adds the picture, the tint, its own two links and the
12
+ * footer, through the same core as `<gp-apps>`, so the popup still signs
13
+ * somebody out while Identity cannot be reached.
14
+ *
15
+ * **What it is to assistive technology:**
16
+ *
17
+ * - The trigger is a `button` with `aria-haspopup="dialog"`, named "Account:
18
+ * Mira Kessler", with the same words as a tooltip on hover and on focus after
19
+ * half a second, which Escape dismisses without closing anything else (1.4.13).
20
+ * - The popup is a `dialog` labelled by the person's name and described by their
21
+ * address, not modal. Below 600 wide it is a modal sheet with a scrim and a
22
+ * contained focus loop, as the picker is.
23
+ * - Focus opens on the pill (Manage your Identity account, or Set up your
24
+ * Identity), else the active organisation, else Sign out. The close button is
25
+ * a stop and never the first one.
26
+ * - The organisations are a `radiogroup`, one tab stop, Up and Down moving
27
+ * between them. Choosing is Space or Enter and never an arrow, because
28
+ * choosing switches the product: it closes the popup and announces
29
+ * "Switched to ...". Five show; "Show all" opens the rest in place, scrolling
30
+ * inside, and focus lands on the row that took its place.
31
+ * - Signing out goes busy in place. A failure keeps the popup open, says in one
32
+ * sentence that the person is still signed in, and turns the row into Try
33
+ * again. It never claims to have signed out.
34
+ *
35
+ * **In a sidebar** (`layout="row"`, `placement="top"`): the trigger is a full
36
+ * width row, the picture with the name over the address, and the popup opens
37
+ * upward from it. A product with a sidebar puts `<gp-apps>` at the row's end.
38
+ *
39
+ * **The product's own items** (`items`) sit after the organisations, as a group
40
+ * headed with the product's name. Until the product signs in through Identity,
41
+ * Sign out is the last row of that same group, because it is one of the
42
+ * product's own actions; once it does (`viaIdentity`), signing out ends both
43
+ * sessions and stands on its own again.
44
+ *
45
+ * **Hidden for now (D20):** the other accounts and "Add another account" are
46
+ * not drawn until Identity holds several sessions in one browser and products
47
+ * sign in through it, and no product can switch them on.
48
+ *
49
+ * The words are English by default and every one can be replaced (`labels`).
50
+ */
51
+ /** Who is signed in, from the product's own session. */
52
+ export interface AccountPerson {
53
+ name: string;
54
+ email: string;
55
+ /** The product's own stable id for this person: it picks the tint where they have no Identity. Never shown. */
56
+ id?: string;
57
+ /** The product's own picture, drawn where Identity has none. */
58
+ avatarUrl?: string | null;
59
+ }
60
+ /** An organisation the person can act in, as the product or Identity passes it (D24, D33). */
61
+ export interface AccountOrganisation {
62
+ id: string;
63
+ name: string;
64
+ /**
65
+ * The product's own role for this person here, already in the person's
66
+ * language and shown as given. Roles differ per product and can be custom,
67
+ * so the element has no list of its own. None, and the row has no second line.
68
+ */
69
+ role?: string | null;
70
+ active: boolean;
71
+ }
72
+ /** One of the product's own items in the popup, such as its profile or its settings. */
73
+ export interface AccountItem {
74
+ /** Stable across renders, so focus stays on the item when the list is set again. */
75
+ id: string;
76
+ label: string;
77
+ /** A link, so middle click and opening in a new tab work as on any link. Relative to the page. */
78
+ href?: string;
79
+ /**
80
+ * Called when it is chosen. On a link, `event.preventDefault()` hands the
81
+ * navigation to the product, such as a client side router.
82
+ */
83
+ onSelect?: (event: MouseEvent) => void;
84
+ /** SVG markup on a 24 grid, drawn at 18 in the row's ink. Only shapes are kept. */
85
+ icon?: string;
86
+ }
87
+ /** Signs the person out of this product. Rejecting means they are still signed in. */
88
+ export type SignOutHandler = () => unknown;
89
+ /** Switches the product to this organisation. Rejecting means it did not. */
90
+ export type SwitchHandler = (organisationId: string) => unknown;
91
+ export interface AccountLabels {
92
+ /** The trigger's name and its tooltip. */
93
+ trigger: (name: string) => string;
94
+ signedInTo: (product: string) => string;
95
+ close: string;
96
+ changePicture: string;
97
+ manage: string;
98
+ setUp: string;
99
+ organisations: string;
100
+ active: string;
101
+ showAll: (count: number) => string;
102
+ switched: (organisation: string) => string;
103
+ switchFailed: (organisation: string) => string;
104
+ signOut: (product: string) => string;
105
+ /** In a product that signs in through Identity, where signing out ends both. */
106
+ signOutBoth: (product: string) => string;
107
+ signingOut: (product: string) => string;
108
+ signOutFailed: (product: string) => string;
109
+ retry: string;
110
+ /** Stands in for the product's name where it was not given. */
111
+ thisProduct: string;
112
+ terms: string;
113
+ privacy: string;
114
+ }
115
+ export declare const ACCOUNT_LABELS: AccountLabels;
116
+ export declare const ACCOUNT_CSS: string;
117
+ /**
118
+ * A product's icon, kept to shapes: no script, no link, no style and no reference
119
+ * to anything outside it, because it is drawn inside every page the element is on.
120
+ */
121
+ export declare function iconOf(markup: string): SVGSVGElement | null;
122
+ /**
123
+ * Defines `<gp-account>` once, where custom elements exist. Safe to import on a
124
+ * server: nothing touches the DOM until this is called in a browser.
125
+ */
126
+ export declare function defineAccountElement(name?: string): void;
127
+ /** The class itself, for a caller that registers it under its own name or in a scoped registry. */
128
+ export declare function createAccountElementClass(): CustomElementConstructor;
129
+ //# sourceMappingURL=account.d.ts.map