0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console): guide modal v2

This commit is contained in:
Gao Sun 2023-08-14 15:05:41 +08:00
parent 0fcea5ae5e
commit 466933a490
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
21 changed files with 835 additions and 2 deletions

View file

@ -156,6 +156,8 @@
{
"files": [
"*.d.ts",
"**/assets/docs/guides/types.ts",
"**/assets/docs/guides/**/index.ts",
"**/mdx-components/*/index.tsx"
],
"rules": {

View file

@ -0,0 +1,49 @@
# Guides
> This directory is a part of the guide v2 project. The sibling directory `tutorial` will be removed once the guide v2 project is complete.
This directory serves as the home for all guides related to the console. Every guide should have a directory with the following structure:
```
[target]-name
├── assets
│ └── image-name.png
├── index.ts
├── logo.svg
└── README.mdx
```
The `README.mdx` file contains the actual guide content. The `assets` directory contains all images used in the guide. The `index.ts` file exports the guide's metadata, which is used to display, sort and filter the guides in the console.
## Write a guide
### Create the guide directory
The guide directory should be named `[target]-name`, where `[target]` is the target of the guide in kebab-case (see `types.ts` for a list of all targets) and `name` is the name of the guide. The name should be kebab-cased and should not contain any special characters.
For example, a guide for the `MachineToMachine` target with the name `General` should be placed in the directory `machine-to-machine-general`; a guide for the `SPA` target with the name `React` should be placed in the directory `spa-react`.
> **Note**
> The directory name will be the unique identifier of the guide.
### Create the guide metadata
The guide metadata should be the default export of the `index.ts` file. It should be an object with the `GuideMetadata` type in `types.ts` as its type.
### Write the guide content
The guide content is written in [MDX](https://mdxjs.com/), which is a combination of Markdown and JSX. This allows us to use React components in the guide content.
### Add the logo
The logo should be placed in the guide directory and named `logo.svg`. It will be displayed in the guide list and other places where the guide is referenced.
### Add images and other assets
Images and other assets (if any) should be placed in the `assets` directory of the guide. They can then be referenced in the guide content.
### Update metadata
Since Parcel doesn't support dynamic import (see [#112](https://github.com/parcel-bundler/parcel/issues/112) [#125](https://github.com/parcel-bundler/parcel/issues/125)), we need to run `node generate-metadata.js` to update the metadata in `index.ts`, thus we can use it in the guide components with React lazy loading.
This may be fixed by replacing Parcel with something else.

View file

@ -0,0 +1,59 @@
import { existsSync } from 'node:fs';
import fs from 'node:fs/promises';
const entries = await fs.readdir('.');
const directories = entries.filter((entry) => !entry.includes('.'));
const metadata = directories
.map((directory) => {
if (!existsSync(`${directory}/README.mdx`)) {
console.warn(`No README.mdx file found in ${directory} directory, skipping.`);
return;
}
if (!existsSync(`${directory}/index.ts`)) {
console.warn(`No index.ts file found in ${directory} directory, skipping.`);
return;
}
// Add `.png` later
const logo = ['logo.svg'].find((logo) => existsSync(`${directory}/${logo}`));
return {
name: directory,
logo,
};
})
.filter(Boolean);
const camelCase = (value) => value.replaceAll(/-./g, (x) => x[1].toUpperCase());
const filename = 'index.ts';
await fs.writeFile(
filename,
"// This is a generated file, don't update manually.\n\nimport { lazy } from 'react';\n\nimport { type Guide } from './types';\n"
);
for (const { name } of metadata) {
// eslint-disable-next-line no-await-in-loop
await fs.appendFile(filename, `import ${camelCase(name)} from './${name}/index';\n`);
}
await fs.appendFile(filename, '\n');
await fs.appendFile(filename, 'const guides: Readonly<Guide[]> = Object.freeze([');
for (const { name, logo } of metadata) {
fs.appendFile(
filename,
`
{
id: '${name}',
logo: ${logo ? `lazy(async () => import('./${name}/${logo}'))` : 'undefined'},
Component: lazy(async () => import('./${name}/README.mdx')),
metadata: ${camelCase(name)},
},
`
);
}
await fs.appendFile(filename, ']);\n\nexport default guides;\n');

View file

@ -0,0 +1,17 @@
// This is a generated file, don't update manually.
import { lazy } from 'react';
import spaReact from './spa-react/index';
import { type Guide } from './types';
const guides: Readonly<Guide[]> = Object.freeze([
{
id: 'spa-react',
logo: lazy(async () => import('./spa-react/logo.svg')),
Component: lazy(async () => import('./spa-react/README.mdx')),
metadata: spaReact,
},
]);
export default guides;

View file

@ -0,0 +1,189 @@
import UriInputField from '@mdx/components/UriInputField';
import Tabs from '@mdx/components/Tabs';
import TabItem from '@mdx/components/TabItem';
import InlineNotification from '@/ds-components/InlineNotification';
import Steps from '@/mdx-components-v2/Steps';
import Step from '@/mdx-components-v2/Step';
<Steps>
<Step
title="Add Logto SDK as a dependency"
subtitle="Please select your favorite package manager"
>
<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>
</Tabs>
</Step>
<Step
title="Init LogtoClient"
subtitle="1 step"
>
Import and use `LogtoProvider` to provide a Logto context:
<pre>
<code className="language-tsx">
{`import { LogtoProvider, LogtoConfig } from '@logto/react';
const config: LogtoConfig = {
endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
appId: '${props.appId}',
};
const App = () => (
<LogtoProvider config={config}>
<YourAppContent />
</LogtoProvider>
);`}
</code>
</pre>
</Step>
<Step
title="Sign in"
subtitle="3 steps"
>
<InlineNotification>
In the following steps, we assume your app is running on <code>http://localhost:3000</code>.
</InlineNotification>
### Configure Redirect URI
First, lets enter your redirect URI. E.g. `http://localhost:3000/callback`.
<UriInputField
appId={props.appId}
isSingle={!props.isCompact}
name="redirectUris"
title="application_details.redirect_uri"
/>
### Implement a sign-in button
We provide two hooks `useHandleSignInCallback()` and `useLogto()` which can help you easily manage the authentication flow.
Go back to your IDE/editor, use the following code to implement the sign-in button:
<pre>
<code className="language-tsx">
{`import { useLogto } from '@logto/react';
const SignIn = () => {
const { signIn, isAuthenticated } = useLogto();
if (isAuthenticated) {
return <div>Signed in</div>;
}
return (
<button onClick={() => signIn('${props.redirectUris[0] ?? 'http://localhost:3000/callback'}')}>
Sign In
</button>
);
};`}
</code>
</pre>
### Handle redirect
We're almost there! In the last step, we use `http://localhost:3000/callback` as the Redirect URI, and now we need to handle it properly.
First let's create a callback component:
```tsx
import { useHandleSignInCallback } from '@logto/react';
const Callback = () => {
const { isLoading } = useHandleSignInCallback(() => {
// Navigate to root path when finished
});
// When it's working in progress
if (isLoading) {
return <div>Redirecting...</div>;
}
};
```
Finally insert the code below to create a `/callback` route which does NOT require authentication:
```tsx
// Assuming react-router
<Route path="/callback" element={<Callback />} />
```
</Step>
<Step
title="Sign out"
subtitle="1 step"
>
Calling `.signOut()` will clear all the Logto data in memory and localStorage if they exist.
After signing out, it'll be great to redirect user back to your website. Let's add `http://localhost:3000` as the Post Sign-out URI below, and use it as the parameter when calling `.signOut()`.
<UriInputField
appId={props.appId}
isSingle={!props.isCompact}
name="postLogoutRedirectUris"
title="application_details.post_sign_out_redirect_uri"
/>
### Implement a sign-out button
<pre>
<code className="language-tsx">
{`const SignOut = () => {
const { signOut } = useLogto();
return (
<button onClick={() => signOut('${
props.postLogoutRedirectUris[0] ?? 'http://localhost:3000'
}')}>
Sign out
</button>
);
};`}
</code>
</pre>
</Step>
<Step
title="Further readings"
subtitle="4 articles"
>
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
</Step>
</Steps>

View file

@ -0,0 +1,12 @@
import { ApplicationType } from '@logto/schemas';
import { type GuideMetadata } from '../types';
const metadata: Readonly<GuideMetadata> = Object.freeze({
name: 'React',
description: 'React is a JavaScript library for building user interfaces.',
target: ApplicationType.SPA,
language: 'javascript',
});
export default metadata;

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-11.5 -10.23174 23 20.46348">
<title>React Logo</title>
<circle cx="0" cy="0" r="2.05" fill="#61dafb"/>
<g stroke="#61dafb" stroke-width="1" fill="none">
<ellipse rx="11" ry="4.2"/>
<ellipse rx="11" ry="4.2" transform="rotate(60)"/>
<ellipse rx="11" ry="4.2" transform="rotate(120)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 366 B

View file

@ -0,0 +1,42 @@
import { type ApplicationType } from '@logto/schemas';
import { type MDXProps } from 'mdx/types';
import { type LazyExoticComponent, type FunctionComponent } from 'react';
type ProgrammingLanguage =
| 'javascript'
| 'typescript'
| 'go'
| 'java'
| 'swift'
| 'kotlin'
| 'php'
| 'python'
| 'agnostic';
/**
* The guide metadata type. The directory name that the metadata is in will be the
* unique identifier of the guide.
*/
export type GuideMetadata = {
/** The name of the target (framework, language, or platform) that the guide is for. */
name: string;
/** The short description of the guide, should be less than 100 characters. */
description?: string;
/**
* The target resource of the guide.
*
* For example, if the guide is for application creation, the target should be `ApplicationType`,
* and an application of the target type should be created.
*/
target: ApplicationType | 'API';
/** The programming language of the guide. If it doesn't apply, set it to `agnostic`. */
language: ProgrammingLanguage;
};
/** The guide instance to build in the console. */
export type Guide = {
id: string;
logo?: LazyExoticComponent<FunctionComponent>;
Component: LazyExoticComponent<FunctionComponent<MDXProps>>;
metadata: Readonly<GuideMetadata>;
};

View file

@ -1,4 +1,5 @@
import { yes } from '@silverhand/essentials';
export const isProduction = process.env.NODE_ENV === 'production';
export const isCloud = yes(process.env.IS_CLOUD);
export const adminEndpoint = process.env.ADMIN_ENDPOINT;

View file

@ -1,7 +1,7 @@
import type { Nullable } from '@silverhand/essentials';
import { useState, useEffect, useCallback } from 'react';
const useScroll = (contentRef: Nullable<HTMLDivElement>) => {
const useScroll = (contentRef: Nullable<HTMLElement>) => {
const [scrollTop, setScrollTop] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);

View file

@ -0,0 +1,5 @@
# MDX components v2
This directory contains the source code for the new app creation guide MDX components. We'll progressively adopt the new design and delete `mdx-components` once we're done.
The core component is `<Steps />` which renders the guide content.

View file

@ -0,0 +1,15 @@
@use '@/scss/underscore' as _;
.wrapper {
border-radius: 16px;
background: var(--color-layer-1);
padding: _.unit(5) _.unit(6);
width: 100%;
header {
display: flex;
align-items: center;
gap: _.unit(4);
margin-bottom: _.unit(6);
}
}

View file

@ -0,0 +1,32 @@
import { type ReactNode, forwardRef, type Ref } from 'react';
import Index from '@/components/Index';
import CardTitle from '@/ds-components/CardTitle';
import DangerousRaw from '@/ds-components/DangerousRaw';
import * as styles from './index.module.scss';
export type Props = {
index: number;
title: string;
subtitle?: string;
children: ReactNode;
};
function Step({ title, subtitle, index, children }: Props, ref?: Ref<HTMLDivElement>) {
return (
<section ref={ref} className={styles.wrapper}>
<header>
<Index index={index + 1} />
<CardTitle
size="medium"
title={<DangerousRaw>{title}</DangerousRaw>}
subtitle={<DangerousRaw>{subtitle}</DangerousRaw>}
/>
</header>
<div>{children}</div>
</section>
);
}
export default forwardRef<HTMLDivElement, Props>(Step);

View file

@ -0,0 +1,44 @@
@use '@/scss/underscore' as _;
.wrapper {
position: relative;
}
.navigationAnchor {
position: absolute;
inset: 0 auto 0 0;
transform: translateX(-100%);
}
.navigation {
position: sticky;
top: 0;
flex-shrink: 0;
margin-right: _.unit(4);
width: 220px;
> :not(:last-child) {
margin-bottom: _.unit(6);
}
}
.content {
max-width: 858px;
flex-grow: 1;
> :not(:last-child) {
margin-bottom: _.unit(6);
}
}
.stepper {
font: var(--font-title-2);
color: var(--color-text);
border-radius: 12px;
border: 1px solid var(--color-surface-5);
padding: _.unit(3) _.unit(4);
&.active {
background: var(--color-surface-1);
}
}

View file

@ -0,0 +1,92 @@
import { type Nullable } from '@silverhand/essentials';
import classNames from 'classnames';
import React, { useRef, type ReactElement, useEffect, useState } from 'react';
import useScroll from '@/hooks/use-scroll';
import { type Props as StepProps } from '../Step';
import type Step from '../Step';
import * as styles from './index.module.scss';
type Props = {
children: Array<ReactElement<StepProps, typeof Step>>;
};
/** Find the first scrollable element in the parent chain. */
const findScrollableElement = (element: Nullable<HTMLElement>): Nullable<HTMLElement> => {
if (!element) {
return null;
}
const { overflowY } = window.getComputedStyle(element);
if (overflowY === 'auto' || overflowY === 'scroll') {
return element;
}
return findScrollableElement(element.parentElement);
};
export default function Steps({ children }: Props) {
const contentRef = useRef<HTMLDivElement>(null);
const stepReferences = useRef<Array<Nullable<HTMLElement>>>([]);
const { scrollTop } = useScroll(findScrollableElement(contentRef.current));
const [activeIndex, setActiveIndex] = useState(-1);
useEffect(() => {
// Make sure the step references length matches the number of children.
// eslint-disable-next-line @silverhand/fp/no-mutation
stepReferences.current = stepReferences.current.slice(0, children.length);
}, [children]);
useEffect(() => {
// Get the active index by comparing the scroll position of the content
// with the top of each step in reverse order because the last satisfied
// step is the active one.
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
const reversedActiveIndex = stepReferences.current
.slice()
.reverse()
.findIndex((stepRef) => {
if (!stepRef) {
return false;
}
const elementScrollTop = stepRef.getBoundingClientRect().top;
// Don't try to understand it, feel it.
return elementScrollTop <= 280;
});
setActiveIndex(reversedActiveIndex === -1 ? -1 : children.length - reversedActiveIndex - 1);
}, [children.length, scrollTop]);
return (
<div className={styles.wrapper}>
<div className={styles.navigationAnchor}>
<nav className={styles.navigation}>
{children.map((component, index) => (
<div
key={component.props.title}
className={classNames(styles.stepper, index === activeIndex && styles.active)}
>
{index + 1}. {component.props.title}
</div>
))}
</nav>
</div>
<div ref={contentRef} className={styles.content}>
{children.map((component, index) =>
React.cloneElement(component, {
key: component.props.title,
index,
ref: (element: HTMLDivElement) => {
// eslint-disable-next-line @silverhand/fp/no-mutation
stepReferences.current[index] = element;
},
})
)}
</div>
</div>
);
}

View file

@ -1,8 +1,11 @@
import type { Application } from '@logto/schemas';
import Modal from 'react-modal';
import { isProduction } from '@/consts/env';
import * as modalStyles from '@/scss/modal.module.scss';
import GuideV2 from '../GuideV2';
import Guide from '.';
type Props = {
@ -26,7 +29,12 @@ function GuideModal({ app, onClose }: Props) {
className={modalStyles.fullScreen}
onRequestClose={closeModal}
>
<Guide app={app} onClose={closeModal} />
{/* Switch to v2 once migration is complete. */}
{isProduction ? (
<Guide app={app} onClose={closeModal} />
) : (
<GuideV2 app={app} onClose={closeModal} />
)}
</Modal>
);
}

View file

@ -0,0 +1,37 @@
@use '@/scss/underscore' as _;
.header {
display: flex;
align-items: center;
background-color: var(--color-layer-1);
height: 64px;
padding: 0 _.unit(6);
.separator {
@include _.vertical-bar;
height: 20px;
margin: 0 _.unit(5) 0 _.unit(4);
}
.closeIcon {
color: var(--color-text-secondary);
}
.githubToolTipAnchor {
margin-right: _.unit(4);
}
.githubIcon {
div {
display: flex;
}
svg {
color: var(--color-text);
}
}
.requestSdkButton {
margin-right: _.unit(15);
}
}

View file

@ -0,0 +1,55 @@
import Close from '@/assets/icons/close.svg';
import Button from '@/ds-components/Button';
import CardTitle from '@/ds-components/CardTitle';
import DangerousRaw from '@/ds-components/DangerousRaw';
import IconButton from '@/ds-components/IconButton';
import Spacer from '@/ds-components/Spacer';
import * as styles from './index.module.scss';
type Props = {
appName: string;
isCompact?: boolean;
onClose: () => void;
};
function GuideHeaderV2({ appName, isCompact = false, onClose }: Props) {
return (
<div className={styles.header}>
{isCompact && (
<>
<CardTitle
size="small"
title={<DangerousRaw>{appName}</DangerousRaw>}
subtitle="applications.guide.header_description"
/>
<Spacer />
<IconButton size="large" onClick={onClose}>
<Close className={styles.closeIcon} />
</IconButton>
</>
)}
{!isCompact && (
<>
<IconButton size="large" onClick={onClose}>
<Close className={styles.closeIcon} />
</IconButton>
<div className={styles.separator} />
<CardTitle
size="small"
title={<DangerousRaw>{appName}</DangerousRaw>}
subtitle="applications.guide.header_description"
/>
<Spacer />
<Button
className={styles.requestSdkButton}
type="outline"
title={<DangerousRaw>Request additional SDK</DangerousRaw>}
/>
</>
)}
</div>
);
}
export default GuideHeaderV2;

View file

@ -0,0 +1,44 @@
@use '@/scss/underscore' as _;
.container {
display: flex;
flex-direction: column;
background-color: var(--color-base);
height: 100vh;
.content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
overflow-y: auto;
padding: _.unit(6) _.unit(6) max(10vh, 120px);
.banner {
display: flex;
align-items: center;
margin-bottom: _.unit(6);
}
}
}
.markdownContent {
margin-top: _.unit(6);
}
.actionBar {
position: absolute;
inset: auto 0 0 0;
padding: _.unit(4);
background-color: var(--color-bg-float);
.layout {
margin: 0 auto;
max-width: 858px;
> button {
margin-right: 0;
margin-left: auto;
}
}
}

View file

@ -0,0 +1,90 @@
import { DomainStatus, type Application } from '@logto/schemas';
import { MDXProvider } from '@mdx-js/react';
import { conditional } from '@silverhand/essentials';
import { useContext, Suspense } from 'react';
import guides from '@/assets/docs/guides';
import { AppDataContext } from '@/contexts/AppDataProvider';
import Button from '@/ds-components/Button';
import CodeEditor from '@/ds-components/CodeEditor';
import TextLink from '@/ds-components/TextLink';
import useCustomDomain from '@/hooks/use-custom-domain';
import DetailsSummary from '@/mdx-components/DetailsSummary';
import { applyDomain } from '@/utils/domain';
import GuideHeaderV2 from '../GuideHeaderV2';
import StepsSkeleton from '../StepsSkeleton';
import * as styles from './index.module.scss';
type Props = {
app?: Application;
isCompact?: boolean;
onClose: () => void;
};
function GuideV2({ app, isCompact, onClose }: Props) {
const { tenantEndpoint } = useContext(AppDataContext);
const { data: customDomain } = useCustomDomain();
const isCustomDomainActive = customDomain?.status === DomainStatus.Active;
const guide = guides.find(({ id }) => id === 'spa-react');
if (!app || !guide) {
throw new Error('Invalid app or guide');
}
const GuideComponent = guide.Component;
const { id: appId, secret: appSecret, name: appName, oidcClientMetadata } = app;
return (
<div className={styles.container}>
<GuideHeaderV2 appName={appName} isCompact={isCompact} onClose={onClose} />
<div className={styles.content}>
<MDXProvider
components={{
code: ({ className, children }) => {
const [, language] = /language-(\w+)/.exec(String(className ?? '')) ?? [];
return language ? (
<CodeEditor isReadonly language={language} value={String(children).trimEnd()} />
) : (
<code>{String(children).trimEnd()}</code>
);
},
a: ({ children, ...props }) => (
<TextLink {...props} target="_blank" rel="noopener noreferrer">
{children}
</TextLink>
),
details: DetailsSummary,
}}
>
<Suspense fallback={<StepsSkeleton />}>
{tenantEndpoint && (
<GuideComponent
appId={appId}
appSecret={appSecret}
endpoint={
isCustomDomainActive
? applyDomain(tenantEndpoint.toString(), customDomain.domain)
: tenantEndpoint
}
alternativeEndpoint={conditional(isCustomDomainActive && tenantEndpoint)}
redirectUris={oidcClientMetadata.redirectUris}
postLogoutRedirectUris={oidcClientMetadata.postLogoutRedirectUris}
isCompact={isCompact}
/>
)}
</Suspense>
</MDXProvider>
<nav className={styles.actionBar}>
<div className={styles.layout}>
<Button size="large" title="cloud.sie.finish_and_done" type="primary" />
</div>
</nav>
</div>
</div>
);
}
export default GuideV2;

View file

@ -178,6 +178,20 @@
--color-guide-dropdown-border: var(--color-border);
--color-skeleton-shimmer-rgb: 255, 255, 255; // rgb of Layer-1
--color-specific-tag-upsell: var(--color-primary-50);
// Background
--color-bg-body-base: var(--color-neutral-95);
--color-bg-body: var(--color-neutral-100);
--color-bg-layer-1: var(--color-static-white);
--color-bg-layer-2: var(--color-neutral-95);
--color-bg-body-overlay: var(--color-neutral-100);
--color-bg-float-base: var(--color-neutral-variant-90);
--color-bg-float: var(--color-neutral-100);
--color-bg-float-overlay: var(--color-neutral-100);
--color-bg-mask: rgba(0, 0, 0, 40%); // 4% --color-neutral-0
--color-bg-toast: var(--color-neutral-20);
--color-bg-state-unselected: var(--color-neutral-90);
--color-bg-state-disabled: rgba(25, 28, 29, 8%); // 8% --color-neutral-10
}
@mixin dark {
@ -360,4 +374,21 @@
--color-guide-dropdown-border: var(--color-neutral-variant-70);
--color-skeleton-shimmer-rgb: 42, 44, 50; // rgb of Layer-1
--color-specific-tag-upsell: var(--color-primary-70);
// Background
--color-bg-body-base: var(--color-neutral-100);
--color-bg-body: var(--color-surface);
--color-bg-body-overlay: var(--color-surface-2);
--color-bg-layer-1:
linear-gradient(0deg, rgba(202, 190, 255, 8%), rgba(202, 190, 255, 8%)),
linear-gradient(0deg, rgba(196, 199, 199, 2%), rgba(196, 199, 199, 2%)),
#191c1d;
--color-bg-layer-2: var(--color-surface-4);
--color-bg-float-base: var(--color-neutral-100);
--color-bg-float: var(--color-surface-3);
--color-bg-float-overlay: var(--color-surface-4);
--color-bg-mask: rgba(0, 0, 0, 60%); // 60% --color-neutral-100;
--color-bg-toast: var(--color-neutral-80);
--color-bg-state-unselected: var(--color-neutral-90);
--color-bg-state-disabled: rgba(247, 248, 248, 8%); // 8% --color-neutral-10
}