@blackforestsentinel/seed-web-people 0.0.0-stage → 0.4.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 +155 -2
- package/dist/cache.d.ts +25 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +116 -0
- package/dist/cache.js.map +1 -0
- package/dist/components/AccessDenied.d.ts +53 -0
- package/dist/components/AccessDenied.d.ts.map +1 -0
- package/dist/components/AccessDenied.js +41 -0
- package/dist/components/AccessDenied.js.map +1 -0
- package/dist/components/Avatar.d.ts +46 -0
- package/dist/components/Avatar.d.ts.map +1 -0
- package/dist/components/Avatar.js +52 -0
- package/dist/components/Avatar.js.map +1 -0
- package/dist/components/AvatarGroup.d.ts +23 -0
- package/dist/components/AvatarGroup.d.ts.map +1 -0
- package/dist/components/AvatarGroup.js +18 -0
- package/dist/components/AvatarGroup.js.map +1 -0
- package/dist/components/CurrentUserMenu.d.ts +51 -0
- package/dist/components/CurrentUserMenu.d.ts.map +1 -0
- package/dist/components/CurrentUserMenu.js +123 -0
- package/dist/components/CurrentUserMenu.js.map +1 -0
- package/dist/components/PeoplePicker.d.ts +69 -0
- package/dist/components/PeoplePicker.d.ts.map +1 -0
- package/dist/components/PeoplePicker.js +196 -0
- package/dist/components/PeoplePicker.js.map +1 -0
- package/dist/components/PersonCard.d.ts +35 -0
- package/dist/components/PersonCard.d.ts.map +1 -0
- package/dist/components/PersonCard.js +27 -0
- package/dist/components/PersonCard.js.map +1 -0
- package/dist/components/PersonLabel.d.ts +31 -0
- package/dist/components/PersonLabel.d.ts.map +1 -0
- package/dist/components/PersonLabel.js +76 -0
- package/dist/components/PersonLabel.js.map +1 -0
- package/dist/components/PresenceBadge.d.ts +17 -0
- package/dist/components/PresenceBadge.d.ts.map +1 -0
- package/dist/components/PresenceBadge.js +30 -0
- package/dist/components/PresenceBadge.js.map +1 -0
- package/dist/components/RoleBadge.d.ts +13 -0
- package/dist/components/RoleBadge.d.ts.map +1 -0
- package/dist/components/RoleBadge.js +10 -0
- package/dist/components/RoleBadge.js.map +1 -0
- package/dist/components/SignInPage.d.ts +31 -0
- package/dist/components/SignInPage.d.ts.map +1 -0
- package/dist/components/SignInPage.js +29 -0
- package/dist/components/SignInPage.js.map +1 -0
- package/dist/components/icons.d.ts +12 -0
- package/dist/components/icons.d.ts.map +1 -0
- package/dist/components/icons.js +26 -0
- package/dist/components/icons.js.map +1 -0
- package/dist/directory.d.ts +59 -0
- package/dist/directory.d.ts.map +1 -0
- package/dist/directory.js +94 -0
- package/dist/directory.js.map +1 -0
- package/dist/graph-client.d.ts +40 -0
- package/dist/graph-client.d.ts.map +1 -0
- package/dist/graph-client.js +188 -0
- package/dist/graph-client.js.map +1 -0
- package/dist/graph-directory.d.ts +53 -0
- package/dist/graph-directory.d.ts.map +1 -0
- package/dist/graph-directory.js +368 -0
- package/dist/graph-directory.js.map +1 -0
- package/dist/hooks.d.ts +83 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +140 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +38 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +58 -0
- package/dist/provider.js.map +1 -0
- package/dist/styles.css +1484 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +22 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +104 -0
- package/dist/utils.js.map +1 -0
- package/package.json +55 -4
package/README.md
CHANGED
|
@@ -1,3 +1,156 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @blackforestsentinel/seed-web-people
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React-Komponenten rund um Personen für Frontends aus [Sentinel Seed](https://github.com/blackforestsentinel/seed-template): Profilbild, Name, Rolle, Abmelden, Personenauswahl, Karten, Anmelde- und Kein-Zugriff-Seite. Die Daten kommen aus Microsoft Graph, mit dem Token der angemeldeten Person direkt im Browser; einen eigenen Server-Endpunkt braucht es nicht.
|
|
4
|
+
|
|
5
|
+
- Optik an Fluent 2 angelehnt (Teams, Outlook), ohne Abhängigkeit von `@fluentui/*`.
|
|
6
|
+
- Ein statisches Stylesheet, keine eingefügten Styles; passt zur CSP der Seed-Apps (`style-src 'self'`).
|
|
7
|
+
- Bilder als `data:`-URL; die CSP erlaubt `img-src 'self' data:` schon.
|
|
8
|
+
- Rund 14 KB JavaScript und 6 KB CSS nach gzip, ohne React. Einzelne Komponenten lassen sich per Tree-Shaking einbinden.
|
|
9
|
+
|
|
10
|
+
## Einbau
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @blackforestsentinel/seed-web-people
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Die Version passt zu `@blackforestsentinel/seed-web-auth` (Peer-Abhängigkeit, ebenso React ab 18).
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
// main.tsx
|
|
20
|
+
import '@blackforestsentinel/seed-web-people/styles.css';
|
|
21
|
+
import { SeedUserProvider } from '@blackforestsentinel/seed-web-auth/react';
|
|
22
|
+
import { DirectoryProvider, SignInPage } from '@blackforestsentinel/seed-web-people';
|
|
23
|
+
|
|
24
|
+
const config = await loadRuntimeConfig<AppConfig>();
|
|
25
|
+
const auth = await startAuth(config.auth); // createSeedAuth oder null (lokal, ohne sso)
|
|
26
|
+
|
|
27
|
+
if (auth && !auth.account) {
|
|
28
|
+
root.render(
|
|
29
|
+
<DirectoryProvider config={config.auth} auth={auth}>
|
|
30
|
+
<SignInPage appName="Kundenportal" />
|
|
31
|
+
</DirectoryProvider>,
|
|
32
|
+
);
|
|
33
|
+
} else {
|
|
34
|
+
root.render(
|
|
35
|
+
<SeedUserProvider load={api.getMe}>
|
|
36
|
+
<DirectoryProvider config={config.auth} auth={auth}>
|
|
37
|
+
<App />
|
|
38
|
+
</DirectoryProvider>
|
|
39
|
+
</SeedUserProvider>,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `DirectoryProvider` entscheidet anhand von `config.auth`: mit `auth.directory` und Anmeldung Graph, im lokalen Modus die Liste aus `config.json`, sonst gar nichts. Ohne `auth.directory` zeigen die Komponenten Namen und Initialen, die die App übergibt, und rufen Graph nie auf.
|
|
45
|
+
- `DirectoryProvider` gehört in `SeedUserProvider`, damit Rollen und Capabilities aus `/api/me` ankommen.
|
|
46
|
+
- Die CSP der App braucht `connect-src https://graph.microsoft.com`; das Template trägt das mit `auth.directory` ein.
|
|
47
|
+
|
|
48
|
+
## Komponenten
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<CurrentUserMenu menuItems={[{ label: 'Hilfe', href: '/hilfe' }]} />
|
|
52
|
+
<PersonLabel id={invoice.approvedBy} secondary={formatDate(invoice.approvedAt)} card />
|
|
53
|
+
<PeoplePicker multiple value={reviewers} onChange={setReviewers} onlyWithAppRole roles={['Admin']} />
|
|
54
|
+
<AvatarGroup people={task.participants} max={4} />
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| Komponente | Zeigt | Wichtige Props |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `Avatar` | Foto; ohne Foto Initialen auf einer Farbe aus der ID; Platzhalter beim Laden | `id`, `name`, `image`, `size` (16 bis 96, Default 32), `shape` (`circular`, `square`), `presence` (`true` oder fester Status), `color`, `decorative` |
|
|
60
|
+
| `PersonLabel` | Avatar, Name, optional zweite Zeile; mit `card` die `PersonCard` beim Überfahren und per Tastatur | `id`, `name`, `person`, `secondary`, `secondaryField` (`mail`, `jobTitle`, `department`), `size`, `presence`, `card` |
|
|
61
|
+
| `PersonCard` | größeres Bild, Name, Position, Abteilung, Status, Mail, Link zum Teams-Chat | `id`, `person`, `name`, `presence`, `chat`, `actions` |
|
|
62
|
+
| `AvatarGroup` | mehrere Avatare überlappend, ab `max` „+n“ mit den übrigen Namen als Titel | `people` (IDs oder `{ id, name }`), `max` (Default 4), `size` |
|
|
63
|
+
| `CurrentUserMenu` | Avatar, Name, Rolle (Anzeigename aus `/api/me`), Abmelden; mit `menuItems` ein Menü | `menuItems`, `renderTrigger`, `showRole`, `presence`, `onSignOut` (`null` blendet aus) |
|
|
64
|
+
| `PeoplePicker` | Suchfeld mit Vorschlägen, eine oder mehrere Personen als Chips | `value`/`defaultValue`, `onChange`, `multiple`, `filter`, `renderOption`, `onlyWithAppRole`, `roles`, `minChars` (2), `debounceMs` (250), `maxSuggestions` (8), `labels` |
|
|
65
|
+
| `SignInPage` | Anmeldeseite mit App-Name und Knopf | `appName`, `description`, `logo`, `onSignIn` (Default `auth.login()`), `error`, `footer` |
|
|
66
|
+
| `AccessDenied` | angemeldet, aber ohne Rolle oder Capability: Erklärung, Ansprechperson, Kontowechsel | `appName`, `reason` (`no-role`, `missing-capability`), `capability`, `contact`, `actions`, `onSignOut` |
|
|
67
|
+
| `RoleBadge` | Rolle als Etikett, Anzeigename aus `/api/me` | `role`, `displayName`, `appearance` (`tint`, `filled`, `outline`) |
|
|
68
|
+
| `PresenceBadge` | Teams-Status als Punkt | `status`, `size`, `label` |
|
|
69
|
+
|
|
70
|
+
### PeoplePicker
|
|
71
|
+
|
|
72
|
+
- ARIA-Muster „Combobox“: Pfeiltasten wählen den Vorschlag, Enter übernimmt ihn, Escape schließt die Liste und leert dann das Feld, Rücktaste im leeren Feld entfernt den letzten Chip.
|
|
73
|
+
- Suche ab zwei Zeichen mit 250 ms Verzögerung. Zuerst die für die Person relevanten Kontakte (`/me/people`), dann das Verzeichnis (`/users?$search`), beides in einem `$batch`.
|
|
74
|
+
- `filter` blendet Vorschläge aus, `renderOption` stellt sie selbst dar.
|
|
75
|
+
- **`onlyWithAppRole`** zeigt nur Personen mit einer App-Rolle der API, mit `roles` nur mit diesen Rollen. Das Paket liest die Zuweisungen der API-Registrierung (`/servicePrincipals(appId='…')/appRoleAssignedTo`) und löst Gruppen samt verschachtelter Gruppen auf (`/groups/{id}/transitiveMembers`). Das Ergebnis bleibt zehn Minuten im Zwischenspeicher.
|
|
76
|
+
- Ohne `auth.directory.roleFilter` zeigt der Picker mit `onlyWithAppRole` niemanden, sondern einen Fehler im Feld (`data-state="error"`) und in der Konsole. Im lokalen Modus filtert er nach `roles` der erfundenen Personen.
|
|
77
|
+
|
|
78
|
+
## Hooks
|
|
79
|
+
|
|
80
|
+
Für eigene Oberflächen gibt es nur die Daten:
|
|
81
|
+
|
|
82
|
+
| Hook | Liefert |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `useCurrentUser()` | `{ status, user }`; `user` mit `id`, `displayName`, `username`, `roles`, `roleNames`, `capabilities`. Der Name kommt sofort aus der Anmeldung, Rollen aus `/api/me`. |
|
|
85
|
+
| `usePerson(id)` | `{ status, person }` mit Name, Mail, Position, Abteilung |
|
|
86
|
+
| `usePhoto(id, size)` | `{ status, url }` als `data:`-URL oder `null` |
|
|
87
|
+
| `usePeopleSearch(text, options)` | `{ status, people, query }`, mit Verzögerung und Mindestlänge |
|
|
88
|
+
| `usePresence(id)` | `{ status, presence }`, gebündelt und jede Minute aktualisiert, solange sichtbar |
|
|
89
|
+
| `useAppRoleMembers(roles)` | `{ status, members, ids }`, Personen mit App-Rolle |
|
|
90
|
+
| `useDirectory()`, `useSignOut()` | das Verzeichnis; Abmelden samt Leeren der Zwischenspeicher |
|
|
91
|
+
|
|
92
|
+
`status` ist `idle` (nichts zu laden), `loading`, `ready` oder `error`. Ohne React: `createGraphDirectory()` und `createLocalDirectory()`.
|
|
93
|
+
|
|
94
|
+
## Anpassen
|
|
95
|
+
|
|
96
|
+
- **CSS-Variablen** für alles Sichtbare, an einer Stelle gesetzt:
|
|
97
|
+
```css
|
|
98
|
+
:root {
|
|
99
|
+
--seed-accent: #8a2be2;
|
|
100
|
+
--seed-font: Inter, sans-serif;
|
|
101
|
+
--seed-avatar-radius: 8px;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
Die wichtigsten: `--seed-font`, `--seed-accent` (mit `-hover`, `-pressed`), `--seed-text` (mit `-secondary`, `-tertiary`), `--seed-surface`, `--seed-stroke`, `--seed-focus`, `--seed-radius-*`, `--seed-shadow-*`, `--seed-presence-*`, `--seed-palette-<farbe>-bg`/`-fg`. Die Werte entsprechen den Tokens von Fluent 2 (Brand `#0F6CBD`).
|
|
105
|
+
- **`@layer seed`:** Alle Standard-Styles liegen in dieser Ebene. Jede Regel der App gewinnt ohne `!important`, auch eine mit geringerer Spezifität.
|
|
106
|
+
- **Feste Klassennamen** nach BEM, etwa `seed-avatar`, `seed-avatar__image`, `seed-picker__option`, `seed-user-menu__role`. Zustände als `data-*`: `data-state="loading"`, `data-active`, `data-open`, `data-presence="busy"`, `data-color="cranberry"`.
|
|
107
|
+
- **`className` und `classNames`:** Jede Komponente nimmt `className` für das Wurzelelement und `classNames` für ihre Teile, z. B. `classNames={{ option: 'px-2 py-1' }}`; das passt auch zu Tailwind.
|
|
108
|
+
- **Eigene Darstellung:** `renderOption` im Picker, `renderTrigger` und `menuItems` im Benutzermenü, die Hooks für alles andere.
|
|
109
|
+
- **Hell und Dunkel** folgen `prefers-color-scheme`. `data-seed-theme="light"` oder `"dark"` am `html`-Element legt es fest, etwa für einen eigenen Umschalter.
|
|
110
|
+
- **Bedienbarkeit:** Picker als Combobox, Benutzermenü als Menu Button, Bilder mit Alternativtext über `role="img"` und `aria-label`, alles mit der Tastatur bedienbar, Fokusrahmen wie Fluent, `forced-colors` berücksichtigt.
|
|
111
|
+
|
|
112
|
+
## Berechtigungen und Einwilligung
|
|
113
|
+
|
|
114
|
+
Alle Berechtigungen sind delegiert: Das Frontend sieht nur, was die angemeldete Person in Outlook oder Teams ohnehin sieht. Das Terraform-Modul `sso` trägt sie an der SPA-Registrierung ein; einwilligen muss ein Mensch.
|
|
115
|
+
|
|
116
|
+
| Berechtigung | Wofür | Admin-Consent nötig |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `User.Read` | eigenes Profil | nein |
|
|
119
|
+
| `User.ReadBasic.All` | Profile und Fotos anderer, Verzeichnissuche | nein |
|
|
120
|
+
| `People.Read` | relevante Personen im Picker | nein |
|
|
121
|
+
| `Presence.Read.All` | Teams-Status (`presence: true`) | nein |
|
|
122
|
+
| `Application.Read.All` | Rollenzuweisungen der API (`roleFilter: true`) | **ja** |
|
|
123
|
+
| `GroupMember.Read.All` | Mitglieder zugewiesener Gruppen (`roleFilter: true`) | **ja** |
|
|
124
|
+
|
|
125
|
+
- Viele Tenants erlauben keine eigene Einwilligung der Personen. Dann erteilt ein Admin sie einmal für alle: Entra ID, App-Registrierungen, die SPA-Registrierung, „API-Berechtigungen“, „Administratorzustimmung erteilen“.
|
|
126
|
+
- Das Paket holt ein Token für alle Scopes aus `auth.directory.scopes` zugleich. Fehlt die Einwilligung für einen davon, bleibt Graph ganz aus. Mit `roleFilter: true` daher erst die Administratorzustimmung erteilen, dann ausrollen.
|
|
127
|
+
- Fehlt die Einwilligung, leitet seed-web-auth je Tab einmal ohne und einmal mit Dialog zu Entra ID um, damit die Person zustimmen kann. Danach läuft die App ohne Graph weiter: Initialen statt Fotos, der Picker meldet einen Fehler, in der Konsole steht ein Hinweis.
|
|
128
|
+
- Lehnt Graph Position und Abteilung ab (403), fragt das Paket nur Name und Mail ab.
|
|
129
|
+
|
|
130
|
+
## Lokaler Modus
|
|
131
|
+
|
|
132
|
+
Mit `"auth": { "mode": "local" }` gibt es kein MSAL und keinen Graph-Zugriff. Unter `auth.directory.people` stehen erfundene Personen; Avatare zeigen Initialen, der Picker sucht in dieser Liste, einen Status gibt es nicht.
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"apiBaseUrl": "http://localhost:7071",
|
|
137
|
+
"auth": {
|
|
138
|
+
"mode": "local",
|
|
139
|
+
"directory": {
|
|
140
|
+
"people": [
|
|
141
|
+
{ "id": "00000000-0000-4000-8000-000000000001", "displayName": "Erika Mustermann", "mail": "erika.mustermann@example.org", "jobTitle": "Buchhalterin", "department": "Buchhaltung", "roles": ["Admin"] }
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`roles` ist optional und nur für `onlyWithAppRole` im lokalen Modus. Ohne `auth.directory` sucht der Picker nicht und sagt das.
|
|
149
|
+
|
|
150
|
+
## Laufzeit und Datenschutz
|
|
151
|
+
|
|
152
|
+
- Bilder lädt das Paket nur, wenn eine Komponente sie zeigt, in festen Größen (48, 64 oder 96 Pixel aus `/photos/{size}/$value`).
|
|
153
|
+
- Anfragen innerhalb weniger Millisekunden gehen gemeinsam als `$batch` (bis 20 je Aufruf). Gleiche laufende Anfragen werden zusammengefasst, gedrosselte (429) nach `Retry-After` wiederholt.
|
|
154
|
+
- Der Teams-Status kommt gebündelt über `/communications/getPresencesByUserId` und wird jede Minute neu geladen, solange er sichtbar ist und der Tab im Vordergrund liegt.
|
|
155
|
+
- Zwischenspeicher im Speicher und im `sessionStorage` (endet mit dem Tab), getrennt je Konto: Profile und Fotos 30 Minuten, Rollenzuweisungen 10 Minuten, Suchergebnisse und Status nur im Speicher. Personen ohne Foto merkt er sich auch. Abmelden über die Komponenten leert alles.
|
|
156
|
+
- Fotos und Profile gehen nie an die eigene API und nicht in Logs. Profilbilder sind personenbezogene Daten; sie sind aber für dieselben Personen sichtbar wie in Teams und Outlook, und die Einstellungen des Tenants (etwa ausgeblendete Fotos) gelten weiter.
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zwischenspeicher mit Lebensdauer im Speicher und optional im sessionStorage, damit ein Neuladen
|
|
3
|
+
* der Seite nicht alles neu anfragt. Der sessionStorage endet mit dem Tab. Auch „gibt es nicht“
|
|
4
|
+
* (null) wird gemerkt, etwa für Personen ohne Foto.
|
|
5
|
+
*/
|
|
6
|
+
export declare class TtlCache {
|
|
7
|
+
private readonly memory;
|
|
8
|
+
private readonly prefix;
|
|
9
|
+
private readonly storage;
|
|
10
|
+
private readonly now;
|
|
11
|
+
constructor(prefix: string, storage: Storage | null, now?: () => number);
|
|
12
|
+
/** Wert oder undefined, wenn unbekannt oder abgelaufen. */
|
|
13
|
+
get<T>(key: string): T | undefined;
|
|
14
|
+
set(key: string, value: unknown, ttl: number, persist?: boolean): void;
|
|
15
|
+
delete(key: string): void;
|
|
16
|
+
/** Leert Speicher und alle eigenen Einträge im sessionStorage, etwa beim Abmelden. */
|
|
17
|
+
clear(): void;
|
|
18
|
+
private load;
|
|
19
|
+
private write;
|
|
20
|
+
private prune;
|
|
21
|
+
private removeWhere;
|
|
22
|
+
}
|
|
23
|
+
/** sessionStorage, falls verfügbar; in privaten Fenstern oder per Richtlinie kann er fehlen. */
|
|
24
|
+
export declare function defaultStorage(): Storage | null;
|
|
25
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC,YAAY,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,GAAE,MAAM,MAAiB,EAIhF;IAED,2DAA2D;IAC3D,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAUjC;IAED,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,UAAO,QAW3D;IAED,MAAM,CAAC,GAAG,EAAE,MAAM,QAOjB;IAED,sFAAsF;IACtF,KAAK,SAGJ;IAED,OAAO,CAAC,IAAI;IAeZ,OAAO,CAAC,KAAK;IASb,OAAO,CAAC,KAAK;IAWb,OAAO,CAAC,WAAW;CAgBpB;AAED,gGAAgG;AAChG,wBAAgB,cAAc,IAAI,OAAO,GAAG,IAAI,CAM/C"}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zwischenspeicher mit Lebensdauer im Speicher und optional im sessionStorage, damit ein Neuladen
|
|
3
|
+
* der Seite nicht alles neu anfragt. Der sessionStorage endet mit dem Tab. Auch „gibt es nicht“
|
|
4
|
+
* (null) wird gemerkt, etwa für Personen ohne Foto.
|
|
5
|
+
*/
|
|
6
|
+
export class TtlCache {
|
|
7
|
+
memory = new Map();
|
|
8
|
+
prefix;
|
|
9
|
+
storage;
|
|
10
|
+
now;
|
|
11
|
+
constructor(prefix, storage, now = Date.now) {
|
|
12
|
+
this.prefix = prefix;
|
|
13
|
+
this.storage = storage;
|
|
14
|
+
this.now = now;
|
|
15
|
+
}
|
|
16
|
+
/** Wert oder undefined, wenn unbekannt oder abgelaufen. */
|
|
17
|
+
get(key) {
|
|
18
|
+
const entry = this.memory.get(key) ?? this.load(key);
|
|
19
|
+
if (!entry) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
if (entry.expires <= this.now()) {
|
|
23
|
+
this.delete(key);
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return entry.value;
|
|
27
|
+
}
|
|
28
|
+
set(key, value, ttl, persist = true) {
|
|
29
|
+
const entry = { value, expires: this.now() + ttl };
|
|
30
|
+
this.memory.set(key, entry);
|
|
31
|
+
if (persist && this.storage) {
|
|
32
|
+
const text = JSON.stringify({ v: value, e: entry.expires });
|
|
33
|
+
if (!this.write(this.prefix + key, text)) {
|
|
34
|
+
// Voll: abgelaufene eigene Einträge entfernen und einmal neu versuchen, sonst nur im Speicher.
|
|
35
|
+
this.prune();
|
|
36
|
+
this.write(this.prefix + key, text);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
delete(key) {
|
|
41
|
+
this.memory.delete(key);
|
|
42
|
+
try {
|
|
43
|
+
this.storage?.removeItem(this.prefix + key);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// gesperrter Speicher
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Leert Speicher und alle eigenen Einträge im sessionStorage, etwa beim Abmelden. */
|
|
50
|
+
clear() {
|
|
51
|
+
this.memory.clear();
|
|
52
|
+
this.removeWhere(() => true);
|
|
53
|
+
}
|
|
54
|
+
load(key) {
|
|
55
|
+
try {
|
|
56
|
+
const text = this.storage?.getItem(this.prefix + key);
|
|
57
|
+
if (!text) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const parsed = JSON.parse(text);
|
|
61
|
+
const entry = { value: parsed.v, expires: parsed.e };
|
|
62
|
+
this.memory.set(key, entry);
|
|
63
|
+
return entry;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
write(key, text) {
|
|
70
|
+
try {
|
|
71
|
+
this.storage?.setItem(key, text);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
prune() {
|
|
79
|
+
const now = this.now();
|
|
80
|
+
this.removeWhere((text) => {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(text).e <= now;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
removeWhere(predicate) {
|
|
90
|
+
try {
|
|
91
|
+
const storage = this.storage;
|
|
92
|
+
if (!storage) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (let i = storage.length - 1; i >= 0; i--) {
|
|
96
|
+
const key = storage.key(i);
|
|
97
|
+
if (key?.startsWith(this.prefix) && predicate(storage.getItem(key) ?? '')) {
|
|
98
|
+
storage.removeItem(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// gesperrter Speicher
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** sessionStorage, falls verfügbar; in privaten Fenstern oder per Richtlinie kann er fehlen. */
|
|
108
|
+
export function defaultStorage() {
|
|
109
|
+
try {
|
|
110
|
+
return globalThis.sessionStorage ?? null;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface AccessDeniedContact {
|
|
3
|
+
name?: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
/** Link, etwa ein Formular oder eine Teams-Seite. */
|
|
6
|
+
url?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AccessDeniedProps {
|
|
9
|
+
/** Name der App im Text. */
|
|
10
|
+
appName?: string;
|
|
11
|
+
/**
|
|
12
|
+
* no-role: angemeldet, aber ohne App-Rolle; missing-capability: Rolle da, aber nicht für diesen
|
|
13
|
+
* Bereich. Default nach den Rollen der Person.
|
|
14
|
+
*/
|
|
15
|
+
reason?: 'no-role' | 'missing-capability';
|
|
16
|
+
/** Fehlende Capability, wird bei missing-capability genannt. */
|
|
17
|
+
capability?: string;
|
|
18
|
+
title?: ReactNode;
|
|
19
|
+
/** Eigener Text statt der Erklärung. */
|
|
20
|
+
message?: ReactNode;
|
|
21
|
+
/** Wen man fragen kann. */
|
|
22
|
+
contact?: AccessDeniedContact;
|
|
23
|
+
/** „Angemeldet als …“ zeigen, Default true. */
|
|
24
|
+
showUser?: boolean;
|
|
25
|
+
/** Mit anderem Konto anmelden; Default Abmelden aus DirectoryProvider, null blendet es aus. */
|
|
26
|
+
onSignOut?: (() => void | Promise<void>) | null;
|
|
27
|
+
/** Eigene Knöpfe, etwa „Zur Startseite“. */
|
|
28
|
+
actions?: ReactNode;
|
|
29
|
+
/** Überschriftenebene, Default 1. */
|
|
30
|
+
headingLevel?: 1 | 2 | 3;
|
|
31
|
+
labels?: {
|
|
32
|
+
signOut?: string;
|
|
33
|
+
signedInAs?: string;
|
|
34
|
+
contact?: string;
|
|
35
|
+
};
|
|
36
|
+
className?: string;
|
|
37
|
+
classNames?: {
|
|
38
|
+
icon?: string;
|
|
39
|
+
title?: string;
|
|
40
|
+
message?: string;
|
|
41
|
+
hint?: string;
|
|
42
|
+
contact?: string;
|
|
43
|
+
user?: string;
|
|
44
|
+
actions?: string;
|
|
45
|
+
signOut?: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Seite für Personen, die angemeldet sind, aber keine Rolle oder nicht die nötige Capability haben:
|
|
50
|
+
* was fehlt, wen man fragen kann und wie man das Konto wechselt.
|
|
51
|
+
*/
|
|
52
|
+
export declare function AccessDenied({ appName, reason, capability, title, message, contact, showUser, onSignOut, actions, headingLevel, labels, className, classNames, }: AccessDeniedProps): import("react").JSX.Element;
|
|
53
|
+
//# sourceMappingURL=AccessDenied.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccessDenied.d.ts","sourceRoot":"","sources":["../../src/components/AccessDenied.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAO9C,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,4BAA4B;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,GAAG,oBAAoB,CAAC;IAC1C,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,wCAAwC;IACxC,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,2BAA2B;IAC3B,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+FAA+F;IAC/F,SAAS,CAAC,EAAE,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;IAChD,4CAA4C;IAC5C,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,qCAAqC;IACrC,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzB,MAAM,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE;QACX,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,MAAM,EACN,UAAU,EACV,KAAK,EACL,OAAO,EACP,OAAO,EACP,QAAe,EACf,SAAS,EACT,OAAO,EACP,YAAgB,EAChB,MAAM,EACN,SAAS,EACT,UAAU,GACX,EAAE,iBAAiB,+BA2DnB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
import { useCurrentUser } from '../hooks.js';
|
|
4
|
+
import { useSignOut } from '../provider.js';
|
|
5
|
+
import { cx } from '../utils.js';
|
|
6
|
+
import { LockIcon } from './icons.js';
|
|
7
|
+
import { PersonLabel } from './PersonLabel.js';
|
|
8
|
+
/**
|
|
9
|
+
* Seite für Personen, die angemeldet sind, aber keine Rolle oder nicht die nötige Capability haben:
|
|
10
|
+
* was fehlt, wen man fragen kann und wie man das Konto wechselt.
|
|
11
|
+
*/
|
|
12
|
+
export function AccessDenied({ appName, reason, capability, title, message, contact, showUser = true, onSignOut, actions, headingLevel = 1, labels, className, classNames, }) {
|
|
13
|
+
const { user } = useCurrentUser();
|
|
14
|
+
const defaultSignOut = useSignOut();
|
|
15
|
+
const signOut = onSignOut === undefined ? defaultSignOut : onSignOut;
|
|
16
|
+
const titleId = useId();
|
|
17
|
+
const kind = reason ?? (user && user.roles.length > 0 ? 'missing-capability' : 'no-role');
|
|
18
|
+
const app = appName ? `„${appName}“` : 'dieser Anwendung';
|
|
19
|
+
const Heading = `h${headingLevel}`;
|
|
20
|
+
const text = message ??
|
|
21
|
+
(kind === 'no-role'
|
|
22
|
+
? `Du bist angemeldet, hast in ${app} aber noch keine Rolle. Ohne Rolle sind keine Inhalte freigegeben.`
|
|
23
|
+
: `Für diesen Bereich von ${app} fehlt dir eine Berechtigung.`);
|
|
24
|
+
const hint = message === undefined
|
|
25
|
+
? kind === 'no-role'
|
|
26
|
+
? 'Rollen vergibt die verantwortliche Person in Entra ID. Nach der Zuweisung meldest du dich einmal ab und wieder an.'
|
|
27
|
+
: 'Frag die verantwortliche Person, ob deine Rolle erweitert werden kann.'
|
|
28
|
+
: null;
|
|
29
|
+
return (_jsxs("section", { className: cx('seed-access-denied', className), "data-reason": kind, "aria-labelledby": titleId, children: [_jsx("span", { className: cx('seed-access-denied__icon', classNames?.icon), "aria-hidden": "true", children: _jsx(LockIcon, {}) }), _jsx(Heading, { id: titleId, className: cx('seed-access-denied__title', classNames?.title), children: title ?? 'Kein Zugriff' }), _jsx("p", { className: cx('seed-access-denied__message', classNames?.message), children: text }), hint && _jsx("p", { className: cx('seed-access-denied__hint', classNames?.hint), children: hint }), kind === 'missing-capability' && capability && (_jsxs("p", { className: cx('seed-access-denied__hint', classNames?.hint), children: ["Ben\u00F6tigt: ", _jsx("code", { className: "seed-access-denied__code", children: capability })] })), contact && (contact.name || contact.email || contact.url) && (_jsxs("p", { className: cx('seed-access-denied__contact', classNames?.contact), children: [labels?.contact ?? 'Ansprechperson', ": ", _jsx(ContactLink, { contact: contact })] })), showUser && user && (_jsxs("div", { className: cx('seed-access-denied__user', classNames?.user), children: [_jsx("span", { className: "seed-access-denied__user-label", children: labels?.signedInAs ?? 'Angemeldet als' }), _jsx(PersonLabel, { id: user.id, name: user.displayName, secondary: user.username ?? undefined })] })), (signOut || actions) && (_jsxs("div", { className: cx('seed-access-denied__actions', classNames?.actions), children: [actions, signOut && (_jsx("button", { type: "button", className: cx('seed-access-denied__sign-out', classNames?.signOut), onClick: () => void signOut(), children: labels?.signOut ?? 'Mit anderem Konto anmelden' }))] }))] }));
|
|
30
|
+
}
|
|
31
|
+
function ContactLink({ contact }) {
|
|
32
|
+
const text = contact.name || contact.email || contact.url;
|
|
33
|
+
if (contact.email) {
|
|
34
|
+
return _jsx("a", { href: `mailto:${contact.email}`, children: text });
|
|
35
|
+
}
|
|
36
|
+
if (contact.url) {
|
|
37
|
+
return (_jsx("a", { href: contact.url, target: "_blank", rel: "noopener noreferrer", children: text }));
|
|
38
|
+
}
|
|
39
|
+
return _jsx(_Fragment, { children: text });
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=AccessDenied.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccessDenied.js","sourceRoot":"","sources":["../../src/components/AccessDenied.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
import type { PresenceStatus } from '../types.js';
|
|
3
|
+
import { type AvatarColor } from '../utils.js';
|
|
4
|
+
export interface AvatarProps {
|
|
5
|
+
/** Objekt-ID der Person; lädt Foto und, wenn name fehlt, den Namen. */
|
|
6
|
+
id?: string | null;
|
|
7
|
+
/** Anzeigename; ohne ihn kommt er aus dem Verzeichnis. */
|
|
8
|
+
name?: string | null;
|
|
9
|
+
/** Eigenes Bild (URL oder data:-URL) statt des Fotos aus Graph. */
|
|
10
|
+
image?: string | null;
|
|
11
|
+
/** Größe in Pixeln, 16 bis 96; Default 32. */
|
|
12
|
+
size?: number;
|
|
13
|
+
/** circular (Default) oder square mit abgerundeten Ecken. */
|
|
14
|
+
shape?: 'circular' | 'square';
|
|
15
|
+
/** true lädt den Teams-Status zur ID; ein Status setzt ihn fest. */
|
|
16
|
+
presence?: boolean | PresenceStatus;
|
|
17
|
+
/** false lädt kein Foto, nur Initialen. */
|
|
18
|
+
photo?: boolean;
|
|
19
|
+
/** colorful (Default): Farbe aus der ID; neutral, brand oder eine feste Farbe der Palette. */
|
|
20
|
+
color?: 'colorful' | 'neutral' | 'brand' | AvatarColor;
|
|
21
|
+
/** Nur Schmuck, etwa wenn der Name daneben steht; dann ohne eigene Beschriftung. */
|
|
22
|
+
decorative?: boolean;
|
|
23
|
+
/** Beschriftung für Screenreader, Default Name und Status. */
|
|
24
|
+
label?: string;
|
|
25
|
+
className?: string;
|
|
26
|
+
classNames?: {
|
|
27
|
+
image?: string;
|
|
28
|
+
initials?: string;
|
|
29
|
+
icon?: string;
|
|
30
|
+
badge?: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Profilbild; ohne Bild Initialen auf einer Farbe, die aus der ID berechnet wird. Während das Foto
|
|
35
|
+
* lädt, zeigt es einen Platzhalter.
|
|
36
|
+
*/
|
|
37
|
+
export declare function Avatar({ id, name, image, size, shape, presence, photo, color, decorative, label, className, classNames, }: AvatarProps): import("react").JSX.Element;
|
|
38
|
+
/**
|
|
39
|
+
* Größe als data-size für die Größen mit festen Regeln in styles.css, sonst als CSS-Variablen
|
|
40
|
+
* per CSSOM (erlaubt unter style-src 'self').
|
|
41
|
+
*/
|
|
42
|
+
export declare function avatarSizeAttributes(size: number): {
|
|
43
|
+
'data-size'?: number;
|
|
44
|
+
style?: CSSProperties;
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=Avatar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Avatar.d.ts","sourceRoot":"","sources":["../../src/components/Avatar.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAErD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAA6E,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAI1H,MAAM,WAAW,WAAW;IAC1B,uEAAuE;IACvE,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,KAAK,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;IACpC,2CAA2C;IAC3C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,8FAA8F;IAC9F,KAAK,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,CAAC;IACvD,oFAAoF;IACpF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACnF;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,EACrB,EAAE,EACF,IAAI,EACJ,KAAK,EACL,IAAS,EACT,KAAkB,EAClB,QAAQ,EACR,KAAY,EACZ,KAAkB,EAClB,UAAkB,EAClB,KAAK,EACL,SAAS,EACT,UAAU,GACX,EAAE,WAAW,+BA+Cb;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,aAAa,CAAA;CAAE,CAWlG"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { usePerson, usePhoto, usePresence } from '../hooks.js';
|
|
4
|
+
import { AVATAR_SIZES, avatarColor, cx, initials, PRESENCE_LABELS, presenceSizeFor } from '../utils.js';
|
|
5
|
+
import { PersonIcon } from './icons.js';
|
|
6
|
+
import { PresenceBadge } from './PresenceBadge.js';
|
|
7
|
+
/**
|
|
8
|
+
* Profilbild; ohne Bild Initialen auf einer Farbe, die aus der ID berechnet wird. Während das Foto
|
|
9
|
+
* lädt, zeigt es einen Platzhalter.
|
|
10
|
+
*/
|
|
11
|
+
export function Avatar({ id, name, image, size = 32, shape = 'circular', presence, photo = true, color = 'colorful', decorative = false, label, className, classNames, }) {
|
|
12
|
+
const person = usePerson(name ? null : id);
|
|
13
|
+
const loadPhoto = photo && !image && !!id;
|
|
14
|
+
const picture = usePhoto(loadPhoto ? id : null, size);
|
|
15
|
+
const live = usePresence(presence === true ? id : null);
|
|
16
|
+
const [failed, setFailed] = useState(null);
|
|
17
|
+
const displayName = name || person.person?.displayName || null;
|
|
18
|
+
const src = image || picture.url;
|
|
19
|
+
const text = initials(displayName);
|
|
20
|
+
const status = typeof presence === 'string' ? presence : (live.presence?.status ?? null);
|
|
21
|
+
let state;
|
|
22
|
+
if (src && src !== failed) {
|
|
23
|
+
state = 'image';
|
|
24
|
+
}
|
|
25
|
+
else if ((loadPhoto && picture.status === 'loading') || (!name && person.status === 'loading')) {
|
|
26
|
+
state = 'loading';
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
state = text ? 'initials' : 'icon';
|
|
30
|
+
}
|
|
31
|
+
const colorKey = id || displayName;
|
|
32
|
+
const colorName = color === 'colorful' ? (colorKey ? avatarColor(colorKey) : 'neutral') : color;
|
|
33
|
+
const accessibleName = label ?? [displayName ?? 'Person', status ? PRESENCE_LABELS[status] : null].filter(Boolean).join(', ');
|
|
34
|
+
return (_jsxs("span", { className: cx('seed-avatar', className), ...avatarSizeAttributes(size), "data-shape": shape, "data-state": state, "data-color": colorName, "data-presence": status ?? undefined, ...(decorative ? { 'aria-hidden': true } : { role: 'img', 'aria-label': accessibleName }), children: [state === 'image' && (_jsx("img", { className: cx('seed-avatar__image', classNames?.image), src: src, alt: "", onError: () => setFailed(src) })), state === 'initials' && (_jsx("span", { className: cx('seed-avatar__initials', classNames?.initials), "aria-hidden": "true", children: text })), state === 'icon' && _jsx(PersonIcon, { className: cx('seed-avatar__icon', classNames?.icon) }), status && _jsx(PresenceBadge, { status: status, size: presenceSizeFor(size), decorative: true, className: cx('seed-avatar__badge', classNames?.badge) })] }));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Größe als data-size für die Größen mit festen Regeln in styles.css, sonst als CSS-Variablen
|
|
38
|
+
* per CSSOM (erlaubt unter style-src 'self').
|
|
39
|
+
*/
|
|
40
|
+
export function avatarSizeAttributes(size) {
|
|
41
|
+
if (AVATAR_SIZES.includes(size)) {
|
|
42
|
+
return { 'data-size': size };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
style: {
|
|
46
|
+
'--seed-avatar-size': `${size}px`,
|
|
47
|
+
'--seed-avatar-font-size': `${Math.max(10, Math.round(size * 0.36))}px`,
|
|
48
|
+
'--seed-avatar-badge-size': `${presenceSizeFor(size)}px`,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=Avatar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Avatar.js","sourceRoot":"","sources":["../../src/components/Avatar.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type AvatarGroupPerson = string | {
|
|
2
|
+
id?: string | null;
|
|
3
|
+
name?: string | null;
|
|
4
|
+
};
|
|
5
|
+
export interface AvatarGroupProps {
|
|
6
|
+
/** Objekt-IDs oder Personen mit ID und Namen. */
|
|
7
|
+
people: readonly AvatarGroupPerson[];
|
|
8
|
+
/** Höchstens so viele Avatare, danach „+n“; Default 4. */
|
|
9
|
+
max?: number;
|
|
10
|
+
/** Default 32. */
|
|
11
|
+
size?: number;
|
|
12
|
+
shape?: 'circular' | 'square';
|
|
13
|
+
/** Beschriftung der Gruppe, Default „n Personen“. */
|
|
14
|
+
label?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
classNames?: {
|
|
17
|
+
avatar?: string;
|
|
18
|
+
overflow?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Mehrere Avatare überlappend, ab max mit „+n“; der Titel von „+n“ nennt die übrigen Namen. */
|
|
22
|
+
export declare function AvatarGroup({ people, max, size, shape, label, className, classNames }: AvatarGroupProps): import("react").JSX.Element;
|
|
23
|
+
//# sourceMappingURL=AvatarGroup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AvatarGroup.d.ts","sourceRoot":"","sources":["../../src/components/AvatarGroup.tsx"],"names":[],"mappings":"AAIA,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAEtF,MAAM,WAAW,gBAAgB;IAC/B,iDAAiD;IACjD,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACrC,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC9B,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD;AAED,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,GAAO,EAAE,IAAS,EAAE,KAAkB,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,gBAAgB,+BA6C7H"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useDirectory } from '../provider.js';
|
|
3
|
+
import { cx } from '../utils.js';
|
|
4
|
+
import { Avatar, avatarSizeAttributes } from './Avatar.js';
|
|
5
|
+
/** Mehrere Avatare überlappend, ab max mit „+n“; der Titel von „+n“ nennt die übrigen Namen. */
|
|
6
|
+
export function AvatarGroup({ people, max = 4, size = 32, shape = 'circular', label, className, classNames }) {
|
|
7
|
+
const directory = useDirectory();
|
|
8
|
+
const entries = people.map((entry) => (typeof entry === 'string' ? { id: entry, name: null } : entry));
|
|
9
|
+
// Bei Überlauf zählt „+n“ als eigener Platz.
|
|
10
|
+
const visible = entries.length > max ? entries.slice(0, Math.max(max - 1, 1)) : entries;
|
|
11
|
+
const hidden = entries.slice(visible.length);
|
|
12
|
+
const hiddenNames = hidden
|
|
13
|
+
.map((entry) => entry.name || (entry.id ? directory.peekPerson(entry.id)?.displayName : null))
|
|
14
|
+
.filter((name) => !!name);
|
|
15
|
+
const overflowLabel = `${hidden.length} weitere${hiddenNames.length > 0 ? `: ${hiddenNames.join(', ')}` : ''}`;
|
|
16
|
+
return (_jsxs("span", { className: cx('seed-avatar-group', className), role: "group", "aria-label": label ?? `${entries.length} ${entries.length === 1 ? 'Person' : 'Personen'}`, children: [visible.map((entry, index) => (_jsx(Avatar, { id: entry.id, name: entry.name, size: size, shape: shape, className: cx('seed-avatar-group__item', classNames?.avatar) }, entry.id ?? `${entry.name}-${index}`))), hidden.length > 0 && (_jsx("span", { className: cx('seed-avatar seed-avatar-group__overflow', classNames?.overflow), ...avatarSizeAttributes(size), "data-shape": shape, "data-state": "initials", "data-color": "neutral", role: "img", "aria-label": overflowLabel, title: hiddenNames.join(', ') || undefined, children: _jsxs("span", { className: "seed-avatar__initials", "aria-hidden": "true", children: ["+", hidden.length] }) }))] }));
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=AvatarGroup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AvatarGroup.js","sourceRoot":"","sources":["../../src/components/AvatarGroup.tsx"],"names":[],"mappings":"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"}
|