mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
Merge pull request #702 from logto-io/charles-log-2171-refactor-sdk-integration-guide-with-mdx
refactor(console): sdk integration guide with mdx
This commit is contained in:
commit
cd9ce73228
46 changed files with 1209 additions and 1625 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -17,6 +17,9 @@ node_modules
|
|||
/packages/*/lib
|
||||
/packages/*/dist
|
||||
|
||||
# docs copied to admin console
|
||||
/packages/console/**/*.mdx
|
||||
|
||||
# logs
|
||||
logs
|
||||
*.log*
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"precommit": "lint-staged",
|
||||
"copyfiles": "copyfiles -u 1 public/**/*.* dist",
|
||||
"copyfiles": "copyfiles -u 2 \"../docs/**/*.mdx\" src/assets",
|
||||
"start": "pnpm copyfiles && parcel src/index.html",
|
||||
"dev": "pnpm copyfiles && PORT=5002 parcel src/index.html --public-url /console --no-cache --hmr-port 6002",
|
||||
"check": "tsc --noEmit",
|
||||
|
@ -21,17 +21,40 @@
|
|||
"@logto/phrases": "^0.1.0",
|
||||
"@logto/react": "^0.1.3",
|
||||
"@logto/schemas": "^0.1.0",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@parcel/core": "^2.5.0",
|
||||
"@parcel/transformer-mdx": "^2.5.0",
|
||||
"@parcel/transformer-sass": "^2.5.0",
|
||||
"@silverhand/eslint-config": "^0.10.2",
|
||||
"@silverhand/eslint-config-react": "^0.10.3",
|
||||
"@silverhand/essentials": "^1.1.6",
|
||||
"@silverhand/ts-config": "^0.10.2",
|
||||
"@silverhand/ts-config-react": "^0.10.3",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"@types/lodash.kebabcase": "^4.1.6",
|
||||
"@types/mdx": "^2.0.1",
|
||||
"@types/mdx-js__react": "^1.5.5",
|
||||
"@types/react": "^17.0.14",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"classnames": "^2.3.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"csstype": "^3.0.11",
|
||||
"dnd-core": "^16.0.0",
|
||||
"eslint": "^8.10.0",
|
||||
"i18next": "^21.6.12",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
"ky": "^0.30.0",
|
||||
"lint-staged": "^12.0.0",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"nanoid": "^3.1.23",
|
||||
"parcel": "^2.5.0",
|
||||
"postcss": "^8.4.6",
|
||||
"postcss-modules": "^4.3.0",
|
||||
"prettier": "^2.3.2",
|
||||
"process": "^0.11.10",
|
||||
"react": "^17.0.2",
|
||||
"react-dnd": "^16.0.0",
|
||||
"react-dnd-html5-backend": "^16.0.0",
|
||||
|
@ -44,30 +67,13 @@
|
|||
"react-paginate": "^8.1.2",
|
||||
"react-router-dom": "^6.2.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"swr": "^1.2.2",
|
||||
"@parcel/core": "^2.3.2",
|
||||
"@parcel/transformer-sass": "^2.3.2",
|
||||
"@silverhand/eslint-config": "^0.10.2",
|
||||
"@silverhand/eslint-config-react": "^0.10.3",
|
||||
"@silverhand/ts-config": "^0.10.2",
|
||||
"@silverhand/ts-config-react": "^0.10.3",
|
||||
"@types/lodash.kebabcase": "^4.1.6",
|
||||
"@types/react": "^17.0.14",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"eslint": "^8.10.0",
|
||||
"lint-staged": "^12.0.0",
|
||||
"parcel": "^2.3.2",
|
||||
"postcss": "^8.4.6",
|
||||
"postcss-modules": "^4.3.0",
|
||||
"prettier": "^2.3.2",
|
||||
"process": "^0.11.10",
|
||||
"stylelint": "^13.13.1",
|
||||
"swr": "^1.2.2",
|
||||
"typescript": "^4.6.2"
|
||||
},
|
||||
"alias": {
|
||||
"@/*": "./src/$1"
|
||||
"@/*": "./src/$1",
|
||||
"@theme/*": "./src/mdx-components/$1"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@silverhand/react"
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"files": ["step1.md", "step2.md", "step3.md", "step4.md", "step5.md"]
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
---
|
||||
title: Install Logto SDK
|
||||
subtitle: 1 step
|
||||
---
|
||||
## Option 1: Install your SDK dependency
|
||||
|
||||
Run the CLI command under your project's root directory.
|
||||
|
||||
```
|
||||
// installation with npm
|
||||
npm install @logto/react --save
|
||||
|
||||
// installation with yarn
|
||||
yarn add @logto/react
|
||||
|
||||
// installation with pnpm
|
||||
pnpm install @logto/react --save
|
||||
```
|
||||
|
||||
## Option 2: Add script tag to your HTML
|
||||
|
||||
```
|
||||
<script src="https://logto.io/js/logto-sdk-react/0.1.0/logto-sdk-react.production.js"></script>
|
||||
```
|
||||
|
||||
## Option 3: Fork your own from github
|
||||
|
||||
```
|
||||
git clone https://github.com/logto-io/js.git
|
||||
```
|
||||
|
||||
```
|
||||
pnpm build
|
||||
```
|
|
@ -1,35 +0,0 @@
|
|||
---
|
||||
title: Initiate LogtoClient
|
||||
subtitle: 1 step | Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
|
||||
---
|
||||
Add the following code to your main html file. You may need client ID and authorization domain.
|
||||
|
||||
```typescript
|
||||
import { LogtoProvider, LogtoConfig } from '@logto/react';
|
||||
import React from 'react';
|
||||
|
||||
...
|
||||
|
||||
const App = () => {
|
||||
const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' }
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<LogtoProvider config={config}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/callback" element={<Callback />} />
|
||||
<Route
|
||||
path="/protected-resource"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<ProtectedResource />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</LogtoProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
```
|
|
@ -1,47 +0,0 @@
|
|||
---
|
||||
title: Sign In
|
||||
subtitle: 2 steps
|
||||
---
|
||||
## Step 1: Setup your login
|
||||
|
||||
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, let’s enter your redirect URI
|
||||
|
||||
```redirectUris
|
||||
Redirect URI
|
||||
```
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
```typescript
|
||||
import React from "react";
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const SignInButton = () => {
|
||||
const { signIn } = useLogto();
|
||||
const redirectUrl = window.location.origin + '/callback';
|
||||
|
||||
return <button onClick={() => signIn(redirectUrl)}>Sign In</button>;
|
||||
};
|
||||
|
||||
export default SignInButton;
|
||||
```
|
||||
|
||||
## Step 2: Retrieve Auth Status
|
||||
|
||||
```typescript
|
||||
import React from "react";
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const App = () => {
|
||||
const { isAuthenticated, signIn } = useLogto();
|
||||
|
||||
if !(isAuthenticated) {
|
||||
return <SignInButton />
|
||||
}
|
||||
|
||||
return <>
|
||||
<AppContent />
|
||||
<SignOutButton />
|
||||
</>
|
||||
};
|
||||
```
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
title: Sign Out
|
||||
subtitle: 1 steps
|
||||
---
|
||||
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
|
||||
|
||||
```postLogoutRedirectUris
|
||||
Post sign out redirect URI
|
||||
```
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
```typescript
|
||||
import React from "react";
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const SignOutButton = () => {
|
||||
const { signOut } = useLogto();
|
||||
|
||||
return (
|
||||
<button onClick={() => signOut(window.location.origin)}>Sign out</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignOutButton;
|
||||
```
|
|
@ -1,15 +0,0 @@
|
|||
---
|
||||
title: Advanced Settings & Documentation Links
|
||||
subtitle: 2 steps
|
||||
---
|
||||
## Step 1: Advanced Settings
|
||||
|
||||
Go to application details page and switch to advanced settings tab
|
||||
|
||||
## Step 2: Now check out the documentation links below
|
||||
|
||||
[- SDK Documentation](https://link-url-here.org)
|
||||
|
||||
[- OIDC Documentation](https://link-url-here.org)
|
||||
|
||||
[- Calling API to fetch accessToken](https://link-url-here.org)
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"files": ["step1.md", "step2.md", "step3.md", "step4.md", "step5.md"]
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
---
|
||||
title: 安装 Logto SDK
|
||||
subtitle: 1 step
|
||||
---
|
||||
## 方案1: 安装 SDK 依赖包
|
||||
|
||||
Run the CLI command under your project's root directory.
|
||||
|
||||
```
|
||||
// installation with npm
|
||||
npm install @logto/react --save
|
||||
|
||||
// installation with yarn
|
||||
yarn add @logto/react
|
||||
|
||||
// installation with pnpm
|
||||
pnpm install @logto/react --save
|
||||
```
|
||||
|
||||
## 方案2: Add script tag to your HTML
|
||||
|
||||
```
|
||||
<script src="https://logto.io/js/logto-sdk-react/0.1.0/logto-sdk-react.production.js"></script>
|
||||
```
|
||||
|
||||
## 方案3: Fork your own from github
|
||||
|
||||
```
|
||||
git clone https://github.com/logto-io/js.git
|
||||
```
|
||||
|
||||
```
|
||||
pnpm build
|
||||
```
|
|
@ -1,35 +0,0 @@
|
|||
---
|
||||
title: 初始化 LogtoClient
|
||||
subtitle: 1 step | Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
|
||||
---
|
||||
Add the following code to your main html file. You may need client ID and authorization domain.
|
||||
|
||||
```typescript
|
||||
import { LogtoProvider, LogtoConfig } from '@logto/react';
|
||||
import React from 'react';
|
||||
|
||||
...
|
||||
|
||||
const App = () => {
|
||||
const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' }
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<LogtoProvider config={config}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/callback" element={<Callback />} />
|
||||
<Route
|
||||
path="/protected-resource"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<ProtectedResource />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</LogtoProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
```
|
|
@ -1,47 +0,0 @@
|
|||
---
|
||||
title: 登录
|
||||
subtitle: 2 steps
|
||||
---
|
||||
## Step 1: Setup your login
|
||||
|
||||
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, let’s enter your redirect URI
|
||||
|
||||
```redirectUris
|
||||
Redirect URI
|
||||
```
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
```typescript
|
||||
import React from "react";
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const SignInButton = () => {
|
||||
const { signIn } = useLogto();
|
||||
const redirectUrl = window.location.origin + '/callback';
|
||||
|
||||
return <button onClick={() => signIn(redirectUrl)}>Sign In</button>;
|
||||
};
|
||||
|
||||
export default SignInButton;
|
||||
```
|
||||
|
||||
## Step 2: Retrieve Auth Status
|
||||
|
||||
```typescript
|
||||
import React from "react";
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const App = () => {
|
||||
const { isAuthenticated, signIn } = useLogto();
|
||||
|
||||
if !(isAuthenticated) {
|
||||
return <SignInButton />
|
||||
}
|
||||
|
||||
return <>
|
||||
<AppContent />
|
||||
<SignOutButton />
|
||||
</>
|
||||
};
|
||||
```
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
title: 登出
|
||||
subtitle: 1 steps
|
||||
---
|
||||
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
|
||||
|
||||
```postLogoutRedirectUris
|
||||
Post sign out redirect URI
|
||||
```
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
```typescript
|
||||
import React from "react";
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const SignOutButton = () => {
|
||||
const { signOut } = useLogto();
|
||||
|
||||
return (
|
||||
<button onClick={() => signOut(window.location.origin)}>Sign out</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignOutButton;
|
||||
```
|
|
@ -1,15 +0,0 @@
|
|||
---
|
||||
title: 高级设置以及相关文档链接
|
||||
subtitle: 2 steps
|
||||
---
|
||||
## Step 1: Advanced Settings
|
||||
|
||||
Go to application details page and switch to advanced settings tab
|
||||
|
||||
## Step 2: Now check out the documentation links below
|
||||
|
||||
[- SDK Documentation](https://link-url-here.org)
|
||||
|
||||
[- OIDC Documentation](https://link-url-here.org)
|
||||
|
||||
[- Calling API to fetch accessToken](https://link-url-here.org)
|
25
packages/console/src/mdx-components/TabItem/index.tsx
Normal file
25
packages/console/src/mdx-components/TabItem/index.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
export type Props = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
value: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
const TabItem = ({ children, ...rest }: Props): JSX.Element => {
|
||||
return (
|
||||
<div role="tabpanel" {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabItem;
|
108
packages/console/src/mdx-components/Tabs/index.tsx
Normal file
108
packages/console/src/mdx-components/Tabs/index.tsx
Normal file
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { Nullable } from '@silverhand/essentials';
|
||||
import React, { useState, isValidElement, type ReactElement, cloneElement } from 'react';
|
||||
|
||||
import type { Props as TabItemProps } from '../TabItem';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
children: ReactElement<TabItemProps>;
|
||||
};
|
||||
|
||||
// A very rough duck type, but good enough to guard against mistakes while
|
||||
// allowing customization
|
||||
function isTabItem(comp: ReactElement): comp is ReactElement<TabItemProps> {
|
||||
return typeof comp.props.value !== 'undefined';
|
||||
}
|
||||
|
||||
const Tabs = ({ className, children }: Props): JSX.Element => {
|
||||
const verifiedChildren = React.Children.map(children, (child) => {
|
||||
if (isValidElement(child) && isTabItem(child)) {
|
||||
return child;
|
||||
}
|
||||
});
|
||||
|
||||
const values =
|
||||
// Only pick keys that we recognize. MDX would inject some keys by default
|
||||
verifiedChildren.map(({ props: { value, label } }) => ({
|
||||
value,
|
||||
label,
|
||||
}));
|
||||
|
||||
const [selectedValue, setSelectedValue] = useState<string>();
|
||||
const tabReferences: Array<Nullable<HTMLLIElement>> = [];
|
||||
|
||||
const handleTabChange = (
|
||||
event: React.FocusEvent<HTMLLIElement> | React.MouseEvent<HTMLLIElement>
|
||||
) => {
|
||||
const newTab = event.currentTarget;
|
||||
const newTabIndex = tabReferences.indexOf(newTab);
|
||||
const newTabValue = values[newTabIndex]?.value;
|
||||
|
||||
if (newTabValue !== selectedValue) {
|
||||
setSelectedValue(newTabValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeydown = (event: React.KeyboardEvent<HTMLLIElement>) => {
|
||||
// eslint-disable-next-line @silverhand/fp/no-let
|
||||
let focusElement: Nullable<HTMLLIElement> = null;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowRight': {
|
||||
const nextTab = tabReferences.indexOf(event.currentTarget) + 1;
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
focusElement = tabReferences[nextTab] ?? tabReferences[0] ?? null;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ArrowLeft': {
|
||||
const previousTab = tabReferences.indexOf(event.currentTarget) - 1;
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
focusElement =
|
||||
tabReferences[previousTab] ?? tabReferences[tabReferences.length - 1] ?? null;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
focusElement?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ul role="tablist" aria-orientation="horizontal" className={className}>
|
||||
{values.map(({ value, label }) => (
|
||||
<li
|
||||
key={value}
|
||||
ref={(tabControl) => tabReferences.concat(tabControl)}
|
||||
role="tab"
|
||||
tabIndex={selectedValue === value ? 0 : -1}
|
||||
aria-selected={selectedValue === value}
|
||||
onKeyDown={handleKeydown}
|
||||
onFocus={handleTabChange}
|
||||
onClick={handleTabChange}
|
||||
>
|
||||
{label ?? value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
{verifiedChildren.map((tabItem) =>
|
||||
cloneElement(tabItem, {
|
||||
key: tabItem.props.value,
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tabs;
|
|
@ -10,9 +10,9 @@ import RadioGroup, { Radio } from '@/components/RadioGroup';
|
|||
import TextInput from '@/components/TextInput';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { applicationTypeI18nKey } from '@/types/applications';
|
||||
import { GetStartedForm } from '@/types/get-started';
|
||||
import { GuideForm } from '@/types/guide';
|
||||
|
||||
import GetStartedModal from '../GetStartedModal';
|
||||
import GuideModal from '../GuideModal';
|
||||
import TypeDescription from '../TypeDescription';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -57,7 +57,7 @@ const CreateForm = ({ onClose }: Props) => {
|
|||
setIsGetStartedModalOpen(true);
|
||||
});
|
||||
|
||||
const onComplete = async (data: GetStartedForm) => {
|
||||
const onComplete = async (data: GuideForm) => {
|
||||
if (!createdApp) {
|
||||
return;
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ const CreateForm = ({ onClose }: Props) => {
|
|||
</FormField>
|
||||
</form>
|
||||
{createdApp && (
|
||||
<GetStartedModal
|
||||
<GuideModal
|
||||
appName={createdApp.name}
|
||||
isOpen={isGetStartedModalOpen}
|
||||
onClose={closeModal}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
import GetStarted from '@/pages/GetStarted';
|
||||
import Guide from '@/pages/Guide';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { SupportedJavascriptLibraries } from '@/types/applications';
|
||||
import { GetStartedForm } from '@/types/get-started';
|
||||
import { GuideForm } from '@/types/guide';
|
||||
|
||||
import LibrarySelector from '../LibrarySelector';
|
||||
|
||||
|
@ -12,16 +12,15 @@ type Props = {
|
|||
appName: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onComplete: (data: GetStartedForm) => Promise<void>;
|
||||
onComplete: (data: GuideForm) => Promise<void>;
|
||||
};
|
||||
|
||||
const GetStartedModal = ({ appName, isOpen, onClose, onComplete }: Props) => (
|
||||
const GuideModal = ({ appName, isOpen, onClose, onComplete }: Props) => (
|
||||
<Modal isOpen={isOpen} className={modalStyles.fullScreen}>
|
||||
<GetStarted
|
||||
<Guide
|
||||
bannerComponent={<LibrarySelector />}
|
||||
title={appName}
|
||||
subtitle="applications.get_started.header_description"
|
||||
type="application"
|
||||
defaultSubtype={SupportedJavascriptLibraries.React}
|
||||
onClose={onClose}
|
||||
onComplete={onComplete}
|
||||
|
@ -29,4 +28,4 @@ const GetStartedModal = ({ appName, isOpen, onClose, onComplete }: Props) => (
|
|||
</Modal>
|
||||
);
|
||||
|
||||
export default GetStartedModal;
|
||||
export default GuideModal;
|
|
@ -11,7 +11,7 @@ import UnnamedTrans from '@/components/UnnamedTrans';
|
|||
import { RequestError } from '@/hooks/use-api';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import GetStartedModal from '../GetStartedModal';
|
||||
import GuideModal from '../GuideModal';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
|
@ -101,7 +101,7 @@ const CreateForm = ({ onClose, isOpen: isFormOpen, type }: Props) => {
|
|||
</RadioGroup>
|
||||
)}
|
||||
{activeConnector && (
|
||||
<GetStartedModal
|
||||
<GuideModal
|
||||
connector={activeConnector}
|
||||
isOpen={isGetStartedModalOpen}
|
||||
onClose={closeModal}
|
||||
|
|
|
@ -16,9 +16,9 @@ import Spacer from '@/components/Spacer';
|
|||
import useApi from '@/hooks/use-api';
|
||||
import Close from '@/icons/Close';
|
||||
import SenderTester from '@/pages/ConnectorDetails/components/SenderTester';
|
||||
import Step from '@/pages/GetStarted/components/Step';
|
||||
import Step from '@/pages/Guide/components/Step';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { GetStartedForm } from '@/types/get-started';
|
||||
import { GuideForm } from '@/types/guide';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -26,7 +26,7 @@ type Props = {
|
|||
connector: ConnectorDTO;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onComplete?: (data: GetStartedForm) => Promise<void>;
|
||||
onComplete?: (data: GuideForm) => Promise<void>;
|
||||
};
|
||||
|
||||
const onClickFetchSampleProject = (name: string) => {
|
||||
|
@ -34,7 +34,7 @@ const onClickFetchSampleProject = (name: string) => {
|
|||
window.open(sampleUrl, '_blank');
|
||||
};
|
||||
|
||||
const GetStartedModal = ({ connector, isOpen, onClose }: Props) => {
|
||||
const GuideModal = ({ connector, isOpen, onClose }: Props) => {
|
||||
const api = useApi();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
|
@ -48,7 +48,7 @@ const GetStartedModal = ({ connector, isOpen, onClose }: Props) => {
|
|||
const isSocialConnector =
|
||||
connectorType !== ConnectorType.SMS && connectorType !== ConnectorType.Email;
|
||||
const [activeStepIndex, setActiveStepIndex] = useState<number>(0);
|
||||
const methods = useForm<GetStartedForm>({ reValidateMode: 'onBlur' });
|
||||
const methods = useForm<GuideForm>({ reValidateMode: 'onBlur' });
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
|
@ -126,9 +126,7 @@ const GetStartedModal = ({ connector, isOpen, onClose }: Props) => {
|
|||
title="Enter your json here"
|
||||
subtitle="Lorem ipsum dolor sit amet, consectetuer adipiscing elit."
|
||||
index={0}
|
||||
isActive={activeStepIndex === 0}
|
||||
isComplete={activeStepIndex > 0}
|
||||
isFinalStep={isSocialConnector}
|
||||
activeIndex={activeStepIndex}
|
||||
buttonHtmlType="submit"
|
||||
>
|
||||
<Controller
|
||||
|
@ -144,13 +142,12 @@ const GetStartedModal = ({ connector, isOpen, onClose }: Props) => {
|
|||
</FormProvider>
|
||||
{!isSocialConnector && (
|
||||
<Step
|
||||
isFinalStep
|
||||
title="Test your message"
|
||||
subtitle="Lorem ipsum dolor sit amet, consectetuer adipiscing elit."
|
||||
index={1}
|
||||
isActive={activeStepIndex === 1}
|
||||
isComplete={activeStepIndex > 1}
|
||||
activeIndex={activeStepIndex}
|
||||
buttonHtmlType="button"
|
||||
buttonText="general.done"
|
||||
onNext={onClose}
|
||||
>
|
||||
<SenderTester connectorType={connectorType} />
|
||||
|
@ -163,4 +160,4 @@ const GetStartedModal = ({ connector, isOpen, onClose }: Props) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default GetStartedModal;
|
||||
export default GuideModal;
|
|
@ -1,79 +0,0 @@
|
|||
import React, { PropsWithChildren, useEffect, useRef } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CodeProps } from 'react-markdown/lib/ast-to-react.js';
|
||||
|
||||
import CodeEditor from '@/components/CodeEditor';
|
||||
import DangerousRaw from '@/components/DangerousRaw';
|
||||
import FormField from '@/components/FormField';
|
||||
import MultiTextInput from '@/components/MultiTextInput';
|
||||
import { createValidatorForRhf, convertRhfErrorMessage } from '@/components/MultiTextInput/utils';
|
||||
import { GetStartedForm } from '@/types/get-started';
|
||||
import { noSpaceRegex } from '@/utilities/regex';
|
||||
|
||||
type Props = PropsWithChildren<CodeProps> & { onError: () => void };
|
||||
|
||||
const CodeComponentRenderer = ({ className, children, onError }: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<GetStartedForm>();
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const [, codeBlockType] = /language-(\w+)/.exec(className ?? '') ?? [];
|
||||
const content = String(children);
|
||||
|
||||
/** Code block types defined in markdown. E.g.
|
||||
* ```typescript
|
||||
* some code
|
||||
* ```
|
||||
* These two custom code block types should be replaced with `MultiTextInput` component:
|
||||
* 'redirectUris' and 'postLogoutRedirectUris'
|
||||
*/
|
||||
const isMultilineInput =
|
||||
codeBlockType === 'redirectUris' || codeBlockType === 'postLogoutRedirectUris';
|
||||
|
||||
const firstErrorKey = Object.keys(errors)[0];
|
||||
const isFirstErrorField = firstErrorKey && firstErrorKey === codeBlockType;
|
||||
|
||||
useEffect(() => {
|
||||
if (isFirstErrorField) {
|
||||
onError();
|
||||
}
|
||||
}, [isFirstErrorField, onError]);
|
||||
|
||||
if (isMultilineInput) {
|
||||
return (
|
||||
<FormField isRequired title={<DangerousRaw>{content}</DangerousRaw>}>
|
||||
<Controller
|
||||
name={codeBlockType}
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
rules={{
|
||||
validate: createValidatorForRhf({
|
||||
required: t('errors.required_field_missing_plural', { field: content }),
|
||||
pattern: {
|
||||
regex: noSpaceRegex,
|
||||
message: t('errors.no_space_in_uri'),
|
||||
},
|
||||
}),
|
||||
}}
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<div ref={ref}>
|
||||
<MultiTextInput
|
||||
value={value}
|
||||
error={convertRhfErrorMessage(error?.message)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
|
||||
return <CodeEditor isReadonly language={codeBlockType} value={content} />;
|
||||
};
|
||||
|
||||
export default CodeComponentRenderer;
|
|
@ -1,43 +0,0 @@
|
|||
import i18next from 'i18next';
|
||||
import { useMemo } from 'react';
|
||||
// eslint-disable-next-line node/file-extension-in-import
|
||||
import useSWRImmutable from 'swr/immutable';
|
||||
|
||||
import { StepMetadata } from '@/types/get-started';
|
||||
import { parseMarkdownWithYamlFrontmatter } from '@/utilities/markdown';
|
||||
|
||||
type DocumentFileNames = {
|
||||
files: string[];
|
||||
};
|
||||
|
||||
export type GetStartedType = 'application' | 'connector';
|
||||
|
||||
/**
|
||||
* Fetch the markdown files for the given type and subtype.
|
||||
* @param type 'application' or 'connector'
|
||||
* @param subtype Application library name or connector name
|
||||
* @returns List of step metadata including Yaml frontmatter and markdown content
|
||||
*/
|
||||
export const useGetStartedSteps = (type: GetStartedType, subtype?: string) => {
|
||||
const subPath = subtype ? `/${subtype}` : '';
|
||||
const publicPath = useMemo(
|
||||
() => `/console/get-started/${type}${subPath}/${i18next.language}`.toLowerCase(),
|
||||
[type, subPath]
|
||||
);
|
||||
|
||||
const { data: jsonData } = useSWRImmutable<DocumentFileNames>(`${publicPath}/index.json`);
|
||||
const { data: steps } = useSWRImmutable<StepMetadata[]>(
|
||||
jsonData,
|
||||
async ({ files }: DocumentFileNames) =>
|
||||
Promise.all(
|
||||
files.map(async (fileName) => {
|
||||
const response = await fetch(`${publicPath}/${fileName}`);
|
||||
const markdownFile = await response.text();
|
||||
|
||||
return parseMarkdownWithYamlFrontmatter<StepMetadata>(markdownFile);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return steps;
|
||||
};
|
|
@ -0,0 +1,64 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DangerousRaw from '@/components/DangerousRaw';
|
||||
import FormField from '@/components/FormField';
|
||||
import MultiTextInput from '@/components/MultiTextInput';
|
||||
import { createValidatorForRhf, convertRhfErrorMessage } from '@/components/MultiTextInput/utils';
|
||||
import { GuideForm } from '@/types/guide';
|
||||
import { noSpaceRegex } from '@/utilities/regex';
|
||||
|
||||
type Props = {
|
||||
name: 'redirectUris' | 'postLogoutRedirectUris';
|
||||
title: string;
|
||||
onError?: () => void;
|
||||
};
|
||||
|
||||
const MultiTextInputField = ({ name, title, onError }: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext<GuideForm>();
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const firstErrorKey = Object.keys(errors)[0];
|
||||
const isFirstErrorField = firstErrorKey && firstErrorKey === name;
|
||||
|
||||
if (isFirstErrorField) {
|
||||
ref.current?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
onError?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<FormField isRequired title={<DangerousRaw>{title}</DangerousRaw>}>
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
rules={{
|
||||
validate: createValidatorForRhf({
|
||||
required: t('errors.required_field_missing_plural', { field: title }),
|
||||
pattern: {
|
||||
regex: noSpaceRegex,
|
||||
message: t('errors.no_space_in_uri'),
|
||||
},
|
||||
}),
|
||||
}}
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<div ref={ref}>
|
||||
<MultiTextInput
|
||||
value={value}
|
||||
error={convertRhfErrorMessage(error?.message)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiTextInputField;
|
|
@ -1,16 +1,7 @@
|
|||
import { I18nKey } from '@logto/phrases';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { CodeProps } from 'react-markdown/lib/ast-to-react.js';
|
||||
import React, { PropsWithChildren, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
|
@ -21,16 +12,15 @@ import Spacer from '@/components/Spacer';
|
|||
import { ArrowDown, ArrowUp } from '@/icons/Arrow';
|
||||
import Tick from '@/icons/Tick';
|
||||
|
||||
import CodeComponentRenderer from '../CodeComponentRenderer';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
index: number;
|
||||
isActive: boolean;
|
||||
isComplete: boolean;
|
||||
isFinalStep: boolean;
|
||||
activeIndex: number;
|
||||
invalidIndex?: number;
|
||||
buttonText?: I18nKey;
|
||||
buttonHtmlType: 'submit' | 'button';
|
||||
onNext?: () => void;
|
||||
}>;
|
||||
|
@ -40,46 +30,30 @@ const Step = ({
|
|||
title,
|
||||
subtitle,
|
||||
index,
|
||||
isActive,
|
||||
isComplete,
|
||||
isFinalStep,
|
||||
buttonHtmlType,
|
||||
activeIndex,
|
||||
invalidIndex,
|
||||
buttonText = 'general.next',
|
||||
buttonHtmlType = 'button',
|
||||
onNext,
|
||||
}: Props) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const isActive = index === activeIndex;
|
||||
const isComplete = index < activeIndex;
|
||||
const isInvalid = index === invalidIndex;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToStep = useCallback(() => {
|
||||
ref.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||
}, []);
|
||||
|
||||
const onError = useCallback(() => {
|
||||
setIsExpanded(true);
|
||||
scrollToStep();
|
||||
}, [scrollToStep]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
if (isActive || isInvalid) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [isActive]);
|
||||
}, [isActive, isInvalid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpanded) {
|
||||
scrollToStep();
|
||||
ref.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||
}
|
||||
}, [isExpanded, scrollToStep]);
|
||||
}, [isExpanded]);
|
||||
|
||||
const memoizedComponents = useMemo(
|
||||
() => ({
|
||||
code: ({ ...props }: PropsWithChildren<CodeProps>) => (
|
||||
<CodeComponentRenderer {...props} onError={onError} />
|
||||
),
|
||||
}),
|
||||
[onError]
|
||||
);
|
||||
|
||||
// TODO: add more styles to markdown renderer
|
||||
return (
|
||||
<Card key={title} ref={ref} className={styles.card}>
|
||||
<div
|
||||
|
@ -106,13 +80,13 @@ const Step = ({
|
|||
<IconButton>{isExpanded ? <ArrowUp /> : <ArrowDown />}</IconButton>
|
||||
</div>
|
||||
<div className={classNames(styles.content, isExpanded && styles.expanded)}>
|
||||
{isValidElement(children) && cloneElement(children, { components: memoizedComponents })}
|
||||
{children}
|
||||
<div className={styles.buttonWrapper}>
|
||||
<Button
|
||||
htmlType={buttonHtmlType}
|
||||
type="primary"
|
||||
title={`general.${isFinalStep ? 'done' : 'next'}`}
|
||||
onClick={conditional(!isFinalStep && onNext)}
|
||||
title={buttonText}
|
||||
onClick={conditional(onNext)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
|
@ -39,6 +39,10 @@
|
|||
.banner {
|
||||
margin-bottom: _.unit(6);
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +1,52 @@
|
|||
import { AdminConsoleKey } from '@logto/phrases';
|
||||
import React, { cloneElement, isValidElement, PropsWithChildren, ReactNode, useState } from 'react';
|
||||
import { MDXProvider } from '@mdx-js/react';
|
||||
import i18next from 'i18next';
|
||||
import { MDXProps } from 'mdx/types';
|
||||
import React, {
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
lazy,
|
||||
LazyExoticComponent,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
Suspense,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import CardTitle from '@/components/CardTitle';
|
||||
import CodeEditor from '@/components/CodeEditor';
|
||||
import DangerousRaw from '@/components/DangerousRaw';
|
||||
import IconButton from '@/components/IconButton';
|
||||
import Spacer from '@/components/Spacer';
|
||||
import Close from '@/icons/Close';
|
||||
import { GetStartedForm } from '@/types/get-started';
|
||||
import { GuideForm } from '@/types/guide';
|
||||
|
||||
import MultiTextInputField from './components/MultiTextInputField';
|
||||
import Step from './components/Step';
|
||||
import { GetStartedType, useGetStartedSteps } from './hooks';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const Guides: Record<string, LazyExoticComponent<(props: MDXProps) => JSX.Element>> = {
|
||||
react: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/react/index.mdx')),
|
||||
'react_zh-cn': lazy(
|
||||
async () =>
|
||||
import(
|
||||
'@/assets/i18n/zh-cn/docusaurus-plugin-content-docs/current/tutorial/integrate-sdk/react/index.mdx'
|
||||
)
|
||||
),
|
||||
};
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
title: string;
|
||||
subtitle?: AdminConsoleKey;
|
||||
type: GetStartedType;
|
||||
/** `subtype` can be an actual type of an application or connector.
|
||||
* e.g. React, Angular, Vue, etc. for application. Or Github, WeChat, etc. for connector.
|
||||
*/
|
||||
defaultSubtype?: string;
|
||||
bannerComponent?: ReactNode;
|
||||
onClose?: () => void;
|
||||
onComplete?: (data: GetStartedForm) => Promise<void>;
|
||||
onComplete?: (data: GuideForm) => Promise<void>;
|
||||
}>;
|
||||
|
||||
const onClickFetchSampleProject = (projectName: string) => {
|
||||
|
@ -33,19 +54,23 @@ const onClickFetchSampleProject = (projectName: string) => {
|
|||
window.open(sampleUrl, '_blank');
|
||||
};
|
||||
|
||||
const GetStarted = ({
|
||||
const Guide = ({
|
||||
title,
|
||||
subtitle,
|
||||
type,
|
||||
defaultSubtype,
|
||||
defaultSubtype = '',
|
||||
bannerComponent,
|
||||
onClose,
|
||||
onComplete,
|
||||
}: Props) => {
|
||||
const [subtype, setSubtype] = useState(defaultSubtype);
|
||||
const [activeStepIndex, setActiveStepIndex] = useState<number>(-1);
|
||||
const steps = useGetStartedSteps(type, subtype) ?? [];
|
||||
const methods = useForm<GetStartedForm>({ reValidateMode: 'onBlur' });
|
||||
const [activeStepIndex, setActiveStepIndex] = useState(-1);
|
||||
const [invalidStepIndex, setInvalidStepIndex] = useState(-1);
|
||||
|
||||
const locale = i18next.language;
|
||||
const guideKey = `${subtype}_${locale}`.toLowerCase();
|
||||
const GuideComponent = Guides[guideKey] ?? Guides[subtype];
|
||||
|
||||
const methods = useForm<GuideForm>({ mode: 'onSubmit', reValidateMode: 'onChange' });
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
|
@ -89,32 +114,32 @@ const GetStarted = ({
|
|||
setActiveStepIndex(0);
|
||||
},
|
||||
})}
|
||||
{steps.map(({ title, subtitle, metadata }, index) => {
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
const isFinalStep = index === steps.length - 1;
|
||||
<MDXProvider
|
||||
components={{
|
||||
code: ({ className, children }) => {
|
||||
const [, language] = /language-(\w+)/.exec(className ?? '') ?? [];
|
||||
|
||||
return (
|
||||
<Step
|
||||
key={title}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
index={index}
|
||||
isActive={activeStepIndex === index}
|
||||
isComplete={activeStepIndex > index}
|
||||
isFinalStep={isFinalStep}
|
||||
buttonHtmlType={isFinalStep ? 'submit' : 'button'}
|
||||
onNext={() => {
|
||||
setActiveStepIndex(index + 1);
|
||||
}}
|
||||
>
|
||||
{metadata && (
|
||||
<ReactMarkdown className={styles.markdownContent}>{metadata}</ReactMarkdown>
|
||||
)}
|
||||
</Step>
|
||||
);
|
||||
})}
|
||||
return <CodeEditor isReadonly language={language} value={String(children)} />;
|
||||
},
|
||||
MultiTextInputField,
|
||||
Step,
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
{GuideComponent && (
|
||||
<GuideComponent
|
||||
activeStepIndex={activeStepIndex}
|
||||
invalidStepIndex={invalidStepIndex}
|
||||
onNext={(nextIndex: number) => {
|
||||
setActiveStepIndex(nextIndex);
|
||||
}}
|
||||
onError={(invalidIndex: number) => {
|
||||
setInvalidStepIndex(invalidIndex);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</MDXProvider>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
|
@ -122,4 +147,4 @@ const GetStarted = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default GetStarted;
|
||||
export default Guide;
|
|
@ -7,7 +7,7 @@ export const applicationTypeI18nKey = Object.freeze({
|
|||
} as const);
|
||||
|
||||
export enum SupportedJavascriptLibraries {
|
||||
Angular = 'Angular',
|
||||
React = 'React',
|
||||
Vue = 'Vue',
|
||||
Angular = 'angular',
|
||||
React = 'react',
|
||||
Vue = 'vue',
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
export type StepMetadata = {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
metadata: string; // Markdown formatted string
|
||||
};
|
||||
|
||||
export type GetStartedForm = {
|
||||
redirectUris: string[];
|
||||
postLogoutRedirectUris: string[];
|
||||
connectorConfigJson: string;
|
||||
};
|
5
packages/console/src/types/guide.ts
Normal file
5
packages/console/src/types/guide.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type GuideForm = {
|
||||
redirectUris: string[];
|
||||
postLogoutRedirectUris: string[];
|
||||
connectorConfigJson: string;
|
||||
};
|
|
@ -1,45 +0,0 @@
|
|||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
## Install SDK
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="npm" label="npm">
|
||||
|
||||
```bash
|
||||
npm i @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
```bash
|
||||
yarn add @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
```bash
|
||||
pnpm add @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="script" label="script">
|
||||
|
||||
```html
|
||||
<script src="https://logto.io/js/logto-sdk-react/0.1.0/logto-sdk-react.production.js" />
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="git" label="Git">
|
||||
|
||||
```bash
|
||||
git clone https://github.com/logto-io/js.git
|
||||
pnpm build
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
|
@ -1,33 +0,0 @@
|
|||
## Initiate LogtoClient
|
||||
|
||||
Add the following code to your main html file. You may need client ID and authorization domain.
|
||||
|
||||
```tsx
|
||||
import { LogtoProvider, LogtoConfig } from '@logto/react';
|
||||
import React from 'react';
|
||||
|
||||
//...
|
||||
|
||||
const App = () => {
|
||||
const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' }
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<LogtoProvider config={config}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/callback" element={<Callback />} />
|
||||
<Route
|
||||
path="/protected-resource"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<ProtectedResource />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</LogtoProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
```
|
|
@ -1,45 +0,0 @@
|
|||
## Sign In
|
||||
|
||||
### Setup your login
|
||||
|
||||
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, let’s enter your redirect URI
|
||||
|
||||
```redirectUris
|
||||
Redirect URI
|
||||
```
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
```typescript
|
||||
import React from "react";
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const SignInButton = () => {
|
||||
const { signIn } = useLogto();
|
||||
const redirectUrl = window.location.origin + '/callback';
|
||||
|
||||
return <button onClick={() => signIn(redirectUrl)}>Sign In</button>;
|
||||
};
|
||||
|
||||
export default SignInButton;
|
||||
```
|
||||
|
||||
### Retrieve Auth Status
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const App = () => {
|
||||
const { isAuthenticated, signIn } = useLogto();
|
||||
|
||||
if !(isAuthenticated) {
|
||||
return <SignInButton />
|
||||
}
|
||||
|
||||
return <>
|
||||
<AppContent />
|
||||
<SignOutButton />
|
||||
</>
|
||||
};
|
||||
```
|
|
@ -1,24 +0,0 @@
|
|||
## Sign Out
|
||||
|
||||
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
|
||||
|
||||
```postLogoutRedirectUris
|
||||
Post sign out redirect URI
|
||||
```
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const SignOutButton = () => {
|
||||
const { signOut } = useLogto();
|
||||
|
||||
return (
|
||||
<button onClick={() => signOut(window.location.origin)}>Sign out</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignOutButton;
|
||||
```
|
|
@ -1,5 +0,0 @@
|
|||
## Further Readings
|
||||
|
||||
- [SDK Documentation](https://link-url-here.org)
|
||||
- [OIDC Documentation](https://link-url-here.org)
|
||||
- [Calling API to fetch accessToken](https://link-url-here.org)
|
|
@ -1,13 +1,190 @@
|
|||
import Step1 from './_step-1.mdx'
|
||||
import Step2 from './_step-2.md'
|
||||
import Step3 from './_step-3.md'
|
||||
import Step4 from './_step-4.md'
|
||||
import Step5 from './_step-5.md'
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# Integrate `@logto/react`
|
||||
|
||||
<Step1 />
|
||||
<Step2 />
|
||||
<Step3 />
|
||||
<Step4 />
|
||||
<Step5 />
|
||||
<Step
|
||||
title="Install SDK"
|
||||
subtitle="Please select your favorite package manager"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(1)}
|
||||
>
|
||||
<Tabs>
|
||||
<TabItem value="npm" label="npm">
|
||||
|
||||
```bash
|
||||
npm i @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
```bash
|
||||
yarn add @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
```bash
|
||||
pnpm add @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="script" label="script">
|
||||
|
||||
```html
|
||||
<script src="https://logto.io/js/logto-sdk-react/0.1.0/logto-sdk-react.production.js" />
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="git" label="Git">
|
||||
|
||||
```bash
|
||||
git clone https://github.com/logto-io/js.git
|
||||
pnpm build
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step
|
||||
title="Initiate LogtoClient"
|
||||
subtitle="1 step"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
Add the following code to your main html file. You may need client ID and authorization domain.
|
||||
|
||||
```typescript
|
||||
import { LogtoProvider, LogtoConfig } from '@logto/react';
|
||||
import React from 'react';
|
||||
|
||||
//...
|
||||
|
||||
const App = () => {
|
||||
const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' };
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<LogtoProvider config={config}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/callback" element={<Callback />} />
|
||||
<Route
|
||||
path="/protected-resource"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<ProtectedResource />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</LogtoProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step
|
||||
title="Sign In"
|
||||
subtitle="2 steps"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
### Setup your login
|
||||
|
||||
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, let’s enter your redirect URI
|
||||
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" onError={() => props.onError(2)} />
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const SignInButton = () => {
|
||||
const { signIn } = useLogto();
|
||||
const redirectUrl = window.location.origin + '/callback';
|
||||
|
||||
return <button onClick={() => signIn(redirectUrl)}>Sign In</button>;
|
||||
};
|
||||
|
||||
export default SignInButton;
|
||||
```
|
||||
|
||||
### Retrieve Auth Status
|
||||
|
||||
```typescript
|
||||
import React from "react";
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const App = () => {
|
||||
const { isAuthenticated, signIn } = useLogto();
|
||||
|
||||
if !(isAuthenticated) {
|
||||
return <SignInButton />
|
||||
}
|
||||
|
||||
return <>
|
||||
<AppContent />
|
||||
<SignOutButton />
|
||||
</>
|
||||
};
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step
|
||||
title="Sign Out"
|
||||
subtitle="1 step"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
|
||||
|
||||
<MultiTextInputField name="postLogoutRedirectUris" title="Post sign out redirect URI" onError={() => props.onError(3)} />
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const SignOutButton = () => {
|
||||
const { signOut } = useLogto();
|
||||
|
||||
return <button onClick={() => signOut(window.location.origin)}>Sign out</button>;
|
||||
};
|
||||
|
||||
export default SignOutButton;
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step
|
||||
title="Further Readings"
|
||||
subtitle="3 steps"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonHtmlType="submit"
|
||||
>
|
||||
|
||||
- [SDK Documentation](https://link-url-here.org)
|
||||
- [OIDC Documentation](https://link-url-here.org)
|
||||
- [Calling API to fetch accessToken](https://link-url-here.org)
|
||||
|
||||
</Step>
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
## 安装 Logto SDK
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="npm" label="npm">
|
||||
|
||||
```bash
|
||||
npm i @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
```bash
|
||||
yarn add @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
```bash
|
||||
pnpm add @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="script" label="script">
|
||||
|
||||
```html
|
||||
<script src="https://logto.io/js/logto-sdk-react/0.1.0/logto-sdk-react.production.js" />
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="git" label="Git">
|
||||
|
||||
```bash
|
||||
git clone https://github.com/logto-io/js.git
|
||||
pnpm build
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
|
@ -1,33 +0,0 @@
|
|||
## 初始化 LogtoClient
|
||||
|
||||
Add the following code to your main html file. You may need client ID and authorization domain.
|
||||
|
||||
```tsx
|
||||
import { LogtoProvider, LogtoConfig } from '@logto/react';
|
||||
import React from 'react';
|
||||
|
||||
...
|
||||
|
||||
const App = () => {
|
||||
const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' }
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<LogtoProvider config={config}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/callback" element={<Callback />} />
|
||||
<Route
|
||||
path="/protected-resource"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<ProtectedResource />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</LogtoProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
```
|
|
@ -1,45 +0,0 @@
|
|||
## 登录
|
||||
|
||||
### Setup your login
|
||||
|
||||
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, let’s enter your redirect URI
|
||||
|
||||
```redirectUris
|
||||
Redirect URI
|
||||
```
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const SignInButton = () => {
|
||||
const { signIn } = useLogto();
|
||||
const redirectUrl = window.location.origin + '/callback';
|
||||
|
||||
return <button onClick={() => signIn(redirectUrl)}>Sign In</button>;
|
||||
};
|
||||
|
||||
export default SignInButton;
|
||||
```
|
||||
|
||||
### Retrieve Auth Status
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const App = () => {
|
||||
const { isAuthenticated, signIn } = useLogto();
|
||||
|
||||
if !(isAuthenticated) {
|
||||
return <SignInButton />
|
||||
}
|
||||
|
||||
return <>
|
||||
<AppContent />
|
||||
<SignOutButton />
|
||||
</>
|
||||
};
|
||||
```
|
|
@ -1,24 +0,0 @@
|
|||
## 登出
|
||||
|
||||
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
|
||||
|
||||
```postLogoutRedirectUris
|
||||
Post sign out redirect URI
|
||||
```
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const SignOutButton = () => {
|
||||
const { signOut } = useLogto();
|
||||
|
||||
return (
|
||||
<button onClick={() => signOut(window.location.origin)}>Sign out</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignOutButton;
|
||||
```
|
|
@ -1,5 +0,0 @@
|
|||
## Further Readings
|
||||
|
||||
- [SDK Documentation](https://link-url-here.org)
|
||||
- [OIDC Documentation](https://link-url-here.org)
|
||||
- [Calling API to fetch accessToken](https://link-url-here.org)
|
|
@ -1,13 +1,190 @@
|
|||
import Step1 from './_step-1.mdx'
|
||||
import Step2 from './_step-2.md'
|
||||
import Step3 from './_step-3.md'
|
||||
import Step4 from './_step-4.md'
|
||||
import Step5 from './_step-5.md'
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# 集成 `@logto/react`
|
||||
|
||||
<Step1 />
|
||||
<Step2 />
|
||||
<Step3 />
|
||||
<Step4 />
|
||||
<Step5 />
|
||||
<Step
|
||||
title="安装 SDK"
|
||||
subtitle="选择您熟悉的安装方式"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(1)}
|
||||
>
|
||||
<Tabs>
|
||||
<TabItem value="npm" label="npm">
|
||||
|
||||
```bash
|
||||
npm i @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
```bash
|
||||
yarn add @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
```bash
|
||||
pnpm add @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="script" label="script">
|
||||
|
||||
```html
|
||||
<script src="https://logto.io/js/logto-sdk-react/0.1.0/logto-sdk-react.production.js" />
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="git" label="Git">
|
||||
|
||||
```bash
|
||||
git clone https://github.com/logto-io/js.git
|
||||
pnpm build
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step
|
||||
title="Initiate LogtoClient"
|
||||
subtitle="1 step"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
在项目的 html 文件中,加入如下代码(需提前准备好 client ID 以及 authorization domain)
|
||||
Add the following code to your main html file. You may need client ID and authorization domain.
|
||||
|
||||
```typescript
|
||||
import { LogtoProvider, LogtoConfig } from '@logto/react';
|
||||
import React from 'react';
|
||||
|
||||
//...
|
||||
|
||||
const App = () => {
|
||||
const config: LogtoConfig = { clientId: 'foo', endpoint: 'https://your-endpoint-domain.com' };
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<LogtoProvider config={config}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/callback" element={<Callback />} />
|
||||
<Route
|
||||
path="/protected-resource"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<ProtectedResource />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</LogtoProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step
|
||||
title="Sign In"
|
||||
subtitle="2 steps"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
### Setup your login
|
||||
|
||||
The Logto React SDK provides you tools and hooks to quickly implement your own authorization flow. First, let’s enter your redirect URI
|
||||
|
||||
<MultiTextInputField name="redirectUris" title="Redirect URI" onError={() => props.onError(2)} />
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const SignInButton = () => {
|
||||
const { signIn } = useLogto();
|
||||
const redirectUrl = window.location.origin + '/callback';
|
||||
|
||||
return <button onClick={() => signIn(redirectUrl)}>Sign In</button>;
|
||||
};
|
||||
|
||||
export default SignInButton;
|
||||
```
|
||||
|
||||
### Retrieve Auth Status
|
||||
|
||||
```typescript
|
||||
import React from "react";
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const App = () => {
|
||||
const { isAuthenticated, signIn } = useLogto();
|
||||
|
||||
if !(isAuthenticated) {
|
||||
return <SignInButton />
|
||||
}
|
||||
|
||||
return <>
|
||||
<AppContent />
|
||||
<SignOutButton />
|
||||
</>
|
||||
};
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step
|
||||
title="Sign Out"
|
||||
subtitle="1 step"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
onNext={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
Execute signOut() methods will redirect users to the Logto sign out page. After a success sign out, all use session data and auth status will be cleared.
|
||||
|
||||
<MultiTextInputField name="postLogoutRedirectUris" title="Post sign out redirect URI" onError={() => props.onError(3)} />
|
||||
|
||||
Add the following code to your web app
|
||||
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { useLogto } from '@logto/react';
|
||||
|
||||
const SignOutButton = () => {
|
||||
const { signOut } = useLogto();
|
||||
|
||||
return <button onClick={() => signOut(window.location.origin)}>Sign out</button>;
|
||||
};
|
||||
|
||||
export default SignOutButton;
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step
|
||||
title="Further Readings"
|
||||
subtitle="3 steps"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
invalidIndex={props.invalidStepIndex}
|
||||
buttonHtmlType="submit"
|
||||
>
|
||||
|
||||
- [SDK Documentation](https://link-url-here.org)
|
||||
- [OIDC Documentation](https://link-url-here.org)
|
||||
- [Calling API to fetch accessToken](https://link-url-here.org)
|
||||
|
||||
</Step>
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
"@logto/jest-config": "^0.1.0",
|
||||
"@logto/phrases": "^0.1.0",
|
||||
"@logto/schemas": "^0.1.0",
|
||||
"@parcel/core": "^2.3.2",
|
||||
"@parcel/transformer-sass": "^2.3.2",
|
||||
"@parcel/core": "^2.5.0",
|
||||
"@parcel/transformer-sass": "^2.5.0",
|
||||
"@peculiar/webcrypto": "^1.3.3",
|
||||
"@silverhand/eslint-config": "^0.10.2",
|
||||
"@silverhand/eslint-config-react": "^0.10.3",
|
||||
|
@ -42,7 +42,7 @@
|
|||
"ky": "^0.30.0",
|
||||
"libphonenumber-js": "^1.9.49",
|
||||
"lint-staged": "^12.0.0",
|
||||
"parcel": "^2.3.2",
|
||||
"parcel": "^2.5.0",
|
||||
"postcss": "^8.4.6",
|
||||
"postcss-modules": "^4.3.0",
|
||||
"prettier": "^2.3.2",
|
||||
|
|
|
@ -23,8 +23,7 @@
|
|||
inset: 0;
|
||||
}
|
||||
|
||||
|
||||
// React modal animation
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
/* stylelint-disable-next-line selector-pseudo-class-no-unknown */
|
||||
:global {
|
||||
.ReactModal__Content[role='popup'] {
|
||||
|
@ -32,7 +31,6 @@
|
|||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
.ReactModal__Content--after-open[role='popup'] {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
@ -40,5 +38,6 @@
|
|||
.ReactModal__Content--before-close[role='popup'] {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
/* stylelint-enable selector-class-pattern */
|
||||
}
|
||||
|
||||
/* stylelint-enable selector-class-pattern */
|
||||
|
|
1196
pnpm-lock.yaml
1196
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue