@devsym/graph-toolkit-react 0.1.0-next.2

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/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) Microsoft and Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ Note: Usage of the assets referenced in Office UI Fabric files is subject to
24
+ the terms listed at https://aka.ms/fabric-assets-license
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Person component - Display a person using Fluent UI Persona
3
+ */
4
+ import React from 'react';
5
+ import { PersonProps } from './Person.types';
6
+ export declare const Person: React.FC<PersonProps>;
7
+ //# sourceMappingURL=Person.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Person.d.ts","sourceRoot":"","sources":["../../../src/components/Person/Person.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AA2C7C,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAuFxC,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Person component types
3
+ */
4
+ import type { CSSProperties } from 'react';
5
+ import { User } from '@microsoft/microsoft-graph-types';
6
+ export type PersonView = 'avatar' | 'oneline' | 'twolines' | 'threelines';
7
+ export type PersonSize = 'small' | 'medium' | 'large' | 'extra-large';
8
+ export type PersonTextAlignment = 'start' | 'center';
9
+ export type PersonTextPosition = 'before' | 'after' | 'below';
10
+ export interface PersonDetails {
11
+ displayName?: string;
12
+ mail?: string;
13
+ jobTitle?: string;
14
+ department?: string;
15
+ officeLocation?: string;
16
+ id?: string;
17
+ userPrincipalName?: string;
18
+ }
19
+ export interface PersonProps {
20
+ userId?: string;
21
+ userPrincipalName?: string;
22
+ email?: string;
23
+ personDetails?: PersonDetails;
24
+ view?: PersonView;
25
+ showPresence?: boolean;
26
+ avatarSize?: PersonSize;
27
+ textAlignment?: PersonTextAlignment;
28
+ textPosition?: PersonTextPosition;
29
+ numericSize?: number;
30
+ fetchImage?: boolean;
31
+ onClick?: (person: User) => void;
32
+ className?: string;
33
+ style?: CSSProperties;
34
+ }
35
+ //# sourceMappingURL=Person.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Person.types.d.ts","sourceRoot":"","sources":["../../../src/components/Person/Person.types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,kCAAkC,CAAC;AAExD,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,YAAY,CAAC;AAC1E,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,aAAa,CAAC;AACtE,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,QAAQ,CAAC;AACrD,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;AAE9D,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAE1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,aAAa,CAAC,EAAE,aAAa,CAAC;IAG9B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,UAAU,CAAC;IAGxB,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,UAAU,CAAC,EAAE,OAAO,CAAC;IAGrB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,KAAK,IAAI,CAAC;IAGjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Person component exports
3
+ */
4
+ export { Person } from './Person';
5
+ export type { PersonProps } from './Person.types';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Person/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Hook to access Graph Client
3
+ */
4
+ import { Client } from '@microsoft/microsoft-graph-client';
5
+ /**
6
+ * Hook to get a configured Graph Client instance
7
+ */
8
+ export declare const useGraphClient: () => Client | null;
9
+ //# sourceMappingURL=useGraphClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGraphClient.d.ts","sourceRoot":"","sources":["../../src/hooks/useGraphClient.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAI3D;;GAEG;AACH,eAAO,MAAM,cAAc,QAAO,MAAM,GAAG,IAS1C,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Hook to fetch person data from Microsoft Graph
3
+ */
4
+ import { User, Presence } from '@microsoft/microsoft-graph-types';
5
+ export interface PersonData {
6
+ user: User | null;
7
+ presence: Presence | null;
8
+ photoUrl: string | null;
9
+ loading: boolean;
10
+ error: Error | null;
11
+ }
12
+ export interface UsePersonDataOptions {
13
+ userId?: string;
14
+ userPrincipalName?: string;
15
+ fetchPresence?: boolean;
16
+ fetchPhoto?: boolean;
17
+ }
18
+ /**
19
+ * Fetch person data from Microsoft Graph
20
+ */
21
+ export declare const usePersonData: (options: UsePersonDataOptions) => PersonData;
22
+ //# sourceMappingURL=usePersonData.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePersonData.d.ts","sourceRoot":"","sources":["../../src/hooks/usePersonData.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAKlE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,SAAS,oBAAoB,KAAG,UAgK7D,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Graph Toolkit React - React Components
3
+ *
4
+ * React components for Microsoft Graph powered by Fluent UI
5
+ */
6
+ export * from './components/Person';
7
+ export * from './providers/IProvider';
8
+ export * from './providers/ProviderContext';
9
+ export * from './providers/MockProvider';
10
+ export * from './providers/MsalBrowserProvider';
11
+ export * from './providers/TeamsHostedProvider';
12
+ export * from './hooks/useGraphClient';
13
+ export * from './hooks/usePersonData';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,cAAc,qBAAqB,CAAC;AAGpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAGhD,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const w=require("react/jsx-runtime"),O=require("@fluentui/react-components"),d=require("react"),q=require("@microsoft/microsoft-graph-client");var o=(t=>(t.Loading="Loading",t.SignedOut="SignedOut",t.SignedIn="SignedIn",t))(o||{});const P=d.createContext({provider:null,state:o.Loading}),G=({provider:t,children:e})=>{const s=d.useCallback(()=>t?.getState?.()??o.Loading,[t]),r=d.useCallback(i=>(t.onStateChanged?.(i),()=>{const c=t;typeof c.removeStateChangedHandler=="function"&&c.removeStateChangedHandler(i)}),[t]),n=d.useSyncExternalStore(r,s,s),a=d.useMemo(()=>({provider:t,state:n}),[t,n]);return w.jsx(P.Provider,{value:a,children:e})},I=()=>{const{provider:t}=d.useContext(P);return t},b=()=>{const{state:t}=d.useContext(P);return t},L=t=>q.Client.init({authProvider:async e=>{try{const s=await t.getAccessToken();e(null,s)}catch(s){e(s,null)}}}),z=t=>{const e=t.trim().split(" ");return e.length===1?e[0].substring(0,2).toUpperCase():(e[0][0]+e[e.length-1][0]).toUpperCase()},U=()=>{const t=I(),e=b();return d.useMemo(()=>!t||e!=="SignedIn"?null:L(t),[t,e])};class C{state=o.SignedOut;listeners=[];constructor(e){e?.autoSignIn&&(this.state=o.SignedIn)}getAccessToken(){return this.state!==o.SignedIn?Promise.reject(new Error("Not signed in")):Promise.resolve("mock-token")}async login(){this.state=o.SignedIn,this.emit()}async logout(){this.state=o.SignedOut,this.emit()}getState(){return this.state}onStateChanged(e){this.listeners.push(e)}removeStateChangedHandler(e){this.listeners=this.listeners.filter(s=>s!==e)}emit(){for(const e of this.listeners)try{e()}catch{}}}const B=()=>new C({autoSignIn:!0}),j=t=>{const e=U(),s=I(),r=b(),{userId:n,userPrincipalName:a,fetchPresence:i=!1,fetchPhoto:c=!0}=t,[f,p]=d.useState({user:null,presence:null,photoUrl:null,loading:!0,error:null});return d.useEffect(()=>{let h=!1;return(async()=>{if(s instanceof C){const l={id:"00000000-0000-0000-0000-000000000000",displayName:"Adele Vance",userPrincipalName:"adelev@contoso.com",jobTitle:"Product Manager",department:"Marketing",officeLocation:"19/3106",mail:"adelev@contoso.com"},u={availability:"Available",activity:"Available",id:l.id,odataType:"#microsoft.graph.presence"};h||p({user:l,presence:i?u:null,photoUrl:c?"https://ui-avatars.com/api/?name=Adele+Vance&background=0D8ABC&color=ffffff":null,loading:!1,error:null});return}if(r!=="SignedIn"){h||p({user:null,presence:null,photoUrl:null,loading:!1,error:r==="SignedOut"?null:new Error("Provider not signed in")});return}if(!e){h||p(l=>({...l,loading:!0}));return}const m=n||a;if(!m){h||p({user:null,presence:null,photoUrl:null,loading:!1,error:null});return}try{p(g=>({...g,loading:!0,error:null}));const l=m.toLowerCase()==="me",u=await e.api(l?"/me":`/users/${m}`).select("id,displayName,jobTitle,mail,department,officeLocation,userPrincipalName").get();if(h)return;let S=null,k=null;if(i&&u.id)try{S=await e.api(l?"/me/presence":`/users/${u.id}/presence`).get()}catch(g){console.warn("Failed to fetch presence:",g)}if(c&&u.id)try{const g=await e.api(l?"/me/photo/$value":`/users/${u.id}/photo/$value`).get();k=URL.createObjectURL(g)}catch(g){console.warn("Failed to fetch photo:",g)}h||p({user:u,presence:S,photoUrl:k,loading:!1,error:null})}catch(l){h||p({user:null,presence:null,photoUrl:null,loading:!1,error:l})}})(),()=>{h=!0}},[e,s,r,n,a,i,c]),f},N=t=>{switch(t){case"small":return"small";case"large":return"large";case"extra-large":return"extra-large";default:return"medium"}},$=t=>{switch(t?.toLowerCase()){case"available":case"availableidle":return"available";case"away":case"berightback":return"away";case"busy":case"busyidle":case"donotdisturb":return"busy";default:return"offline"}},D=({userId:t,userPrincipalName:e,email:s,personDetails:r,view:n="oneline",showPresence:a=!1,avatarSize:i="medium",textAlignment:c,textPosition:f,numericSize:p,fetchImage:h=!0,onClick:y,className:m,style:l})=>{const{user:u,presence:S,photoUrl:k,loading:g}=j({userId:r?void 0:t||e||s,fetchPresence:a,fetchPhoto:h}),T=r||u,E=p??N(i);if(g)return w.jsx(O.Persona,{size:E,name:"Loading...",textAlignment:c,textPosition:f,className:m,style:l});if(!T)return null;const M=T.displayName||"Unknown User",F=z(M),v={size:E,name:M,textAlignment:c,textPosition:f,avatar:{image:k?{src:k}:void 0,initials:k?void 0:F},className:m,style:l};return a&&S&&(v.presence={status:$(S.availability)}),n!=="avatar"&&((n==="twolines"||n==="threelines")&&(v.secondaryText=T.jobTitle??void 0),n==="threelines"&&(v.tertiaryText=T.department??void 0)),y&&(v.onClick=()=>{u&&y(u)}),w.jsx(O.Persona,{...v})};function H(t){return`See https://aka.ms/msal.js.errors#${t} for details`}class x extends Error{constructor(e,s,r){const n=s||(e?H(e):""),a=n?`${e}: ${n}`:e;super(a),Object.setPrototypeOf(this,x.prototype),this.errorCode=e||"",this.errorMessage=n||"",this.subError=r||"",this.name="AuthError"}setCorrelationId(e){this.correlationId=e}}class A extends x{constructor(e,s,r,n,a,i,c,f){super(e,s,r),Object.setPrototypeOf(this,A.prototype),this.timestamp=n||"",this.traceId=a||"",this.correlationId=i||"",this.claims=c||"",this.name="InteractionRequiredAuthError",this.errorNo=f}}class R{msalInstance;defaultScopes;account=null;state=o.Loading;listeners=[];constructor(e,s){this.msalInstance=e,this.defaultScopes=s}async initialize(){await this.msalInstance.initialize(),await this.msalInstance.handleRedirectPromise();const e=this.msalInstance.getActiveAccount(),s=this.msalInstance.getAllAccounts();this.account=e??s[0]??null,this.account?(this.msalInstance.setActiveAccount(this.account),this.setState(o.SignedIn)):this.setState(o.SignedOut)}async login(){const e={scopes:this.defaultScopes},s=await this.msalInstance.loginPopup(e);this.account=s.account,this.account&&this.msalInstance.setActiveAccount(this.account),this.setState(o.SignedIn)}async logout(){const e=this.msalInstance.getActiveAccount()??this.account??void 0;await this.msalInstance.logoutPopup({account:e}),this.account=null,this.setState(o.SignedOut)}async getAccessToken(e){const s=e&&e.length>0?e:this.defaultScopes,r=this.msalInstance.getActiveAccount()??this.account;if(!r)throw new Error("No active account. User must sign in first.");const n={scopes:s,account:r};try{return(await this.msalInstance.acquireTokenSilent(n)).accessToken}catch(a){if(a instanceof A){const i={scopes:s,account:r};return(await this.msalInstance.acquireTokenPopup(i)).accessToken}throw a}}getState(){return this.state}onStateChanged(e){this.listeners.push(e)}removeStateChangedHandler(e){this.listeners=this.listeners.filter(s=>s!==e)}setState(e){this.state=e,this.emit()}emit(){for(const e of this.listeners)try{e()}catch{}}}const K=6e4;function V(t){const e=t.fetchFn??fetch;return async({ssoToken:s,scopes:r})=>{const n=await e(t.endpoint,{method:"POST",headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json"},body:JSON.stringify({scopes:r})}),a=await n.json().catch(()=>({error:n.statusText||"Token exchange failed"}));if(!n.ok){const i=a?.error||a?.message||n.statusText;throw new Error(`Backend token exchange failed: ${i}`)}if(!a?.accessToken)throw new Error("Backend token exchange response did not include accessToken.");return{accessToken:a.accessToken,expiresAt:a.expiresAt}}}class J{defaultScopes;getTeamsSsoTokenFn;exchangeForGraphTokenFn;refreshSkewMs;state=o.SignedOut;listeners=[];tokenCache=new Map;constructor(e){if(!e.defaultScopes||e.defaultScopes.length===0)throw new Error("TeamsHostedProvider requires at least one default scope.");this.defaultScopes=[...new Set(e.defaultScopes)],this.getTeamsSsoTokenFn=e.getTeamsSsoToken,this.exchangeForGraphTokenFn=e.exchangeForGraphToken,this.refreshSkewMs=e.refreshSkewMs??K}async login(){try{await this.getAccessToken(),this.setState(o.SignedIn)}catch(e){throw this.clearCache(),this.setState(o.SignedOut),e}}async logout(){this.clearCache(),this.setState(o.SignedOut)}async getAccessToken(e){const s=this.normalizeScopes(e),r=this.buildScopeKey(s),n=this.tokenCache.get(r);if(n&&this.isTokenValid(n))return n.accessToken;const a=await this.getTeamsSsoTokenFn(s);if(!a)throw new Error("Teams SSO token acquisition returned empty token.");const i=await this.exchangeForGraphTokenFn({ssoToken:a,scopes:s}),c=this.normalizeExchangedToken(i);return this.tokenCache.set(r,c),this.setState(o.SignedIn),c.accessToken}getState(){return this.state}onStateChanged(e){this.listeners.push(e)}removeStateChangedHandler(e){this.listeners=this.listeners.filter(s=>s!==e)}normalizeScopes(e){const s=e&&e.length>0?e:this.defaultScopes,r=[...new Set(s.filter(Boolean))];if(r.length===0)throw new Error("At least one scope is required to request a Graph token.");return r}buildScopeKey(e){return[...e].sort().join(" ")}isTokenValid(e){return e.expiresAtMs?Date.now()+this.refreshSkewMs<e.expiresAtMs:!0}normalizeExchangedToken(e){if(typeof e=="string"){if(!e)throw new Error("Graph token exchange returned empty access token.");return{accessToken:e}}if(!e.accessToken)throw new Error("Graph token exchange returned empty access token.");return{accessToken:e.accessToken,expiresAtMs:this.toEpochMs(e.expiresAt)}}toEpochMs(e){if(e)return e instanceof Date?e.getTime():e}clearCache(){this.tokenCache.clear()}setState(e){this.state!==e&&(this.state=e,this.emit())}emit(){for(const e of this.listeners)try{e()}catch{}}}exports.GraphProvider=G;exports.MockProvider=C;exports.MsalBrowserProvider=R;exports.Person=D;exports.ProviderState=o;exports.TeamsHostedProvider=J;exports.createBackendTokenExchange=V;exports.createMockSignedInProvider=B;exports.useGraphClient=U;exports.usePersonData=j;exports.useProvider=I;exports.useProviderState=b;
package/dist/index.mjs ADDED
@@ -0,0 +1,465 @@
1
+ import { jsx as w } from "react/jsx-runtime";
2
+ import { Persona as C } from "@fluentui/react-components";
3
+ import { createContext as j, useCallback as x, useSyncExternalStore as G, useMemo as E, useContext as M, useState as q, useEffect as z } from "react";
4
+ import { Client as N } from "@microsoft/microsoft-graph-client";
5
+ var o = /* @__PURE__ */ ((t) => (t.Loading = "Loading", t.SignedOut = "SignedOut", t.SignedIn = "SignedIn", t))(o || {});
6
+ const v = j({
7
+ provider: null,
8
+ state: o.Loading
9
+ }), Y = ({ provider: t, children: e }) => {
10
+ const s = x(
11
+ () => t?.getState?.() ?? o.Loading,
12
+ [t]
13
+ ), r = x(
14
+ (i) => (t.onStateChanged?.(i), () => {
15
+ const c = t;
16
+ typeof c.removeStateChangedHandler == "function" && c.removeStateChangedHandler(i);
17
+ }),
18
+ [t]
19
+ ), n = G(r, s, s), a = E(() => ({ provider: t, state: n }), [t, n]);
20
+ return /* @__PURE__ */ w(v.Provider, { value: a, children: e });
21
+ }, U = () => {
22
+ const { provider: t } = M(v);
23
+ return t;
24
+ }, O = () => {
25
+ const { state: t } = M(v);
26
+ return t;
27
+ }, $ = (t) => N.init({
28
+ authProvider: async (e) => {
29
+ try {
30
+ const s = await t.getAccessToken();
31
+ e(null, s);
32
+ } catch (s) {
33
+ e(s, null);
34
+ }
35
+ }
36
+ }), B = (t) => {
37
+ const e = t.trim().split(" ");
38
+ return e.length === 1 ? e[0].substring(0, 2).toUpperCase() : (e[0][0] + e[e.length - 1][0]).toUpperCase();
39
+ }, D = () => {
40
+ const t = U(), e = O();
41
+ return E(() => !t || e !== "SignedIn" ? null : $(t), [t, e]);
42
+ };
43
+ class F {
44
+ state = o.SignedOut;
45
+ listeners = [];
46
+ constructor(e) {
47
+ e?.autoSignIn && (this.state = o.SignedIn);
48
+ }
49
+ getAccessToken() {
50
+ return this.state !== o.SignedIn ? Promise.reject(new Error("Not signed in")) : Promise.resolve("mock-token");
51
+ }
52
+ async login() {
53
+ this.state = o.SignedIn, this.emit();
54
+ }
55
+ async logout() {
56
+ this.state = o.SignedOut, this.emit();
57
+ }
58
+ getState() {
59
+ return this.state;
60
+ }
61
+ onStateChanged(e) {
62
+ this.listeners.push(e);
63
+ }
64
+ removeStateChangedHandler(e) {
65
+ this.listeners = this.listeners.filter((s) => s !== e);
66
+ }
67
+ emit() {
68
+ for (const e of this.listeners)
69
+ try {
70
+ e();
71
+ } catch {
72
+ }
73
+ }
74
+ }
75
+ const Z = () => new F({ autoSignIn: !0 }), H = (t) => {
76
+ const e = D(), s = U(), r = O(), { userId: n, userPrincipalName: a, fetchPresence: i = !1, fetchPhoto: c = !0 } = t, [p, d] = q({
77
+ user: null,
78
+ presence: null,
79
+ photoUrl: null,
80
+ loading: !0,
81
+ error: null
82
+ });
83
+ return z(() => {
84
+ let h = !1;
85
+ return (async () => {
86
+ if (s instanceof F) {
87
+ const l = {
88
+ id: "00000000-0000-0000-0000-000000000000",
89
+ displayName: "Adele Vance",
90
+ userPrincipalName: "adelev@contoso.com",
91
+ jobTitle: "Product Manager",
92
+ department: "Marketing",
93
+ officeLocation: "19/3106",
94
+ mail: "adelev@contoso.com"
95
+ }, u = {
96
+ availability: "Available",
97
+ activity: "Available",
98
+ id: l.id,
99
+ odataType: "#microsoft.graph.presence"
100
+ };
101
+ h || d({
102
+ user: l,
103
+ presence: i ? u : null,
104
+ photoUrl: c ? "https://ui-avatars.com/api/?name=Adele+Vance&background=0D8ABC&color=ffffff" : null,
105
+ loading: !1,
106
+ error: null
107
+ });
108
+ return;
109
+ }
110
+ if (r !== "SignedIn") {
111
+ h || d({
112
+ user: null,
113
+ presence: null,
114
+ photoUrl: null,
115
+ loading: !1,
116
+ error: r === "SignedOut" ? null : new Error("Provider not signed in")
117
+ });
118
+ return;
119
+ }
120
+ if (!e) {
121
+ h || d((l) => ({ ...l, loading: !0 }));
122
+ return;
123
+ }
124
+ const g = n || a;
125
+ if (!g) {
126
+ h || d({
127
+ user: null,
128
+ presence: null,
129
+ photoUrl: null,
130
+ loading: !1,
131
+ error: null
132
+ });
133
+ return;
134
+ }
135
+ try {
136
+ d((f) => ({ ...f, loading: !0, error: null }));
137
+ const l = g.toLowerCase() === "me", u = await e.api(l ? "/me" : `/users/${g}`).select("id,displayName,jobTitle,mail,department,officeLocation,userPrincipalName").get();
138
+ if (h) return;
139
+ let m = null, S = null;
140
+ if (i && u.id)
141
+ try {
142
+ m = await e.api(l ? "/me/presence" : `/users/${u.id}/presence`).get();
143
+ } catch (f) {
144
+ console.warn("Failed to fetch presence:", f);
145
+ }
146
+ if (c && u.id)
147
+ try {
148
+ const f = await e.api(l ? "/me/photo/$value" : `/users/${u.id}/photo/$value`).get();
149
+ S = URL.createObjectURL(f);
150
+ } catch (f) {
151
+ console.warn("Failed to fetch photo:", f);
152
+ }
153
+ h || d({
154
+ user: u,
155
+ presence: m,
156
+ photoUrl: S,
157
+ loading: !1,
158
+ error: null
159
+ });
160
+ } catch (l) {
161
+ h || d({
162
+ user: null,
163
+ presence: null,
164
+ photoUrl: null,
165
+ loading: !1,
166
+ error: l
167
+ });
168
+ }
169
+ })(), () => {
170
+ h = !0;
171
+ };
172
+ }, [
173
+ e,
174
+ s,
175
+ r,
176
+ n,
177
+ a,
178
+ i,
179
+ c
180
+ ]), p;
181
+ }, R = (t) => {
182
+ switch (t) {
183
+ case "small":
184
+ return "small";
185
+ case "large":
186
+ return "large";
187
+ case "extra-large":
188
+ return "extra-large";
189
+ default:
190
+ return "medium";
191
+ }
192
+ }, K = (t) => {
193
+ switch (t?.toLowerCase()) {
194
+ case "available":
195
+ case "availableidle":
196
+ return "available";
197
+ case "away":
198
+ case "berightback":
199
+ return "away";
200
+ case "busy":
201
+ case "busyidle":
202
+ case "donotdisturb":
203
+ return "busy";
204
+ default:
205
+ return "offline";
206
+ }
207
+ }, ee = ({
208
+ userId: t,
209
+ userPrincipalName: e,
210
+ email: s,
211
+ personDetails: r,
212
+ view: n = "oneline",
213
+ showPresence: a = !1,
214
+ avatarSize: i = "medium",
215
+ textAlignment: c,
216
+ textPosition: p,
217
+ numericSize: d,
218
+ fetchImage: h = !0,
219
+ onClick: T,
220
+ className: g,
221
+ style: l
222
+ }) => {
223
+ const { user: u, presence: m, photoUrl: S, loading: f } = H({
224
+ userId: r ? void 0 : t || e || s,
225
+ fetchPresence: a,
226
+ fetchPhoto: h
227
+ }), y = r || u, A = d ?? R(i);
228
+ if (f)
229
+ return /* @__PURE__ */ w(
230
+ C,
231
+ {
232
+ size: A,
233
+ name: "Loading...",
234
+ textAlignment: c,
235
+ textPosition: p,
236
+ className: g,
237
+ style: l
238
+ }
239
+ );
240
+ if (!y)
241
+ return null;
242
+ const b = y.displayName || "Unknown User", L = B(b), k = {
243
+ size: A,
244
+ name: b,
245
+ textAlignment: c,
246
+ textPosition: p,
247
+ avatar: {
248
+ image: S ? { src: S } : void 0,
249
+ initials: S ? void 0 : L
250
+ },
251
+ className: g,
252
+ style: l
253
+ };
254
+ return a && m && (k.presence = {
255
+ status: K(m.availability)
256
+ }), n !== "avatar" && ((n === "twolines" || n === "threelines") && (k.secondaryText = y.jobTitle ?? void 0), n === "threelines" && (k.tertiaryText = y.department ?? void 0)), T && (k.onClick = () => {
257
+ u && T(u);
258
+ }), /* @__PURE__ */ w(C, { ...k });
259
+ };
260
+ function V(t) {
261
+ return `See https://aka.ms/msal.js.errors#${t} for details`;
262
+ }
263
+ class I extends Error {
264
+ constructor(e, s, r) {
265
+ const n = s || (e ? V(e) : ""), a = n ? `${e}: ${n}` : e;
266
+ super(a), Object.setPrototypeOf(this, I.prototype), this.errorCode = e || "", this.errorMessage = n || "", this.subError = r || "", this.name = "AuthError";
267
+ }
268
+ setCorrelationId(e) {
269
+ this.correlationId = e;
270
+ }
271
+ }
272
+ class P extends I {
273
+ constructor(e, s, r, n, a, i, c, p) {
274
+ super(e, s, r), Object.setPrototypeOf(this, P.prototype), this.timestamp = n || "", this.traceId = a || "", this.correlationId = i || "", this.claims = c || "", this.name = "InteractionRequiredAuthError", this.errorNo = p;
275
+ }
276
+ }
277
+ class te {
278
+ msalInstance;
279
+ defaultScopes;
280
+ account = null;
281
+ state = o.Loading;
282
+ listeners = [];
283
+ constructor(e, s) {
284
+ this.msalInstance = e, this.defaultScopes = s;
285
+ }
286
+ async initialize() {
287
+ await this.msalInstance.initialize(), await this.msalInstance.handleRedirectPromise();
288
+ const e = this.msalInstance.getActiveAccount(), s = this.msalInstance.getAllAccounts();
289
+ this.account = e ?? s[0] ?? null, this.account ? (this.msalInstance.setActiveAccount(this.account), this.setState(o.SignedIn)) : this.setState(o.SignedOut);
290
+ }
291
+ async login() {
292
+ const e = { scopes: this.defaultScopes }, s = await this.msalInstance.loginPopup(e);
293
+ this.account = s.account, this.account && this.msalInstance.setActiveAccount(this.account), this.setState(o.SignedIn);
294
+ }
295
+ async logout() {
296
+ const e = this.msalInstance.getActiveAccount() ?? this.account ?? void 0;
297
+ await this.msalInstance.logoutPopup({ account: e }), this.account = null, this.setState(o.SignedOut);
298
+ }
299
+ async getAccessToken(e) {
300
+ const s = e && e.length > 0 ? e : this.defaultScopes, r = this.msalInstance.getActiveAccount() ?? this.account;
301
+ if (!r)
302
+ throw new Error("No active account. User must sign in first.");
303
+ const n = {
304
+ scopes: s,
305
+ account: r
306
+ };
307
+ try {
308
+ return (await this.msalInstance.acquireTokenSilent(n)).accessToken;
309
+ } catch (a) {
310
+ if (a instanceof P) {
311
+ const i = {
312
+ scopes: s,
313
+ account: r
314
+ };
315
+ return (await this.msalInstance.acquireTokenPopup(i)).accessToken;
316
+ }
317
+ throw a;
318
+ }
319
+ }
320
+ getState() {
321
+ return this.state;
322
+ }
323
+ onStateChanged(e) {
324
+ this.listeners.push(e);
325
+ }
326
+ removeStateChangedHandler(e) {
327
+ this.listeners = this.listeners.filter((s) => s !== e);
328
+ }
329
+ setState(e) {
330
+ this.state = e, this.emit();
331
+ }
332
+ emit() {
333
+ for (const e of this.listeners)
334
+ try {
335
+ e();
336
+ } catch {
337
+ }
338
+ }
339
+ }
340
+ const J = 6e4;
341
+ function se(t) {
342
+ const e = t.fetchFn ?? fetch;
343
+ return async ({ ssoToken: s, scopes: r }) => {
344
+ const n = await e(t.endpoint, {
345
+ method: "POST",
346
+ headers: {
347
+ Authorization: `Bearer ${s}`,
348
+ "Content-Type": "application/json"
349
+ },
350
+ body: JSON.stringify({ scopes: r })
351
+ }), a = await n.json().catch(() => ({ error: n.statusText || "Token exchange failed" }));
352
+ if (!n.ok) {
353
+ const i = a?.error || a?.message || n.statusText;
354
+ throw new Error(`Backend token exchange failed: ${i}`);
355
+ }
356
+ if (!a?.accessToken)
357
+ throw new Error("Backend token exchange response did not include accessToken.");
358
+ return {
359
+ accessToken: a.accessToken,
360
+ expiresAt: a.expiresAt
361
+ };
362
+ };
363
+ }
364
+ class ne {
365
+ defaultScopes;
366
+ getTeamsSsoTokenFn;
367
+ exchangeForGraphTokenFn;
368
+ refreshSkewMs;
369
+ state = o.SignedOut;
370
+ listeners = [];
371
+ tokenCache = /* @__PURE__ */ new Map();
372
+ constructor(e) {
373
+ if (!e.defaultScopes || e.defaultScopes.length === 0)
374
+ throw new Error("TeamsHostedProvider requires at least one default scope.");
375
+ this.defaultScopes = [...new Set(e.defaultScopes)], this.getTeamsSsoTokenFn = e.getTeamsSsoToken, this.exchangeForGraphTokenFn = e.exchangeForGraphToken, this.refreshSkewMs = e.refreshSkewMs ?? J;
376
+ }
377
+ async login() {
378
+ try {
379
+ await this.getAccessToken(), this.setState(o.SignedIn);
380
+ } catch (e) {
381
+ throw this.clearCache(), this.setState(o.SignedOut), e;
382
+ }
383
+ }
384
+ async logout() {
385
+ this.clearCache(), this.setState(o.SignedOut);
386
+ }
387
+ async getAccessToken(e) {
388
+ const s = this.normalizeScopes(e), r = this.buildScopeKey(s), n = this.tokenCache.get(r);
389
+ if (n && this.isTokenValid(n))
390
+ return n.accessToken;
391
+ const a = await this.getTeamsSsoTokenFn(s);
392
+ if (!a)
393
+ throw new Error("Teams SSO token acquisition returned empty token.");
394
+ const i = await this.exchangeForGraphTokenFn({
395
+ ssoToken: a,
396
+ scopes: s
397
+ }), c = this.normalizeExchangedToken(i);
398
+ return this.tokenCache.set(r, c), this.setState(o.SignedIn), c.accessToken;
399
+ }
400
+ getState() {
401
+ return this.state;
402
+ }
403
+ onStateChanged(e) {
404
+ this.listeners.push(e);
405
+ }
406
+ removeStateChangedHandler(e) {
407
+ this.listeners = this.listeners.filter((s) => s !== e);
408
+ }
409
+ normalizeScopes(e) {
410
+ const s = e && e.length > 0 ? e : this.defaultScopes, r = [...new Set(s.filter(Boolean))];
411
+ if (r.length === 0)
412
+ throw new Error("At least one scope is required to request a Graph token.");
413
+ return r;
414
+ }
415
+ buildScopeKey(e) {
416
+ return [...e].sort().join(" ");
417
+ }
418
+ isTokenValid(e) {
419
+ return e.expiresAtMs ? Date.now() + this.refreshSkewMs < e.expiresAtMs : !0;
420
+ }
421
+ normalizeExchangedToken(e) {
422
+ if (typeof e == "string") {
423
+ if (!e)
424
+ throw new Error("Graph token exchange returned empty access token.");
425
+ return { accessToken: e };
426
+ }
427
+ if (!e.accessToken)
428
+ throw new Error("Graph token exchange returned empty access token.");
429
+ return {
430
+ accessToken: e.accessToken,
431
+ expiresAtMs: this.toEpochMs(e.expiresAt)
432
+ };
433
+ }
434
+ toEpochMs(e) {
435
+ if (e)
436
+ return e instanceof Date ? e.getTime() : e;
437
+ }
438
+ clearCache() {
439
+ this.tokenCache.clear();
440
+ }
441
+ setState(e) {
442
+ this.state !== e && (this.state = e, this.emit());
443
+ }
444
+ emit() {
445
+ for (const e of this.listeners)
446
+ try {
447
+ e();
448
+ } catch {
449
+ }
450
+ }
451
+ }
452
+ export {
453
+ Y as GraphProvider,
454
+ F as MockProvider,
455
+ te as MsalBrowserProvider,
456
+ ee as Person,
457
+ o as ProviderState,
458
+ ne as TeamsHostedProvider,
459
+ se as createBackendTokenExchange,
460
+ Z as createMockSignedInProvider,
461
+ D as useGraphClient,
462
+ H as usePersonData,
463
+ U as useProvider,
464
+ O as useProviderState
465
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Provider interface for Microsoft Graph authentication
3
+ */
4
+ export declare enum ProviderState {
5
+ Loading = "Loading",
6
+ SignedOut = "SignedOut",
7
+ SignedIn = "SignedIn"
8
+ }
9
+ export interface IProvider {
10
+ /**
11
+ * Get an access token for the specified scopes
12
+ */
13
+ getAccessToken(scopes?: string[]): Promise<string>;
14
+ /**
15
+ * Initiate login flow
16
+ */
17
+ login(): Promise<void>;
18
+ /**
19
+ * Initiate logout flow
20
+ */
21
+ logout(): Promise<void>;
22
+ /**
23
+ * Get the current provider state
24
+ */
25
+ getState(): ProviderState;
26
+ /**
27
+ * Event handler for state changes
28
+ */
29
+ onStateChanged?: (handler: () => void) => void;
30
+ }
31
+ //# sourceMappingURL=IProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IProvider.d.ts","sourceRoot":"","sources":["../../src/providers/IProvider.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,oBAAY,aAAa;IACvB,OAAO,YAAY;IACnB,SAAS,cAAc;IACvB,QAAQ,aAAa;CACtB;AAED,MAAM,WAAW,SAAS;IACxB;;OAEG;IACH,cAAc,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEnD;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB;;OAEG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAExB;;OAEG;IACH,QAAQ,IAAI,aAAa,CAAC;IAE1B;;OAEG;IACH,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CAChD"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * MockProvider - simple in-memory provider for development and tests
3
+ */
4
+ import { IProvider, ProviderState } from './IProvider';
5
+ export declare class MockProvider implements IProvider {
6
+ private state;
7
+ private listeners;
8
+ constructor(options?: {
9
+ autoSignIn?: boolean;
10
+ mockUserId?: string;
11
+ });
12
+ getAccessToken(): Promise<string>;
13
+ login(): Promise<void>;
14
+ logout(): Promise<void>;
15
+ getState(): ProviderState;
16
+ onStateChanged(handler: () => void): void;
17
+ removeStateChangedHandler(handler: () => void): void;
18
+ private emit;
19
+ }
20
+ export declare const createMockSignedInProvider: () => MockProvider;
21
+ //# sourceMappingURL=MockProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MockProvider.d.ts","sourceRoot":"","sources":["../../src/providers/MockProvider.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIvD,qBAAa,YAAa,YAAW,SAAS;IAC5C,OAAO,CAAC,KAAK,CAA0C;IACvD,OAAO,CAAC,SAAS,CAAuB;gBAC5B,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAMnE,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAQ3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAK7B,QAAQ,IAAI,aAAa;IAIzB,cAAc,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIzC,yBAAyB,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIpD,OAAO,CAAC,IAAI;CASb;AAED,eAAO,MAAM,0BAA0B,oBAA+C,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Native MSAL browser provider for Graph Toolkit React
3
+ */
4
+ import { PublicClientApplication } from '@azure/msal-browser';
5
+ import { IProvider, ProviderState } from './IProvider';
6
+ export declare class MsalBrowserProvider implements IProvider {
7
+ private readonly msalInstance;
8
+ private readonly defaultScopes;
9
+ private account;
10
+ private state;
11
+ private listeners;
12
+ constructor(msalInstance: PublicClientApplication, defaultScopes: string[]);
13
+ initialize(): Promise<void>;
14
+ login(): Promise<void>;
15
+ logout(): Promise<void>;
16
+ getAccessToken(scopes?: string[]): Promise<string>;
17
+ getState(): ProviderState;
18
+ onStateChanged(handler: () => void): void;
19
+ removeStateChangedHandler(handler: () => void): void;
20
+ private setState;
21
+ private emit;
22
+ }
23
+ //# sourceMappingURL=MsalBrowserProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MsalBrowserProvider.d.ts","sourceRoot":"","sources":["../../src/providers/MsalBrowserProvider.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAIH,uBAAuB,EAE1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEvD,qBAAa,mBAAoB,YAAW,SAAS;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA0B;IACvD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAW;IACzC,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,KAAK,CAAwC;IACrD,OAAO,CAAC,SAAS,CAAyB;gBAE9B,YAAY,EAAE,uBAAuB,EAAE,aAAa,EAAE,MAAM,EAAE;IAKpE,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAYtB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAOvB,cAAc,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IA8BxD,QAAQ,IAAI,aAAa;IAIzB,cAAc,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIzC,yBAAyB,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIpD,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,IAAI;CASf"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * React Context for Graph Provider
3
+ */
4
+ import React, { ReactNode } from 'react';
5
+ import { IProvider, ProviderState } from './IProvider';
6
+ export interface GraphProviderProps {
7
+ provider: IProvider;
8
+ children: ReactNode;
9
+ }
10
+ export declare const GraphProvider: React.FC<GraphProviderProps>;
11
+ /**
12
+ * Hook to access the current Graph provider
13
+ */
14
+ export declare const useProvider: () => IProvider | null;
15
+ /**
16
+ * Hook to access the current provider state
17
+ */
18
+ export declare const useProviderState: () => ProviderState;
19
+ //# sourceMappingURL=ProviderContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProviderContext.d.ts","sourceRoot":"","sources":["../../src/providers/ProviderContext.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,EAIZ,SAAS,EAGV,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAYvD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA2BtD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,QAAO,SAAS,GAAG,IAG1C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,gBAAgB,QAAO,aAGnC,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { IProvider, ProviderState } from './IProvider';
2
+ export interface ExchangeForGraphTokenParams {
3
+ ssoToken: string;
4
+ scopes: string[];
5
+ }
6
+ export interface ExchangedGraphToken {
7
+ accessToken: string;
8
+ expiresAt?: number | Date;
9
+ }
10
+ export interface TeamsHostedProviderOptions {
11
+ defaultScopes: string[];
12
+ getTeamsSsoToken: (scopes: string[]) => Promise<string>;
13
+ exchangeForGraphToken: (params: ExchangeForGraphTokenParams) => Promise<string | ExchangedGraphToken>;
14
+ refreshSkewMs?: number;
15
+ }
16
+ export interface BackendTokenExchangeResponse extends ExchangedGraphToken {
17
+ accessToken: string;
18
+ }
19
+ export interface CreateBackendTokenExchangeOptions {
20
+ endpoint: string;
21
+ fetchFn?: typeof fetch;
22
+ }
23
+ /**
24
+ * Creates a typed callback for exchanging a Teams SSO token with a backend
25
+ * endpoint that performs OBO and returns a Graph token.
26
+ */
27
+ export declare function createBackendTokenExchange(options: CreateBackendTokenExchangeOptions): ({ ssoToken, scopes }: ExchangeForGraphTokenParams) => Promise<BackendTokenExchangeResponse>;
28
+ /**
29
+ * Teams-hosted auth provider for Graph Toolkit React.
30
+ *
31
+ * This provider does not perform Teams interactive login itself.
32
+ * The consumer application must handle TeamsJS login/token acquisition and
33
+ * provide callbacks for SSO token retrieval and backend Graph token exchange.
34
+ */
35
+ export declare class TeamsHostedProvider implements IProvider {
36
+ private readonly defaultScopes;
37
+ private readonly getTeamsSsoTokenFn;
38
+ private readonly exchangeForGraphTokenFn;
39
+ private readonly refreshSkewMs;
40
+ private state;
41
+ private listeners;
42
+ private readonly tokenCache;
43
+ constructor(options: TeamsHostedProviderOptions);
44
+ login(): Promise<void>;
45
+ logout(): Promise<void>;
46
+ getAccessToken(scopes?: string[]): Promise<string>;
47
+ getState(): ProviderState;
48
+ onStateChanged(handler: () => void): void;
49
+ removeStateChangedHandler(handler: () => void): void;
50
+ private normalizeScopes;
51
+ private buildScopeKey;
52
+ private isTokenValid;
53
+ private normalizeExchangedToken;
54
+ private toEpochMs;
55
+ private clearCache;
56
+ private setState;
57
+ private emit;
58
+ }
59
+ //# sourceMappingURL=TeamsHostedProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TeamsHostedProvider.d.ts","sourceRoot":"","sources":["../../src/providers/TeamsHostedProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIvD,MAAM,WAAW,2BAA2B;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA0B;IACvC,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACxD,qBAAqB,EAAE,CACnB,MAAM,EAAE,2BAA2B,KAClC,OAAO,CAAC,MAAM,GAAG,mBAAmB,CAAC,CAAC;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,4BAA6B,SAAQ,mBAAmB;IACrE,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iCAAiC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AASD;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,iCAAiC,IAGnE,sBAAsB,2BAA2B,KAAG,OAAO,CAAC,4BAA4B,CAAC,CA4B1G;AAED;;;;;;GAMG;AACH,qBAAa,mBAAoB,YAAW,SAAS;IACjD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAW;IACzC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAwC;IAC3E,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAEG;IAC3C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,KAAK,CAA0C;IACvD,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;gBAEjD,OAAO,EAAE,0BAA0B;IAWzC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAWtB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAKvB,cAAc,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IA0BxD,QAAQ,IAAI,aAAa;IAIzB,cAAc,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIzC,yBAAyB,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIpD,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,uBAAuB;IAmB/B,OAAO,CAAC,SAAS;IAYjB,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,IAAI;CASf"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Graph utility functions
3
+ */
4
+ import { Client } from '@microsoft/microsoft-graph-client';
5
+ import { IProvider } from '../providers/IProvider';
6
+ /**
7
+ * Create a Graph client from a provider
8
+ */
9
+ export declare const createGraphClient: (provider: IProvider) => Client;
10
+ /**
11
+ * Extract initials from a display name
12
+ */
13
+ export declare const getInitials: (displayName?: string) => string;
14
+ //# sourceMappingURL=graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/utils/graph.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,SAAS,KAAG,MAWvD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,cAAc,MAAM,KAAG,MASlD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "@devsym/graph-toolkit-react",
3
+ "version": "0.1.0-next.2",
4
+ "description": "React components for Microsoft Graph powered by Fluent UI",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "build": "vite build && tsc --emitDeclarationOnly",
22
+ "dev": "vite build --watch",
23
+ "changeset": "changeset",
24
+ "version-packages": "changeset version",
25
+ "publish-packages": "changeset publish",
26
+ "type-check": "tsc --noEmit",
27
+ "lint": "eslint src --ext .ts,.tsx",
28
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "test:coverage": "vitest run --coverage",
32
+ "prettier:check": "prettier --check \"src/**/*.{ts,tsx}\"",
33
+ "prettier:write": "prettier --write \"src/**/*.{ts,tsx}\"",
34
+ "storybook": "storybook dev -p 6006",
35
+ "build-storybook": "storybook build --output-dir storybook-static"
36
+ },
37
+ "keywords": [
38
+ "react",
39
+ "microsoft-graph",
40
+ "fluent-ui",
41
+ "components",
42
+ "graph-toolkit"
43
+ ],
44
+ "author": "",
45
+ "license": "MIT",
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "peerDependencies": {
50
+ "@azure/msal-browser": "^4.0.0 || ^5.0.0",
51
+ "@fluentui/react-components": "^9.0.0",
52
+ "react": "^18.0.0 || ^19.0.0",
53
+ "react-dom": "^18.0.0 || ^19.0.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@azure/msal-browser": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "dependencies": {
61
+ "@microsoft/microsoft-graph-client": "^3.0.7",
62
+ "@microsoft/microsoft-graph-types": "^2.40.0"
63
+ },
64
+ "devDependencies": {
65
+ "@changesets/cli": "^2.29.7",
66
+ "@azure/msal-browser": "^5.2.0",
67
+ "@fluentui/react-components": "^9.72.6",
68
+ "@storybook/addon-a11y": "^10.0.6",
69
+ "@storybook/addon-docs": "^10.0.6",
70
+ "@storybook/addon-links": "^10.0.6",
71
+ "@storybook/react-vite": "^10.0.6",
72
+ "@testing-library/react": "^16.3.0",
73
+ "@types/react": "^18.2.0",
74
+ "@types/react-dom": "^18.2.0",
75
+ "@typescript-eslint/eslint-plugin": "^8.46.3",
76
+ "@typescript-eslint/parser": "^8.46.3",
77
+ "@vitejs/plugin-react": "^5.1.0",
78
+ "eslint": "^9.39.1",
79
+ "eslint-config-prettier": "^10.1.8",
80
+ "eslint-plugin-react": "^7.33.2",
81
+ "eslint-plugin-react-hooks": "^7.0.1",
82
+ "eslint-plugin-storybook": "^10.2.8",
83
+ "globals": "^17.3.0",
84
+ "jsdom": "^27.1.0",
85
+ "prettier": "^3.2.5",
86
+ "react": "^18.2.0",
87
+ "react-dom": "^18.2.0",
88
+ "storybook": "^10.0.6",
89
+ "typescript": "^5.9.3",
90
+ "vite": "^7.2.2",
91
+ "vite-plugin-dts": "^4.5.4",
92
+ "vitest": "^4.0.18"
93
+ }
94
+ }
package/readme.md ADDED
@@ -0,0 +1,355 @@
1
+ <h1 align="center">
2
+ Graph Toolkit React
3
+ </h1>
4
+
5
+ <h4 align="center">Modern React Components for <a href="https://graph.microsoft.com">Microsoft Graph</a> powered by <a href="https://react.fluentui.dev/">Fluent UI</a></h4>
6
+
7
+ <p align="center">
8
+ <img src="https://img.shields.io/badge/alpha-0.1.0-orange" alt="Alpha Release">
9
+ <img src="https://img.shields.io/badge/React-18%2F19-blue" alt="React 18/19">
10
+ <img src="https://img.shields.io/badge/Fluent_UI-9.72-purple" alt="Fluent UI 9">
11
+ <img src="https://img.shields.io/badge/TypeScript-5.9-blue" alt="TypeScript 5.9">
12
+ </p>
13
+
14
+ <h3 align="center">
15
+ <a href="#getting-started">Get Started</a> •
16
+ <a href="#storybook">Storybook</a> •
17
+ <a href="#contribute">Contribute</a>
18
+ </h3>
19
+
20
+ ## 🎯 Project Vision
21
+
22
+ This project provides a **modern, React-first component library** built on top of Fluent UI to help React developers integrate with Microsoft Graph quickly and consistently.
23
+
24
+ > Note: This project is a spiritual successor to Microsoft Graph Toolkit, redesigned as a React-first library.
25
+
26
+ ### Key Principles
27
+
28
+ - **React-First**: Built specifically for React applications, not framework-agnostic web components
29
+ - **Fluent UI Native**: Uses official Fluent UI v9 components instead of custom implementations
30
+ - **Single Package**: One cohesive npm package instead of a complex monorepo
31
+ - **Modern Stack**: TypeScript 5, Vite, Vitest, and Storybook
32
+ - **Developer Experience**: Simple API, excellent TypeScript support, comprehensive documentation
33
+
34
+ ### Design Philosophy
35
+
36
+ Instead of maintaining the original web component approach, this fork:
37
+
38
+ 1. **Leverages Fluent UI**: Uses the official `Persona` component from Fluent UI instead of custom Avatar/Text components
39
+ 2. **Embraces React Patterns**: Components designed for React's patterns (hooks, context) from the ground up
40
+ 3. **Reduces Complexity**: Smaller codebase enables quicker development and easier maintenance
41
+ 4. **Focuses on Quality**: Fewer components, better implementation, comprehensive documentation
42
+
43
+ ## 📦 Package
44
+
45
+ ```bash
46
+ npm install @devsym/graph-toolkit-react
47
+ ```
48
+
49
+ | Package | Version | Description |
50
+ | ------- | ------- | ----------- |
51
+ | `@devsym/graph-toolkit-react` | `0.1.0-alpha.1` | React components for Microsoft Graph powered by Fluent UI |
52
+
53
+ ## 🎨 Components
54
+
55
+ Currently available in alpha:
56
+
57
+ ### Person Component
58
+ A flexible component for displaying user information from Microsoft Graph.
59
+
60
+ ```tsx
61
+ import { Person } from '@devsym/graph-toolkit-react';
62
+
63
+ <Person
64
+ userId="user@contoso.com"
65
+ view="twoLines"
66
+ showPresence
67
+ textAlignment="center"
68
+ onClick={() => console.log('Clicked!')}
69
+ />
70
+ ```
71
+
72
+
73
+ <a id="getting-started"></a>
74
+ ## 🚀 Getting Started
75
+
76
+ ### Prerequisites
77
+
78
+ ```json
79
+ {
80
+ "react": "^18.0.0 || ^19.0.0",
81
+ "react-dom": "^18.0.0 || ^19.0.0",
82
+ "@fluentui/react-components": "^9.0.0"
83
+ }
84
+ ```
85
+
86
+ ### Installation
87
+
88
+ ```bash
89
+ npm install --save @devsym/graph-toolkit-react
90
+ ```
91
+
92
+ Also install required peer dependencies in your app if they are not already present:
93
+
94
+ ```bash
95
+ npm install --save react react-dom @fluentui/react-components
96
+ ```
97
+
98
+ If you plan to use `MsalBrowserProvider`, also install:
99
+
100
+ ```bash
101
+ npm install --save @azure/msal-browser
102
+ ```
103
+
104
+ If you only use `TeamsHostedProvider` (Teams-hosted app with consumer-managed Teams auth), `@azure/msal-browser` is not required.
105
+
106
+ ### Quick Start
107
+
108
+ ```tsx
109
+ import { FluentProvider, webLightTheme } from '@fluentui/react-components';
110
+ import { GraphProvider, MockProvider, Person } from '@devsym/graph-toolkit-react';
111
+
112
+ function App() {
113
+ const provider = new MockProvider(); // Use MockProvider for development
114
+
115
+ return (
116
+ <FluentProvider theme={webLightTheme}>
117
+ <GraphProvider provider={provider}>
118
+ <Person userId="AdeleV@contoso.com" view="twoLines" showPresence />
119
+ </GraphProvider>
120
+ </FluentProvider>
121
+ );
122
+ }
123
+ ```
124
+
125
+ ### Authentication Providers
126
+
127
+ The library uses a provider pattern for authentication:
128
+
129
+ - **MockProvider**: Returns mock data (Adele Vance) without API calls - perfect for development
130
+ - **MsalBrowserProvider**: Native MSAL browser provider included in this package
131
+ - **TeamsHostedProvider**: For Teams-hosted apps using consumer-managed Teams login + backend Graph token exchange
132
+ - **Custom Providers**: Implement `IProvider` interface to integrate with other auth systems
133
+
134
+ #### `MsalBrowserProvider` quick start
135
+
136
+ ```tsx
137
+ import { PublicClientApplication } from '@azure/msal-browser';
138
+ import {
139
+ GraphProvider,
140
+ MsalBrowserProvider,
141
+ Person,
142
+ ProviderState,
143
+ } from '@devsym/graph-toolkit-react';
144
+
145
+ const msal = new PublicClientApplication({
146
+ auth: {
147
+ clientId: 'YOUR_CLIENT_ID',
148
+ authority: 'https://login.microsoftonline.com/common',
149
+ redirectUri: window.location.origin,
150
+ },
151
+ });
152
+
153
+ const provider = new MsalBrowserProvider(msal, ['User.Read']);
154
+ await provider.initialize();
155
+
156
+ function App() {
157
+ return (
158
+ <GraphProvider provider={provider}>
159
+ <Person userId="me" view="threelines" />
160
+ </GraphProvider>
161
+ );
162
+ }
163
+ ```
164
+
165
+ #### `TeamsHostedProvider` quick start (consumer-managed Teams login)
166
+
167
+ Use this when your app is hosted in Microsoft Teams and your app code already handles TeamsJS auth for multiple APIs.
168
+ The provider does not open Teams login popups itself; it uses callbacks you provide.
169
+
170
+ ```tsx
171
+ import {
172
+ createBackendTokenExchange,
173
+ GraphProvider,
174
+ Person,
175
+ TeamsHostedProvider,
176
+ } from '@devsym/graph-toolkit-react';
177
+ import { authentication } from '@microsoft/teams-js';
178
+
179
+ const exchangeForGraphToken = createBackendTokenExchange({
180
+ endpoint: '/api/token/exchange',
181
+ });
182
+
183
+ const provider = new TeamsHostedProvider({
184
+ defaultScopes: ['User.Read'],
185
+ getTeamsSsoToken: async (scopes) => {
186
+ // Consumer app owns Teams auth and can reuse token for other APIs
187
+ return authentication.getAuthToken({ resources: scopes });
188
+ },
189
+ exchangeForGraphToken,
190
+ });
191
+
192
+ await provider.login();
193
+
194
+ function App() {
195
+ return (
196
+ <GraphProvider provider={provider}>
197
+ <Person userId="me" view="threelines" />
198
+ </GraphProvider>
199
+ );
200
+ }
201
+ ```
202
+
203
+ Use explicit mode selection in your app:
204
+
205
+ - Browser-hosted app: instantiate `MsalBrowserProvider`
206
+ - Teams-hosted app: instantiate `TeamsHostedProvider`
207
+
208
+ #### Scopes by feature
209
+
210
+ | Feature | Minimum delegated scope | Notes |
211
+ | ------- | ------------------------ | ----- |
212
+ | Load current user profile (`userId="me"`) | `User.Read` | Required for basic person data (`displayName`, `mail`, etc.) |
213
+ | Load another user by id/upn (`userId="{id}"`) | `User.ReadBasic.All` | May require admin consent depending on tenant policy |
214
+ | Show presence (`showPresence`) | `Presence.Read` | If not granted, person still renders without presence |
215
+ | Load profile photo (`fetchImage`) | `User.Read` | Falls back to initials if photo is unavailable |
216
+
217
+ ```tsx
218
+ import { IProvider, ProviderState } from '@devsym/graph-toolkit-react';
219
+
220
+ class MyAuthProvider implements IProvider {
221
+ state = ProviderState.SignedOut;
222
+
223
+ async login(): Promise<void> { /* ... */ }
224
+ async logout(): Promise<void> { /* ... */ }
225
+ async getAccessToken(): Promise<string> { /* ... */ }
226
+ }
227
+ ```
228
+
229
+ <a id="storybook"></a>
230
+ ## 📚 Storybook
231
+
232
+ Explore all components interactively in our Storybook documentation:
233
+
234
+ **[View Storybook →](https://thomaspe.github.io/graph-toolkit-react/)** _(Deployed to GitHub Pages on every main branch update)_
235
+
236
+ Run locally:
237
+ ```bash
238
+ npm run storybook
239
+ ```
240
+
241
+ ## 🛠️ Development
242
+
243
+ ### Tech Stack
244
+
245
+ - **Build**: Vite 7 (ESM + CJS output)
246
+ - **Language**: TypeScript 5.9
247
+ - **Testing**: Vitest 3 + Testing Library
248
+ - **Documentation**: Storybook 10
249
+ - **UI Framework**: Fluent UI v9 (React Components)
250
+ - **Graph SDK**: @microsoft/microsoft-graph-client 3.0
251
+
252
+ ### Build & Test
253
+
254
+ ```bash
255
+ # Install dependencies
256
+ npm install
257
+
258
+ # Type check
259
+ npm run type-check
260
+
261
+ # Lint
262
+ npm run lint
263
+
264
+ # Build library
265
+ npm run build
266
+
267
+ # Run tests
268
+ npm run test
269
+
270
+ # Run Storybook
271
+ npm run storybook
272
+
273
+ # Build Storybook for deployment
274
+ npm run build-storybook
275
+ ```
276
+
277
+ ### Project Structure
278
+
279
+ ```
280
+ src/
281
+ ├── components/
282
+ │ └── Person/
283
+ │ ├── Person.tsx # Component implementation
284
+ │ ├── Person.types.ts # TypeScript definitions
285
+ │ └── __tests__/ # Component tests
286
+ ├── providers/
287
+ │ ├── IProvider.ts # Provider interface
288
+ │ ├── MockProvider.ts # Development provider
289
+ │ └── ProviderContext.tsx # React context
290
+ ├── hooks/
291
+ │ ├── useGraphClient.ts # Graph client hook
292
+ │ ├── usePersonData.ts # Person data fetching
293
+ │ └── useProvider.ts # Provider access
294
+ └── index.ts # Public API
295
+
296
+ stories/
297
+ └── Person.stories.tsx # Storybook stories (19 examples)
298
+ ```
299
+
300
+ ## 🎯 Current Status
301
+
302
+ **Alpha** ✅
303
+
304
+ - ✅ Repository restructured from monorepo to single package
305
+ - ✅ Provider infrastructure with MockProvider for development
306
+ - ✅ Person component using Fluent UI Persona
307
+ - ✅ Full Persona configuration support (textAlignment, textPosition, sizing)
308
+ - ✅ Build system (ESM + CJS + TypeScript declarations)
309
+ - ✅ Storybook documentation and GitHub Pages deployment
310
+ - ✅ CI/CD with GitHub Actions
311
+ - ✅ Automatic Storybook deployment to GitHub Pages
312
+ - ✅ Sample app with MSAL sign-in and `Person` (`userId="me"`) at `samples/react-msal-sample`
313
+
314
+ **Next Steps**:
315
+ - Additional components (PeoplePicker, PersonCard, Login)
316
+ - Expanded provider guidance and production auth examples
317
+ - Comprehensive test coverage
318
+ - First stable release (1.0.0)
319
+
320
+ <a id="contribute"></a>
321
+ ## 🤝 Contribute
322
+
323
+ This **community-driven project** provides React developers with components for Microsoft Graph. Contributions are welcome!
324
+
325
+ ### Guidelines
326
+
327
+ 1. Follow the existing code style (TypeScript, ESLint, Prettier)
328
+ 2. Use Fluent UI components whenever possible
329
+ 3. Write tests for new components
330
+ 4. Add Storybook stories demonstrating all component features
331
+ 5. Update documentation
332
+
333
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
334
+
335
+ ## 📝 License
336
+
337
+ MIT License - see [LICENSE](LICENSE) for details.
338
+
339
+ ## 🙏 Acknowledgments
340
+
341
+ - **Fluent UI Team**: For the excellent React component library
342
+ - **Microsoft Graph**: For the powerful API that makes this all possible
343
+ - **Community Contributors**: For helping improve this project
344
+
345
+ ---
346
+
347
+ **Note**: This is an alpha release under active development. APIs may change. Not recommended for production use yet.
348
+
349
+ ## 🔗 Links
350
+
351
+ - **Fluent UI**: [react.fluentui.dev](https://react.fluentui.dev/)
352
+ - **Microsoft Graph**: [graph.microsoft.com](https://graph.microsoft.com)
353
+ - **Changelog**: [CHANGELOG.md](CHANGELOG.md)
354
+ - **Publishing**: [PUBLISHING.md](PUBLISHING.md)
355
+