@blackforestsentinel/seed-web-people

中文文档
0.10.0发布于 2026/11/06MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 1919

React-Komponenten für Personen in Sentinel-Seed-Frontends: Avatar, Benutzermenü, Personenauswahl, Karten und Anmeldeseiten, mit Daten aus Microsoft Graph direkt im Browser.

原始使用文档
文档目录9 节
  1. Einbau
  2. Komponenten
  3. PeoplePicker
  4. Anwendungen und verwaltete Identitäten
  5. Hooks
  6. Anpassen
  7. Berechtigungen und Einwilligung
  8. Lokaler Modus
  9. Laufzeit und Datenschutz

@blackforestsentinel/seed-web-people

React-Komponenten rund um Personen für Frontends aus Sentinel Seed: 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.

  • Optik an Fluent 2 angelehnt (Teams, Outlook), ohne Abhängigkeit von @fluentui/*.
  • Ein statisches Stylesheet, keine eingefügten Styles; passt zur CSP der Seed-Apps (style-src 'self').
  • Bilder als data:-URL; die CSP erlaubt img-src 'self' data: schon.
  • Rund 16 KB JavaScript und 6 KB CSS nach gzip, ohne React. Einzelne Komponenten lassen sich per Tree-Shaking einbinden.

Einbau

npm install @blackforestsentinel/seed-web-people

Die Version passt zu @blackforestsentinel/seed-web-auth (Peer-Abhängigkeit, ebenso React ab 18).

// main.tsx
import '@blackforestsentinel/seed-web-people/styles.css';
import { SeedUserProvider } from '@blackforestsentinel/seed-web-auth/react';
import { DirectoryProvider, SignInPage } from '@blackforestsentinel/seed-web-people';

const config = await loadRuntimeConfig<AppConfig>();
const auth = await startAuth(config.auth); // createSeedAuth oder null (lokal, ohne sso)

if (auth && !auth.account) {
  root.render(
    <DirectoryProvider config={config.auth} auth={auth}>
      <SignInPage appName="Kundenportal" />
    </DirectoryProvider>,
  );
} else {
  root.render(
    <SeedUserProvider load={api.getMe}>
      <DirectoryProvider config={config.auth} auth={auth}>
        <App />
      </DirectoryProvider>
    </SeedUserProvider>,
  );
}
  • 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.
  • DirectoryProvider gehört in SeedUserProvider, damit Rollen und Capabilities aus /api/me ankommen.
  • Die CSP der App braucht connect-src https://graph.microsoft.com; das Template trägt das mit auth.directory ein.

Komponenten

<CurrentUserMenu menuItems={[{ label: 'Hilfe', href: '/hilfe' }]} />
<PersonLabel id={invoice.approvedBy} secondary={formatDate(invoice.approvedAt)} card />
<PeoplePicker multiple value={reviewers} onChange={setReviewers} onlyWithAppRole roles={['Admin']} />
<PeoplePicker multiple value={writers} onChange={setWriters} principalTypes={['user', 'servicePrincipal']} />
<AvatarGroup people={task.participants} max={4} />
KomponenteZeigtWichtige Props
AvatarFoto; ohne Foto Initialen auf einer Farbe aus der ID; Platzhalter beim Laden; für Anwendungen ein Symbolid, name, image, kind, servicePrincipalType, size (16 bis 96, Default 32), shape (circular, square), presence (true oder fester Status), color, decorative
PersonLabelAvatar, Name, optional zweite Zeile; mit card die PersonCard beim Überfahren und per Tastaturid, name, person, secondary, secondaryField (mail, jobTitle, department), size, presence, card
PersonCardgrößeres Bild, Name, Position, Abteilung, Status, Mail, Link zum Teams-Chatid, person, name, presence, chat, actions
AvatarGroupmehrere Avatare überlappend, ab max „+n“ mit den übrigen Namen als Titelpeople (IDs oder { id, name }), max (Default 4), size
CurrentUserMenuAvatar, Name, Rolle (Anzeigename aus /api/me), Abmelden; mit menuItems ein MenümenuItems, renderTrigger, showRole, presence, onSignOut (null blendet aus)
PeoplePickerSuchfeld mit Vorschlägen, eine oder mehrere Personen als Chipsvalue/defaultValue, onChange, multiple, filter, renderOption, onlyWithAppRole, roles, principalTypes, servicePrincipalTypes, minChars (2), debounceMs (250), maxSuggestions (8), labels
SignInPageAnmeldeseite mit App-Name und KnopfappName, description, logo, onSignIn (Default auth.login()), error, footer
AccessDeniedangemeldet, aber ohne Rolle oder Capability: Erklärung, Ansprechperson, KontowechselappName, reason (no-role, missing-capability), capability, contact, actions, onSignOut
RoleBadgeRolle als Etikett, Anzeigename aus /api/merole, displayName, appearance (tint, filled, outline)
PresenceBadgeTeams-Status als Punktstatus, size, label

PeoplePicker

  • 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.
  • 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.
  • filter blendet Vorschläge aus, renderOption stellt sie selbst dar.
  • 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.
  • 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.

Anwendungen und verwaltete Identitäten

Wer einen Datensatz bearbeiten darf, ist oft nicht nur eine Person, sondern auch eine verwaltete Identität (Managed Identity) oder eine andere Anwendung. Mit auth.directory.servicePrincipals (in project.yaml auth.directory.servicePrincipals: true) kennen die Komponenten auch diese Service Principals:

<PeoplePicker
  multiple
  principalTypes={['user', 'servicePrincipal']}
  servicePrincipalTypes={['ManagedIdentity', 'Application']}
  value={writers}
  onChange={setWriters}
/>
<PersonLabel id={record.updatedBy} secondaryField="jobTitle" card />
  • Person.kind ist user oder servicePrincipal. Für Service Principals kommen appId (Client-ID) und servicePrincipalType (ManagedIdentity, Application) dazu. Gespeicherte Daten ohne kind gelten als Person.
  • principalTypes im Picker, Default ['user']. Mit servicePrincipal sucht er zusätzlich über /servicePrincipals?$search="displayName:…", im selben $batch wie die Personen. servicePrincipalTypes beschränkt die Suche, etwa auf ['ManagedIdentity']. Ohne Beschränkung erscheinen auch Anwendungen von Microsoft (etwa „Microsoft Graph“); filter blendet weitere aus.
  • Personen und Anwendungen stehen abwechselnd in den Vorschlägen. Anwendungen zeigen ihre Art und die App-ID als zweite Zeile.
  • Mit onlyWithAppRole erscheinen Service Principals, denen eine Rolle der API direkt zugewiesen ist. Gruppen löst der Filter nur nach Personen auf.
  • PersonLabel, PersonCard, Avatar erkennen Service Principals an der Objekt-ID: Gibt es keine Person dazu (/users/{id}), fragen sie /servicePrincipals/{id}. Statt Initialen zeigen sie ein Symbol auf einem Quadrat, einen Schlüssel für verwaltete Identitäten, sonst vier Kacheln. Foto und Teams-Status gibt es für Anwendungen nicht, Mail und Chat auf der Karte auch nicht; die Karte nennt Art und App-ID.
  • Mit dem Schalter lädt jedes PersonLabel und jeder Avatar mit id das Profil, um die Art zu kennen, auch wenn name gesetzt ist (im selben $batch, 30 Minuten im Zwischenspeicher). Den Teams-Status lädt das Label erst, wenn feststeht, dass es eine Person ist. Wer die Art schon kennt, gibt sie mit und spart die Abfrage: person={{ kind: 'user' }} bzw. kind am Avatar.
  • secondaryField zeigt bei Anwendungen deren Art („Verwaltete Identität“, „Anwendung“), wenn das Feld leer ist.
  • Ohne auth.directory.servicePrincipals meldet der Picker mit principalTypes einen Fehler im Feld und in der Konsole, statt nur Personen zu zeigen. Labels und Karten bleiben wie bisher: Für die ID einer Anwendung zeigen sie den übergebenen Namen mit Initialen.
  • Im lokalen Modus stehen Anwendungen mit "kind": "servicePrincipal" in auth.directory.people.

Hooks

Für eigene Oberflächen gibt es nur die Daten:

HookLiefert
useCurrentUser(){ status, user }; user mit id, displayName, username, roles, roleNames, capabilities. Der Name kommt sofort aus der Anmeldung, Rollen aus /api/me.
usePerson(id){ status, person } mit Name, Mail, Position, Abteilung
usePhoto(id, size){ status, url } als data:-URL oder null
usePeopleSearch(text, options){ status, people, query }, mit Verzögerung und Mindestlänge; principalTypes und servicePrincipalTypes wie im Picker
usePresence(id){ status, presence }, gebündelt und jede Minute aktualisiert, solange sichtbar
useAppRoleMembers(roles, options){ status, members, ids }, Personen mit App-Rolle; mit principalTypes auch Service Principals
useDirectory(), useSignOut()das Verzeichnis; Abmelden samt Leeren der Zwischenspeicher

status ist idle (nichts zu laden), loading, ready oder error. Ohne React: createGraphDirectory() und createLocalDirectory().

Anpassen

  • CSS-Variablen für alles Sichtbare, an einer Stelle gesetzt:
    :root {
      --seed-accent: #8a2be2;
      --seed-font: Inter, sans-serif;
      --seed-avatar-radius: 8px;
    }
    
    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).
  • @layer seed: Alle Standard-Styles liegen in dieser Ebene. Jede Regel der App gewinnt ohne !important, auch eine mit geringerer Spezifität.
  • 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".
  • 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.
  • Eigene Darstellung: renderOption im Picker, renderTrigger und menuItems im Benutzermenü, die Hooks für alles andere.
  • 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.
  • 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.

Berechtigungen und Einwilligung

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.

BerechtigungWofürAdmin-Consent nötig
User.Readeigenes Profilnein
User.ReadBasic.AllProfile und Fotos anderer, Verzeichnissuchenein
People.Readrelevante Personen im Pickernein
Presence.Read.AllTeams-Status (presence: true)nein
Application.Read.AllRollenzuweisungen der API (roleFilter: true), Service Principals suchen und anzeigen (servicePrincipals: true)ja
GroupMember.Read.AllMitglieder zugewiesener Gruppen (roleFilter: true)ja
  • 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“.
  • 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 oder servicePrincipals: true daher erst die Administratorzustimmung erteilen, dann ausrollen.
  • 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.
  • Lehnt Graph Position und Abteilung ab (403), fragt das Paket nur Name und Mail ab.

Lokaler Modus

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.

{
  "apiBaseUrl": "http://localhost:7071",
  "auth": {
    "mode": "local",
    "directory": {
      "people": [
        { "id": "00000000-0000-4000-8000-000000000001", "displayName": "Erika Mustermann", "mail": "[email protected]", "jobTitle": "Buchhalterin", "department": "Buchhaltung", "roles": ["Admin"] },
        { "id": "00000000-0000-4000-8000-000000000101", "displayName": "Nachtlauf", "kind": "servicePrincipal", "servicePrincipalType": "ManagedIdentity", "appId": "00000000-0000-4000-8000-000000000201", "roles": ["Admin"] }
      ]
    }
  }
}

roles ist optional und nur für onlyWithAppRole im lokalen Modus. Einträge mit "kind": "servicePrincipal" findet der Picker nur mit principalTypes. Ohne auth.directory sucht der Picker nicht und sagt das.

Laufzeit und Datenschutz

  • Bilder lädt das Paket nur, wenn eine Komponente sie zeigt, in festen Größen (48, 64 oder 96 Pixel aus /photos/{size}/$value).
  • Anfragen innerhalb weniger Millisekunden gehen gemeinsam als $batch (bis 20 je Aufruf). Gleiche laufende Anfragen werden zusammengefasst, gedrosselte (429) nach Retry-After wiederholt.
  • 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.
  • 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.
  • 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.

版本与发布记录

@blackforestsentinel/seed-web-people 共发布 10 个版本,首次发布于 2026/10/11;过去 12 个月发布了 10 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.10.0latest2026/11/061 天前
0.9.02026/10/309 天前
0.8.02026/10/2415 天前
0.7.02026/10/1821 天前
0.6.12026/10/1721 天前
0.6.02026/10/1722 天前

关于 @blackforestsentinel/seed-web-people 的常见问题

@blackforestsentinel/seed-web-people 是什么?

React-Komponenten für Personen in Sentinel-Seed-Frontends: Avatar, Benutzermenü, Personenauswahl, Karten und Anmeldeseiten, mit Daten aus Microsoft Graph direkt im Browser.

如何安装 @blackforestsentinel/seed-web-people?

在项目目录运行 npm install @blackforestsentinel/seed-web-people,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=18)、@blackforestsentinel/seed-web-auth(^0.10.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@blackforestsentinel/seed-web-people 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@blackforestsentinel/seed-web-people 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@blackforestsentinel/seed-web-people 可以商用吗?

@blackforestsentinel/seed-web-people 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@blackforestsentinel/seed-web-people 的下载量有多少?

根据 npm 官方下载统计,@blackforestsentinel/seed-web-people 在 2026/10/09 – 2026/11/07 这 30 天内被下载 1,919 次,日均约 64 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@blackforestsentinel/seed-web-people 还在维护吗?

最新版本 0.10.0 发布于 2026/11/06(1 天前);至今共发布 10 个版本,其中过去 12 个月发布了 10 个;首次发布于 2026/10/11。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。