finish base jotai structure + firestore with suspense

This commit is contained in:
Sidney Alcantara
2022-04-22 20:30:52 +10:00
parent 2c785753a9
commit 0e037e4f0c
34 changed files with 2976 additions and 106 deletions

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@ cloud_functions/functions/lib
*.firebaserc
*-firebase.json
.firebase/
*.log
# misc
.DS_Store

View File

@@ -1,2 +1 @@
cd www
yarn lint-staged

View File

@@ -1,4 +1,4 @@
const { whenProd } = require("@craco/craco");
const { whenDev } = require("@craco/craco");
const CracoAlias = require("craco-alias");
const CracoSwcPlugin = require("craco-swc");
@@ -12,10 +12,12 @@ module.exports = {
tsConfigPath: "./tsconfig.extend.json",
},
},
// Use swc on production only since Jotai doesnt have swc plugins yet
// Use Babel on dev since Jotai doesnt have swc plugins yet
// See https://github.com/pmndrs/jotai/discussions/1057
...whenProd(
() => [
// Use swc on production and test since Babel seems to break Jest
...whenDev(
() => [],
[
{
plugin: CracoSwcPlugin,
options: {
@@ -31,11 +33,16 @@ module.exports = {
},
},
},
],
[]
]
),
],
babel: {
plugins: ["jotai/babel/plugin-debug-label"],
},
jest: {
configure: (jestConfig) => {
jestConfig.moduleNameMapper["^lodash-es$"] = "lodash";
return jestConfig;
},
},
};

View File

@@ -1,6 +1,11 @@
{
"name": "rowy",
"version": "3.0.0-alpha.0",
"homepage": "https://rowy.io",
"repository": {
"type": "git",
"url": "https://github.com/rowyio/rowy.git"
},
"private": true,
"dependencies": {
"@emotion/react": "^11.9.0",
@@ -10,27 +15,31 @@
"@mui/lab": "^5.0.0-alpha.76",
"@mui/material": "^5.6.0",
"@mui/styles": "^5.6.0",
"@rowy/multiselect": "^0.2.3",
"date-fns": "^2.28.0",
"dompurify": "^2.3.6",
"firebase": "^9.6.10",
"firebaseui": "^6.0.1",
"jotai": "^1.6.2",
"jotai": "^1.6.4",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"react": "^18.0.0",
"react-data-grid": "7.0.0-beta.5",
"react-div-100vh": "^0.7.0",
"react-dom": "^18.0.0",
"react-error-boundary": "^3.1.4",
"react-firebaseui": "^6.0.0",
"react-helmet-async": "^1.3.0",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.0",
"reactfire": "^4.2.1",
"typescript": "^4.6.3",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "cross-env PORT=7699 craco start",
"startWithEmulator": "cross-env PORT=7699 REACT_APP_FIREBASE_EMULATOR=true craco start",
"build": "craco build",
"test": "craco test",
"eject": "craco eject",
"analyze": "source-map-explorer ./build/static/js/*.js",
"prepare": "husky install",
"env": "node createDotEnv",
"target": "firebase target:apply hosting rowy",
@@ -40,11 +49,24 @@
"node": ">=16"
},
"eslintConfig": {
"plugins": [
"eslint-plugin-no-relative-import-paths",
"eslint-plugin-tsdoc"
],
"extends": [
"react-app",
"react-app/jest",
"prettier"
]
],
"rules": {
"no-relative-import-paths/no-relative-import-paths": [
"error",
{
"allowSameFolder": true
}
],
"tsdoc/syntax": "warn"
}
},
"browserslist": {
"production": [
@@ -65,12 +87,15 @@
"@craco/craco": "^6.4.3",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.0.0",
"@testing-library/react-hooks": "^8.0.0",
"@testing-library/user-event": "^14.0.4",
"@types/dompurify": "^2.3.3",
"@types/jest": "^27.4.1",
"@types/lodash": "^4.14.181",
"@types/lodash-es": "^4.17.6",
"@types/node": "^17.0.23",
"@types/react": "^18.0.0",
"@types/react": "^18.0.5",
"@types/react-div-100vh": "^0.4.0",
"@types/react-dom": "^18.0.0",
"@types/react-router-dom": "^5.3.3",
"@typescript-eslint/parser": "^5.18.0",
@@ -80,9 +105,15 @@
"eslint": "^8.12.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-react-app": "^7.0.0",
"eslint-plugin-no-relative-import-paths": "^1.2.0",
"eslint-plugin-tsdoc": "^0.2.16",
"husky": ">=7.0.4",
"lint-staged": ">=12.3.7",
"prettier": "^2.6.2"
"prettier": "^2.6.2",
"source-map-explorer": "^2.5.2"
},
"resolutions": {
"@types/react": "^18"
},
"lint-staged": {
"*.{js,ts,tsx}": "eslint --cache --fix",

View File

@@ -25,6 +25,7 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<meta name="color-scheme" content="light dark" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -3,6 +3,6 @@ import App from "./App";
test("renders learn react link", () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
const linkElement = screen.getByText(//i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -1,25 +1,55 @@
import "./App.css";
import DataGrid from "react-data-grid";
import CloudLogs from "@src/assets/icons/CloudLogs";
import { lazy, Suspense } from "react";
import { HelmetProvider } from "react-helmet-async";
const columns = [
{ key: "id", name: "ID" },
{ key: "title", name: "Title" },
];
import { ErrorBoundary } from "react-error-boundary";
import ErrorFallback from "@src/components/ErrorFallback";
const rows = [
{ id: 0, title: "Example" },
{ id: 1, title: "Demo" },
];
import { Provider } from "jotai";
import { globalScope } from "@src/atoms/globalScope";
import Loading from "@src/components/Loading";
function App() {
import FirebaseProject from "@src/sources/ProjectSourceFirebase";
import ThemeProvider from "@src/theme/ThemeProvider";
const AuthPage = lazy(
() => import("@src/pages/Auth" /* webpackChunkName: "AuthPage" */)
);
export default function App() {
return (
<div className="App">
<DataGrid columns={columns} rows={rows} />
<CloudLogs />
</div>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<HelmetProvider>
<Provider scope={globalScope}>
<ThemeProvider>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense
fallback={
<Loading
fullScreen
message="FirebaseProject suspended"
timeout={0}
delay={0}
/>
}
>
<FirebaseProject />
</Suspense>
<Suspense
fallback={
<Loading
fullScreen
message="AuthPage suspended"
timeout={0}
delay={0}
/>
}
>
<AuthPage />
</Suspense>
</ErrorBoundary>
</ThemeProvider>
</Provider>
</HelmetProvider>
</ErrorBoundary>
);
}
export default App;

5
src/atoms/auth.ts Normal file
View File

@@ -0,0 +1,5 @@
import { atom } from "jotai";
import type { User } from "firebase/auth";
export const currentUserAtom = atom<User | null | undefined>(undefined);
export const userRolesAtom = atom<string[]>([]);

1
src/atoms/globalScope.ts Normal file
View File

@@ -0,0 +1 @@
export const globalScope = Symbol("globalScope");

5
src/atoms/project.ts Normal file
View File

@@ -0,0 +1,5 @@
import { atom } from "jotai";
import { DocumentData } from "firebase/firestore";
export const publicSettingsAtom = atom<DocumentData>({});
export const projectSettingsAtom = atom<DocumentData>({});

43
src/atoms/user.ts Normal file
View File

@@ -0,0 +1,43 @@
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { DocumentData } from "firebase/firestore";
import { merge } from "lodash-es";
import themes from "@src/theme";
import { publicSettingsAtom } from "./project";
export const userSettingsAtom = atom<DocumentData>({});
export const themeAtom = atomWithStorage<"light" | "dark">(
"__ROWY__THEME",
"light"
);
export const themeOverriddenAtom = atomWithStorage(
"__ROWY__THEME_OVERRIDDEN",
false
);
export const customizedThemesAtom = atom((get) => {
const publicSettings = get(publicSettingsAtom);
const userSettings = get(userSettingsAtom);
const lightCustomizations = merge(
{},
publicSettings.theme?.base,
publicSettings.theme?.light,
userSettings.theme?.base,
userSettings.theme?.light
);
const darkCustomizations = merge(
{},
publicSettings.theme?.base,
publicSettings.theme?.dark,
userSettings.theme?.base,
userSettings.theme?.dark
);
return {
light: themes.light(lightCustomizations),
dark: themes.dark(darkCustomizations),
};
});

View File

@@ -0,0 +1,19 @@
import { CircularProgress, CircularProgressProps } from "@mui/material";
export default function CircularProgressOptical({
size = 40,
...props
}: CircularProgressProps & { size?: number }) {
const DEFAULT_SIZE = 40;
const DEFAULT_THICKNESS = 3.6;
const linearThickness = (DEFAULT_SIZE / size) * DEFAULT_THICKNESS;
const opticalRatio = 1 - (1 - size / DEFAULT_SIZE) / 2;
return (
<CircularProgress
{...props}
size={size}
thickness={linearThickness * opticalRatio}
/>
);
}

View File

@@ -0,0 +1,106 @@
import { use100vh } from "react-div-100vh";
import {
Grid,
GridProps,
Stack,
Typography,
SvgIconTypeMap,
} from "@mui/material";
import { OverridableComponent } from "@mui/material/OverridableComponent";
import ErrorIcon from "@mui/icons-material/ErrorOutline";
export interface IEmptyStateProps extends Partial<GridProps> {
/** Primary message displayed under the icon */
message?: React.ReactNode;
/** Description text displayed under primary message */
description?: React.ReactNode;
/** Override icon component */
Icon?: OverridableComponent<SvgIconTypeMap>;
/** Set height to `100vh`. Default: `false` */
fullScreen?: boolean;
/** Basic inline presentation without padding. Default: `false` */
basic?: boolean;
}
/**
* Display an empty state message with sensible defaults.
* By default, height is `100%`.
* Override with props that are passed to the root MUI `Grid` component.
*/
export default function EmptyState({
message = "Nothing here",
description,
Icon = ErrorIcon,
fullScreen = false,
basic = false,
...props
}: IEmptyStateProps) {
const fullScreenHeight = use100vh() ?? "100vh";
if (basic)
return (
<Grid container alignItems="center" spacing={1} {...props}>
<Grid item>
<Icon style={{ display: "block" }} />
</Grid>
<Grid item>
{message}
{description && ": "}
{description}
</Grid>
</Grid>
);
return (
<Grid
container
direction="column"
justifyContent="center"
alignItems="center"
{...props}
style={{
width: "100%",
height: fullScreen ? fullScreenHeight : "100%",
textAlign: "center",
...props.style,
}}
>
<Grid
item
sx={{
maxWidth: "25em !important",
width: (theme) => `calc(100% - ${theme.spacing(1 * 2)})`,
px: 1,
typography: "body2",
"& .icon": {
color: "action.active",
fontSize: "3rem",
mx: "auto",
display: "block",
mb: 1,
},
}}
>
<Icon className="icon" />
<Typography
component="h1"
variant="h6"
gutterBottom
style={{ cursor: "default" }}
>
{message}
</Typography>
{description && (
<Stack spacing={2} alignItems="center">
{description}
</Stack>
)}
</Grid>
</Grid>
);
}

View File

@@ -0,0 +1,61 @@
import { FallbackProps } from "react-error-boundary";
import { Button } from "@mui/material";
import ReloadIcon from "@mui/icons-material/Refresh";
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
import EmptyState, { IEmptyStateProps } from "@src/components/EmptyState";
import meta from "@root/package.json";
export interface IErrorFallbackProps extends FallbackProps, IEmptyStateProps {}
export default function ErrorFallback({
error,
resetErrorBoundary,
...props
}: IErrorFallbackProps) {
if (error.message.startsWith("Loading chunk"))
return (
<EmptyState
Icon={ReloadIcon}
message="New update available"
description={
<>
<span>Reload this page to get the latest update</span>
<Button
variant="outlined"
color="secondary"
startIcon={<ReloadIcon />}
onClick={() => window.location.reload()}
>
Reload
</Button>
</>
}
fullScreen
/>
);
return (
<EmptyState
message="Something went wrong"
description={
<>
<span>{error.message}</span>
<Button
href={
meta.repository.url.replace(".git", "") + "/issues/new/choose"
}
target="_blank"
rel="noopener noreferrer"
>
Report issue
<InlineOpenInNewIcon />
</Button>
</>
}
fullScreen
{...props}
/>
);
}

View File

@@ -0,0 +1,19 @@
import { styled } from "@mui/material";
export const InlineOpenInNewIcon = styled("span")(() => ({
position: "relative",
width: "1em",
height: "1em",
marginLeft: "0.25ch",
display: "inline-block",
verticalAlign: "baseline",
"&::after": {
content: "'\\2197'",
position: "absolute",
top: 0,
left: 0,
},
}));
export default InlineOpenInNewIcon;

View File

@@ -0,0 +1,52 @@
import { use100vh } from "react-div-100vh";
import { Fade, Stack, StackProps, Typography } from "@mui/material";
import CircularProgressOptical from "@src/components/CircularProgressOptical";
interface ILoadingProps extends Partial<StackProps> {
message?: string;
fullScreen?: boolean;
timeout?: number;
delay?: number;
}
export default function Loading({
message = "Loading",
fullScreen = false,
timeout = 1000,
delay = 1000,
...props
}: ILoadingProps) {
const fullScreenHeight = use100vh() ?? "100vh";
return (
<Fade
in
timeout={timeout}
style={{ transitionDelay: `${delay}ms` }}
unmountOnExit
>
<Stack
justifyContent="center"
alignItems="center"
spacing={1}
{...props}
style={{
width: "100%",
height: fullScreen ? fullScreenHeight : "100%",
alignItems: "center",
...props.style,
}}
>
<CircularProgressOptical />
<Typography
variant="subtitle1"
component="div"
style={{ userSelect: "none" }}
>
{message}
</Typography>
</Stack>
</Fade>
);
}

View File

@@ -0,0 +1,55 @@
import { useEffect } from "react";
import { useAtom, PrimitiveAtom } from "jotai";
import { Scope } from "jotai/core/atom";
import { useUpdateAtom } from "jotai/utils";
import {
doc,
DocumentData,
onSnapshot,
FirestoreError,
} from "firebase/firestore";
import { globalScope } from "@src/atoms/globalScope";
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
/**
* Attaches a listener for Firestore documents and unsubscribes on unmount.
* Gets the Firestore instance initiated in globalScope.
* Updates an atom and suspends that atom until the first snapshot is received.
*
* @param dataAtom - Atom to store data in
* @param dataScope - Atom scope
* @param path - Document path
* @param pathSegments - Additional path segments appended to the path
* @param onError - Called when an error occurs. Make sure to wrap in useCallback!
*/
export default function useFirestoreDocWithAtom(
dataAtom: PrimitiveAtom<DocumentData>,
dataScope: Scope | undefined,
path: string | undefined,
pathSegments?: Array<string | undefined>,
onError?: (error: FirestoreError) => void
) {
const [firebaseDb] = useAtom(firebaseDbAtom, globalScope);
const setDataAtom = useUpdateAtom(dataAtom, dataScope);
useEffect(() => {
if (!path || (Array.isArray(pathSegments) && pathSegments.some((x) => !x)))
return;
// Suspend data atom until we get the first snapshot
setDataAtom(new Promise(() => {}));
const unsubscribe = onSnapshot(
doc(firebaseDb, path, ...((pathSegments as string[]) || [])),
(doc) => {
setDataAtom(doc.data()!);
},
onError
);
return () => {
unsubscribe();
};
}, [firebaseDb, path, pathSegments, onError, setDataAtom]);
}

View File

@@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -1,6 +1,6 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

77
src/pages/Auth.tsx Normal file
View File

@@ -0,0 +1,77 @@
import { useAtom } from "jotai";
import { globalScope } from "@src/atoms/globalScope";
import { currentUserAtom, userRolesAtom } from "@src/atoms/auth";
import { publicSettingsAtom } from "@src/atoms/project";
// import StyledFirebaseAuth from "react-firebaseui/FirebaseAuth";
// import "firebase/compat/auth";
// import { GoogleAuthProvider } from "firebase/auth";
import { firebaseAuthAtom } from "@src/sources/ProjectSourceFirebase";
import { Button } from "@mui/material";
import {
GoogleAuthProvider,
signInWithPopup,
signOut,
User,
} from "firebase/auth";
import { userSettingsAtom } from "@src/atoms/user";
const provider = new GoogleAuthProvider();
function CurrentUser({ currentUser }: { currentUser: User }) {
console.log("currentUser", currentUser);
return <p>{currentUser?.email}</p>;
}
function Auth() {
const [firebaseAuth] = useAtom(firebaseAuthAtom, globalScope);
const [currentUser] = useAtom(currentUserAtom, globalScope);
const [userRoles] = useAtom(userRolesAtom, globalScope);
const [publicSettings] = useAtom(publicSettingsAtom, globalScope);
const [userSettings] = useAtom(userSettingsAtom, globalScope);
console.log("publicSettings", publicSettings);
console.log("userSettings", userSettings);
return (
<>
<Button
variant={currentUser ? "outlined" : "contained"}
color={currentUser ? "secondary" : "primary"}
onClick={() => {
signInWithPopup(firebaseAuth, provider);
}}
sx={{ my: 4, mx: 1 }}
>
Sign in with Google
</Button>
<Button
variant={!currentUser ? "outlined" : "contained"}
color={!currentUser ? "secondary" : "primary"}
onClick={() => {
signOut(firebaseAuth);
}}
sx={{ my: 4, mx: 1 }}
>
Sign out
</Button>
{currentUser === undefined && <p>Authenticating </p>}
<CurrentUser currentUser={currentUser!} />
<p>{JSON.stringify(userRoles)}</p>
<p>{JSON.stringify(publicSettings)}</p>
<p>{JSON.stringify(userSettings)}</p>
{/* <StyledFirebaseAuth
uiConfig={{
signInFlow: "popup",
signInSuccessUrl: "/",
signInOptions: [GoogleAuthProvider.PROVIDER_ID],
}}
firebaseAuth={firebaseAuth}
/> */}
</>
);
}
export default Auth;

View File

@@ -0,0 +1,93 @@
import { useEffect } from "react";
import { atom, useAtom } from "jotai";
import { FirebaseOptions, initializeApp } from "firebase/app";
import { getAuth, connectAuthEmulator, getIdTokenResult } from "firebase/auth";
import { getFirestore, connectFirestoreEmulator } from "firebase/firestore";
import { currentUserAtom, userRolesAtom } from "@src/atoms/auth";
import useFirestoreDocWithAtom from "@src/hooks/useFirestoreDocWithAtom";
import { globalScope } from "@src/atoms/globalScope";
import { publicSettingsAtom } from "@src/atoms/project";
import { useUpdateAtom } from "jotai/utils";
import { userSettingsAtom } from "@src/atoms/user";
const envConfig = {
apiKey: process.env.REACT_APP_FIREBASE_PROJECT_WEB_API_KEY,
authDomain: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseapp.com`,
databaseURL: `https://${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseio.com`,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.appspot.com`,
};
// Connect emulators based on env vars
const envConnectEmulators =
process.env.NODE_ENV === "test" ||
process.env.REACT_APP_FIREBASE_EMULATOR === "true";
// Store Firebase config here so it can be set programmatically.
// This lets us switch between Firebase projects.
// Then app, auth, db, storage need to be derived atoms.
export const firebaseConfigAtom = atom<FirebaseOptions>(envConfig);
export const firebaseAppAtom = atom((get) =>
initializeApp(get(firebaseConfigAtom))
);
export const firebaseAuthAtom = atom((get) => {
const auth = getAuth(get(firebaseAppAtom));
if (envConnectEmulators) connectAuthEmulator(auth, "http://localhost:9099");
return auth;
});
export const firebaseDbAtom = atom((get) => {
const db = getFirestore(get(firebaseAppAtom));
if (envConnectEmulators) connectFirestoreEmulator(db, "localhost", 8080);
return db;
});
export default function ProjectSourceFirebase() {
// Get current user and store in atoms
const [firebaseAuth] = useAtom(firebaseAuthAtom, globalScope);
// const setCurrentUser: any = useUpdateAtom(currentUserAtom, globalScope);
const [currentUser, setCurrentUser] = useAtom(currentUserAtom, globalScope);
const setUserRoles = useUpdateAtom(userRolesAtom, globalScope);
useEffect(() => {
// Suspend when currentUser has not been read yet
(setCurrentUser as any)(new Promise(() => {}));
const unsubscribe = firebaseAuth.onAuthStateChanged(async (user) => {
setCurrentUser(user);
if (user) {
const tokenResult = await getIdTokenResult(user);
setUserRoles((tokenResult.claims.roles as string[]) ?? []);
} else {
setUserRoles([]);
}
});
return () => {
unsubscribe();
};
}, [firebaseAuth, setCurrentUser, setUserRoles]);
// Store public settings in atom
useFirestoreDocWithAtom(
publicSettingsAtom,
globalScope,
"_rowy_/publicSettings"
);
// Store user settings in atom when a user is signed in
useFirestoreDocWithAtom(
userSettingsAtom,
globalScope,
`_rowy_/userManagement/users`,
[currentUser?.uid]
);
return null;
}

View File

@@ -0,0 +1,35 @@
import { useState } from "react";
import { useUpdateAtom } from "jotai/utils";
import { firebaseConfigAtom } from "@src/sources/ProjectSourceFirebase";
import { globalScope } from "@src/atoms/globalScope";
const envConfig = {
apiKey: process.env.REACT_APP_FIREBASE_PROJECT_WEB_API_KEY,
authDomain: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseapp.com`,
databaseURL: `https://${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseio.com`,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.appspot.com`,
appId: "x",
};
export default function RowyProject({ children }: React.PropsWithChildren<{}>) {
const [hasConfig, setHasConfig] = useState(false);
const setConfigAtom = useUpdateAtom(firebaseConfigAtom, globalScope);
if (!hasConfig) {
return (
<div>
<button
onClick={() => {
setConfigAtom(envConfig);
setHasConfig(true);
}}
>
Load Firebase project
</button>
</div>
);
}
return <>{children}</>;
}

View File

@@ -0,0 +1,84 @@
import { Box } from "@mui/material";
import { toRem } from "./typography";
export default function CheckboxIcon() {
return (
<Box
component="span"
sx={{
width: toRem(18),
height: toRem(18),
margin: toRem((24 - 18) / 2),
borderRadius: 1,
display: "flex",
position: "relative",
bgcolor: "action.input",
border: "1px solid",
borderColor: "text.disabled",
color: "primary.main",
transition: (theme) =>
theme.transitions.create(["background-color", "border-color"], {
easing: theme.transitions.easing.easeIn,
duration: theme.transitions.duration.shortest,
delay: theme.transitions.duration.shortest,
}),
"& svg": {
position: "absolute",
width: toRem(18),
height: toRem(18),
top: -1,
left: -1,
color: "inherit",
},
"& .tick": {
stroke: (theme) => theme.palette.primary.contrastText,
strokeDasharray: 18,
strokeDashoffset: 18,
transition: (theme) =>
theme.transitions.create(["stroke-dashoffset"], {
easing: theme.transitions.easing.easeIn,
duration: theme.transitions.duration.shortest,
}),
boxShadow: 1,
},
".Mui-checked &, [aria-selected='true'] &": {
backgroundColor: "currentColor",
borderColor: "currentColor",
transition: (theme) =>
theme.transitions.create(["background-color", "border-color"], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.shortest,
}),
"& .tick": {
strokeDashoffset: 0,
transition: (theme) =>
theme.transitions.create(["stroke-dashoffset"], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.shortest,
delay: theme.transitions.duration.shortest,
}),
},
},
}}
>
<svg viewBox="0 0 18 18">
<polyline
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
points="2.705 8.29 7 12.585 15.295 4.29"
fill="none"
className="tick"
/>
</svg>
</Box>
);
}

View File

@@ -0,0 +1,85 @@
import { Box } from "@mui/material";
import { toRem } from "./typography";
export default function CheckboxIndeterminateIcon() {
return (
<Box
component="span"
sx={{
width: toRem(18),
height: toRem(18),
margin: toRem((24 - 18) / 2),
borderRadius: 1,
display: "flex",
position: "relative",
bgcolor: "action.input",
border: "1px solid",
borderColor: "text.disabled",
color: "primary.main",
transition: (theme) =>
theme.transitions.create(["background-color", "border-color"], {
easing: theme.transitions.easing.easeIn,
duration: theme.transitions.duration.shortest,
delay: theme.transitions.duration.shortest,
}),
"& svg": {
position: "absolute",
width: toRem(18),
height: toRem(18),
top: -1,
left: -1,
color: "inherit",
},
"& .tick": {
stroke: (theme) => theme.palette.primary.contrastText,
strokeDasharray: 12,
strokeDashoffset: 12,
transition: (theme) =>
theme.transitions.create(["stroke-dashoffset"], {
easing: theme.transitions.easing.easeIn,
duration: theme.transitions.duration.shortest,
}),
boxShadow: 1,
},
".Mui-checked &, [aria-selected='true'] &": {
backgroundColor: "currentColor",
borderColor: "currentColor",
transition: (theme) =>
theme.transitions.create(["background-color", "border-color"], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.shortest,
}),
"& .tick": {
strokeDashoffset: 0,
transition: (theme) =>
theme.transitions.create(["stroke-dashoffset"], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.shortest,
delay: theme.transitions.duration.shortest,
}),
},
},
}}
>
<svg viewBox="0 0 18 18">
<line
x1="3"
y1="9"
x2="15"
y2="9"
strokeWidth="2"
strokeLinecap="round"
className="tick"
/>
</svg>
</Box>
);
}

66
src/theme/RadioIcon.tsx Normal file
View File

@@ -0,0 +1,66 @@
import { Box } from "@mui/material";
import { toRem } from "./typography";
export default function RadioIcon() {
return (
<Box
component="span"
sx={{
width: toRem(20),
height: toRem(20),
margin: toRem((24 - 20) / 2),
borderRadius: "50%",
display: "flex",
backgroundColor: "transparent",
border: "1px solid",
borderColor: "text.disabled",
color: "primary.main",
transition: (theme) =>
theme.transitions.create(["background-color", "border-color"], {
easing: theme.transitions.easing.easeIn,
duration: theme.transitions.duration.shortest,
}),
"&::before": {
content: '""',
width: "100%",
height: "100%",
borderRadius: "50%",
display: "block",
bgcolor: "action.input",
transition: (theme) =>
theme.transitions.create(["transform", "background-color"], {
easing: theme.transitions.easing.easeIn,
duration: theme.transitions.duration.shortest,
}),
},
".Mui-checked &, [aria-selected='true'] &": {
backgroundColor: "currentColor",
borderColor: "currentColor",
transition: (theme) =>
theme.transitions.create(["background-color", "border-color"], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.shortest,
}),
"&::before": {
bgcolor: "primary.contrastText",
boxShadow: 1,
transform: `scale(${12 / 20})`,
transition: (theme) =>
theme.transitions.create(["transform", "background-color"], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.shortest,
}),
},
},
}}
/>
);
}

View File

@@ -0,0 +1,60 @@
import { useEffect } from "react";
import { useAtom } from "jotai";
import { Helmet } from "react-helmet-async";
import {
useMediaQuery,
ThemeProvider as MuiThemeProvider,
CssBaseline,
} from "@mui/material";
import { globalScope } from "@src/atoms/globalScope";
import {
themeAtom,
themeOverriddenAtom,
customizedThemesAtom,
} from "@src/atoms/user";
/**
* Injects the MUI theme with customizations from project and user settings.
* Also adds dark mode support.
*/
export default function ThemeProvider({
children,
}: React.PropsWithChildren<{}>) {
const [theme, setTheme] = useAtom(themeAtom, globalScope);
const [themeOverridden] = useAtom(themeOverriddenAtom, globalScope);
const [customizedThemes] = useAtom(customizedThemesAtom, globalScope);
// Infer theme based on system settings
const prefersDarkTheme = useMediaQuery("(prefers-color-scheme: dark)", {
noSsr: true,
});
// Update theme when system settings change
useEffect(() => {
if (themeOverridden) return;
setTheme(prefersDarkTheme ? "dark" : "light");
}, [prefersDarkTheme, themeOverridden, setTheme]);
// Sync theme to body data-theme attribute for Feedback Fin
useEffect(() => {
document.body.setAttribute("data-theme", theme);
}, [theme]);
return (
<>
{Array.isArray(customizedThemes[theme].typography.fontCssUrls) && (
<Helmet>
{customizedThemes[theme].typography.fontCssUrls!.map((url) => (
<link key={url} rel="stylesheet" href={url} />
))}
</Helmet>
)}
<MuiThemeProvider theme={customizedThemes[theme]}>
<CssBaseline />
{children}
</MuiThemeProvider>
</>
);
}

209
src/theme/colors.ts Normal file
View File

@@ -0,0 +1,209 @@
import { ThemeOptions } from "@mui/material/styles";
import { Shadows } from "@mui/material/styles/shadows";
import { colord, extend } from "colord";
import lchPlugin from "colord/plugins/lch";
extend([lchPlugin]);
declare module "@mui/material/styles/createPalette" {
interface TypeAction {
activeOpacity: number;
input: string;
inputOutline: string;
}
}
export const PRIMARY = "#4200FF";
export const ERROR = "#B00020"; // https://material.io/design/color/dark-theme.html#ui-application
export const DARK_PRIMARY = "#B0B6FD"; // l: 75, c: 65, h: 275
export const colorsLight = (
_primary: Parameters<typeof colord>[0] = PRIMARY
): ThemeOptions => {
const primary = colord(_primary);
const h = primary.toLch().h;
const secondary = colord({ l: 10, c: 10, h });
const secondaryDark = colord({ l: 0, c: 0, h });
const bgDefault = colord({ l: 98, c: 1, h });
const textBase = colord({ l: 0, c: 10, h });
const shadowBase = colord({ l: 0, c: 10, h });
const tooltip = shadowBase.alpha(0.8);
return {
palette: {
primary: { main: primary.toHslString() },
secondary: {
main: secondary.toHslString(),
dark: secondaryDark.toHslString(),
},
error: { main: ERROR },
success: { light: "#34c759", main: "#00802e", dark: "#105e24" },
background: { default: bgDefault.toHslString() },
text: {
primary: textBase.alpha(0.87).toHslString(),
secondary: textBase.alpha(0.6).toHslString(),
disabled: textBase.alpha(0.38).toHslString(),
},
action: {
active: textBase.alpha(0.6).toHslString(),
activeOpacity: 0.6,
hover: textBase.alpha(0.04).toHslString(),
selected: textBase.alpha(0.08).toHslString(),
disabled: textBase.alpha(0.26).toHslString(),
disabledBackground: textBase.alpha(0.12).toHslString(),
input: "#fff",
inputOutline: shadowBase.alpha(0.12).toRgbString(),
},
divider: shadowBase.alpha(0.12).toRgbString(), // Using hsl string breaks table borders
},
shadows: new Array(25).fill(undefined).map((_, i) => {
// Based on https://tailwindcss.com/docs/box-shadow
// with additional “outline” shadow
// and bigger shadow from https://github.com/outline/outline/blob/37fd7ec97a496094077a59f4d10fa0081516e3ef/shared/theme.js#L148
if (i === 0) return "none";
if (i === 1)
// prettier-ignore
return `0 0 0 1px ${shadowBase.alpha(0.03).toHslString()}, 0 1px 2px 0 ${shadowBase.alpha(0.1).toHslString()}`;
if (i < 4)
// prettier-ignore
return `0 0 0 1px ${shadowBase.alpha(0.04).toHslString()}, 0 1px 3px 0 ${shadowBase.alpha(0.1).toHslString()}, 0 1px 2px 0 ${shadowBase.alpha(0.06).toHslString()}`;
if (i < 8)
// prettier-ignore
return `0 0 0 1px ${shadowBase.alpha(0.05).toHslString()}, 0 4px 6px -1px ${shadowBase.alpha(0.1).toHslString()}, 0 2px 4px ${shadowBase.alpha(0.06).toHslString()}`;
if (i < 16)
// prettier-ignore
return `0 0 0 1px ${shadowBase.alpha(0.06).toHslString()}, 0 10px 15px -3px ${shadowBase.alpha(0.1).toHslString()}, 0 4px 6px ${shadowBase.alpha(0.05).toHslString()}, 0 30px 40px ${shadowBase.alpha(0.05).toHslString()}`;
if (i < 24)
// prettier-ignore
return `0 0 0 1px ${shadowBase.alpha(0.08).toHslString()}, 0 20px 25px -5px ${shadowBase.alpha(0.1).toHslString()}, 0 10px 10px ${shadowBase.alpha(0.04).toHslString()}, 0 40px 60px ${shadowBase.alpha(0.06).toHslString()}`;
else
// prettier-ignore
return `0 0 0 1px ${shadowBase.alpha(0.08).toHslString()}, 0 25px 50px -12px ${shadowBase.alpha(0.25).toHslString()}, 0 50px 80px ${shadowBase.alpha(0.06).toHslString()}`;
}) as Shadows,
components: {
MuiCssBaseline: {
styleOverrides: {
":root": { colorScheme: "light" },
".rdg": { colorScheme: "light" },
},
},
MuiBackdrop: {
styleOverrides: {
root: {
backgroundColor: colord({ l: 70, c: 5, h })
.alpha(0.6)
.toHslString(),
},
invisible: { backgroundColor: "transparent" },
},
},
MuiTooltip: {
styleOverrides: {
tooltip: { backgroundColor: tooltip.toHslString() },
arrow: { color: tooltip.toHslString() },
},
},
MuiSlider: {
styleOverrides: {
valueLabel: { backgroundColor: secondary.toHslString() },
},
},
},
};
};
export const colorsDark = (
_primary: Parameters<typeof colord>[0] = DARK_PRIMARY,
darker?: boolean
): ThemeOptions => {
const primary = colord(_primary);
const h = primary.toLch().h;
const secondary = colord({ l: 96, c: 1, h });
const bgDefault = darker ? colord("#000") : colord({ l: 5, c: 2, h });
const bgPaper = darker ? colord("#000") : bgDefault;
const shadowBase = colord({ l: 0, c: 2, h });
return {
palette: {
mode: "dark",
primary: { main: primary.toHslString() },
secondary: { main: secondary.toHslString() },
background: {
default: bgDefault.toHslString(),
paper: bgPaper.toHslString(),
},
error: {
main: colord({
l: 75,
c: 72,
h: colord(ERROR).toLch().h,
}).toHslString(),
},
action: {
active: "rgba(255, 255, 255, 0.7)",
activeOpacity: 0.7,
hover: "rgba(255, 255, 255, 0.08)",
hoverOpacity: 0.08,
input: "rgba(255, 255, 255, 0.06)",
inputOutline: "rgba(255, 255, 255, 0.08)",
},
// success: { light: "#34c759" },
},
shadows: new Array(25).fill(undefined).map((_, i) => {
// Based on https://tailwindcss.com/docs/box-shadow
// with additional “outline” shadow
// and bigger shadow from https://github.com/outline/outline/blob/37fd7ec97a496094077a59f4d10fa0081516e3ef/shared/theme.js#L148
if (i === 0) return "none";
if (i === 1)
// prettier-ignore
return `0 0 0 1px ${shadowBase.alpha(0.03 * 4).toHslString()}, 0 1px 2px 0 ${shadowBase.alpha(0.1 * 4).toHslString()}`;
if (i < 4)
// prettier-ignore
return `0 0 0 1px ${shadowBase.alpha(0.04 * 4).toHslString()}, 0 1px 3px 0 ${shadowBase.alpha(0.1 * 4).toHslString()}, 0 1px 2px 0 ${shadowBase.alpha(0.06 * 4).toHslString()}`;
if (i < 8)
// prettier-ignore
return `0 0 0 1px ${shadowBase.alpha(0.05 * 4).toHslString()}, 0 4px 6px -1px ${shadowBase.alpha(0.1 * 4).toHslString()}, 0 2px 4px ${shadowBase.alpha(0.06 * 4).toHslString()}`;
if (i < 16)
// prettier-ignore
return `0 0 0 1px ${shadowBase.alpha(0.06 * 4).toHslString()}, 0 10px 15px -3px ${shadowBase.alpha(0.1 * 4).toHslString()}, 0 4px 6px ${shadowBase.alpha(0.05 * 4).toHslString()}, 0 30px 40px ${shadowBase.alpha(0.05 * 4).toHslString()}`;
if (i < 24)
// prettier-ignore
return `0 0 0 1px ${shadowBase.alpha(0.08 * 4).toHslString()}, 0 20px 25px -5px ${shadowBase.alpha(0.1 * 4).toHslString()}, 0 10px 10px ${shadowBase.alpha(0.04 * 4).toHslString()}, 0 40px 60px ${shadowBase.alpha(0.06 * 4).toHslString()}`;
else
// prettier-ignore
return `0 0 0 1px ${shadowBase.alpha(0.08 * 4).toHslString()}, 0 25px 50px -12px ${shadowBase.alpha(0.25 * 4).toHslString()}, 0 50px 80px ${shadowBase.alpha(0.06 * 4).toHslString()}`;
}) as Shadows,
components: {
MuiCssBaseline: {
styleOverrides: {
":root": { colorScheme: "dark" },
".rdg": { colorScheme: "dark" },
},
},
MuiBackdrop: {
styleOverrides: {
root: {
backgroundColor: colord({ l: 0, c: 1, h }).alpha(0.6).toHslString(),
},
invisible: { backgroundColor: "transparent" },
},
},
},
};
};

1211
src/theme/components.tsx Normal file

File diff suppressed because it is too large Load Diff

53
src/theme/index.tsx Normal file
View File

@@ -0,0 +1,53 @@
import { createTheme, ThemeOptions } from "@mui/material/styles";
import _merge from "lodash/merge";
import { typography } from "@src/theme/typography";
import { colorsLight, colorsDark } from "@src/theme/colors";
import { components } from "@src/theme/components";
export const customizableLightTheme = (customization: ThemeOptions) => {
const customizedLightThemeBase = createTheme(
_merge(
{},
typography((customization?.typography as any) ?? {}),
colorsLight((customization?.palette?.primary as any)?.main)
)
);
return createTheme(
_merge(
{},
customizedLightThemeBase,
components(customizedLightThemeBase),
customization
)
);
};
export const customizableDarkTheme = (customization: ThemeOptions) => {
const customizedDarkThemeBase = createTheme(
_merge(
{},
typography((customization?.typography as any) ?? {}),
colorsDark(
(customization?.palette?.primary as any)?.main,
(customization?.palette as any)?.darker
)
)
);
return createTheme(
_merge(
{},
customizedDarkThemeBase,
components(customizedDarkThemeBase),
customization
)
);
};
const themes = {
light: customizableLightTheme,
dark: customizableDarkTheme,
};
export default themes;

148
src/theme/palette.ts Normal file
View File

@@ -0,0 +1,148 @@
export const palette = {
aBlack: {
500: "#282829",
},
aWhite: {
500: "#ffffff",
},
aRed: {
100: "#fae4e5",
300: "#fb8c8c",
500: "#ed4747",
600: "#e91c1c",
700: "#c12929",
},
aGray: {
50: "#fafafa",
100: "#f2f2f2",
200: "#e9e9e9",
300: "#cccccc",
500: "#999999",
700: "#595959",
},
gray: {
100: "#f2f2f2",
300: "#cccccc",
500: "#999999",
700: "#595959",
},
blueGray: {
100: "#93a4ad",
300: "#647c8a",
500: "#485a63",
700: "#394c55",
},
indigo: {
100: "#7986cb",
300: "#3f51b5",
500: "#303f9f",
700: "#213092",
},
blue: {
100: "#64b5f6",
300: "#2196f3",
500: "#1976d2",
700: "#0a59a8",
},
lightBlue: {
100: "#4fc3f7",
300: "#03a9f4",
500: "#0b8ed6",
700: "#007fc5",
},
cyan: {
100: "#4dd0e1",
300: "#00bcd4",
500: "#0097a7",
700: "#258493",
},
teal: {
100: "#4db6ac",
300: "#049587",
500: "#037b6d",
700: "#006055",
},
green: {
100: "#81c784",
300: "#4caf50",
500: "#388e3c",
700: "#2e7d32",
},
lightGreen: {
100: "#aed581",
300: "#8bc34a",
500: "#689f38",
700: "#4f6f33",
},
lime: {
100: "#dce775",
300: "#cddc39",
500: "#afb42b",
700: "#8e9d01",
},
yellow: {
100: "#fff176",
300: "#ffeb3b",
500: "#fbc02d",
700: "#d07e04",
},
amber: {
100: "#ffd54f",
300: "#ffc107",
500: "#ffa000",
700: "#b38c2b",
},
orange: {
100: "#ffb74d",
300: "#ff9800",
500: "#f57c00",
700: "#bb661e",
},
brown: {
100: "#a1887f",
300: "#795548",
500: "#5d4037",
700: "#4e3229",
},
tangerine: {
100: "#ff8a65",
300: "#ff5722",
500: "#e64a19",
700: "#c0360a",
},
errorRed: {
100: "#e57373",
300: "#f44336",
500: "#d32f2f",
700: "#c62323",
},
pink: {
100: "#f06292",
300: "#e91e63",
500: "#c2185b",
700: "#b0104f",
},
purple: {
100: "#ba68c8",
300: "#9c27b0",
500: "#7b1fa2",
700: "#650e89",
},
violet: {
100: "#9575cd",
300: "#673ab7",
500: "#512da8",
700: "#341878",
},
} as const;
export const paletteToMui = (
color: Record<"700" | "300" | "500" | "100", string>
) => ({
main: color[500],
light: color[300],
dark: color[700],
});
export default palette;

169
src/theme/typography.ts Normal file
View File

@@ -0,0 +1,169 @@
import { ThemeOptions } from "@mui/material/styles";
import {
FontStyle,
TypographyStyleOptions,
} from "@mui/material/styles/createTypography";
declare module "@mui/material/styles/createTypography" {
interface FontStyle {
fontFamilyMono: string;
fontFamilyHeading: string;
fontCssUrls?: string[];
}
}
export const BODY_FONT = "Inter, system-ui, sans-serif";
export const HEADING_FONT = "Space Grotesk, " + BODY_FONT;
export const MONO_FONT = "JetBrains Mono, ui-monospace, monospace";
export const ROOT_FONT_SIZE = 16;
export const toRem = (px: number) => `${px / ROOT_FONT_SIZE}rem`;
export const toEm = (px: number, root: number) => `${px / root}em`;
export const typography = ({
fontFamily: customizedFontFamily,
fontFamilyMono: customizedFontFamilyMono,
fontFamilyHeading: customizedFontFamilyHeading,
fontWeightLight = 300,
fontWeightRegular = 400,
fontWeightMedium = 500,
fontWeightBold = 600,
}: Partial<ThemeOptions["typography"] & FontStyle>): ThemeOptions => {
const fontFamily = [customizedFontFamily, BODY_FONT]
.filter((x) => x)
.join(", ");
const fontFamilyMono = [customizedFontFamilyMono, MONO_FONT]
.filter((x) => x)
.join(", ");
const fontFamilyHeading = [customizedFontFamilyHeading, HEADING_FONT]
.filter((x) => x)
.join(", ");
const fontStyleBody: TypographyStyleOptions = {
fontFamily,
fontFeatureSettings:
fontFamily !== BODY_FONT
? "normal"
: `"calt", "ss01", "ss03", "cv05", "cv08", "cv09"`,
};
const fontStyleHeading: TypographyStyleOptions = {
fontFamily: fontFamilyHeading,
fontFeatureSettings:
fontFamilyHeading !== HEADING_FONT ? "normal" : `"ss02", "ss03"`,
};
return {
typography: {
fontFamily,
fontFamilyMono,
fontFamilyHeading,
fontWeightLight,
fontWeightRegular,
fontWeightMedium,
fontWeightBold,
h1: {
...fontStyleHeading,
fontWeight: fontWeightBold,
fontSize: toRem(96),
letterSpacing: toEm(-1.5, 96),
lineHeight: 112 / 96,
},
h2: {
...fontStyleHeading,
fontWeight: fontWeightBold,
fontSize: toRem(60),
letterSpacing: toEm(-0.75, 60),
lineHeight: 72 / 60,
},
h3: {
...fontStyleHeading,
fontWeight: fontWeightBold,
fontSize: toRem(48),
letterSpacing: toEm(-0.5, 48),
lineHeight: 60 / 48,
},
h4: {
...fontStyleHeading,
fontWeight: fontWeightBold,
fontSize: toRem(34),
letterSpacing: toEm(-0.35, 34),
lineHeight: 44 / 34,
},
h5: {
...fontStyleHeading,
fontWeight: fontWeightBold,
fontSize: toRem(24),
letterSpacing: "0",
// letterSpacing: toEm(-0.2, 24),
lineHeight: 32 / 24,
},
h6: {
...fontStyleHeading,
fontWeight: fontWeightBold,
fontSize: toRem(20),
letterSpacing: "0",
// letterSpacing: toEm(-0.15, 20),
lineHeight: 28 / 20,
},
subtitle1: {
...fontStyleHeading,
fontWeight: fontWeightBold,
fontSize: toRem(17),
letterSpacing: toEm(0.2, 16),
lineHeight: 24 / 17,
},
subtitle2: {
...fontStyleHeading,
fontWeight: fontWeightBold,
fontSize: toRem(15),
letterSpacing: toEm(0.25, 14),
lineHeight: 20 / 15,
},
body1: {
...fontStyleBody,
fontSize: toRem(16),
letterSpacing: toEm(0.5, 16),
lineHeight: 24 / 16,
},
body2: {
...fontStyleBody,
fontSize: toRem(14),
letterSpacing: toEm(0.25, 14),
lineHeight: 20 / 14,
},
button: {
...fontStyleBody,
fontWeight: fontWeightMedium,
fontSize: toRem(14),
letterSpacing: toEm(0.25, 14),
lineHeight: 20 / 14,
textTransform: "none",
},
caption: {
...fontStyleBody,
fontSize: toRem(12),
letterSpacing: toEm(0.4, 12),
lineHeight: 20 / 12,
},
overline: {
...fontStyleBody,
fontSize: toRem(12),
letterSpacing: toEm(1.5, 12),
lineHeight: 20 / 12,
},
},
components: {
MuiTypography: {
defaultProps: { variant: "body2" },
},
MuiLink: {
styleOverrides: {
root: { textUnderlineOffset: toRem(1) },
},
},
},
};
};

View File

@@ -17,8 +17,8 @@
"newLine": "lf",
"noEmit": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"jsx": "react-jsx",
"baseUrl": "src"
},

269
yarn.lock
View File

@@ -2702,6 +2702,21 @@
resolved "https://registry.yarnpkg.com/@mdi/js/-/js-6.6.96.tgz#119f79fa9327359421167a7d4b8bde26e84702ce"
integrity sha512-ke9PN5DjPCOlMfhioxeZYADz8Yiz6v47W0IYRza01SSJD7y1EwESVpwFnnFUso+eCoWtE1CO9cTIvQF6sEreuA==
"@microsoft/tsdoc-config@0.16.1":
version "0.16.1"
resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.16.1.tgz#4de11976c1202854c4618f364bf499b4be33e657"
integrity sha512-2RqkwiD4uN6MLnHFljqBlZIXlt/SaUT6cuogU1w2ARw4nKuuppSmR0+s+NC+7kXBQykd9zzu0P4HtBpZT5zBpQ==
dependencies:
"@microsoft/tsdoc" "0.14.1"
ajv "~6.12.6"
jju "~1.4.0"
resolve "~1.19.0"
"@microsoft/tsdoc@0.14.1":
version "0.14.1"
resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.14.1.tgz#155ef21065427901994e765da8a0ba0eaae8b8bd"
integrity sha512-6Wci+Tp3CgPt/B9B0a3J4s3yMgLNSku6w5TV6mN+61C71UqsRBv2FUibBf3tPGlNxebgPHMEUzKpb1ggE8KCKw==
"@mui/base@5.0.0-alpha.75":
version "5.0.0-alpha.75"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.75.tgz#0b352ff951765dfa83723fd1faf46a128dfe63ca"
@@ -2974,6 +2989,11 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"
"@rowy/multiselect@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@rowy/multiselect/-/multiselect-0.2.3.tgz#caf5ee769a6c3ce5a4124120d38820caf1f411a1"
integrity sha512-FiESN3VE2Rz++y6cZXzkA4RJmRZ0r3/1Hy6oAfJtroJ/H/r0xjJYGA5qwpmQKNI08W0KCfkBC7JM0OtRiD3GVA==
"@rushstack/eslint-patch@^1.1.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.1.tgz#782fa5da44c4f38ae9fd38e9184b54e451936118"
@@ -3231,6 +3251,14 @@
lodash "^4.17.15"
redent "^3.0.0"
"@testing-library/react-hooks@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.0.tgz#7d0164bffce4647f506039de0a97f6fcbd20f4bf"
integrity sha512-uZqcgtcUUtw7Z9N32W13qQhVAD+Xki2hxbTR461MKax8T6Jr8nsUvZB+vcBTkzY2nFvsUet434CsgF0ncW2yFw==
dependencies:
"@babel/runtime" "^7.12.5"
react-error-boundary "^3.1.0"
"@testing-library/react@^13.0.0":
version "13.0.0"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.0.0.tgz#8cdaf4667c6c2b082eb0513731551e9db784e8bc"
@@ -3498,7 +3526,14 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/lodash@^4.14.181":
"@types/lodash-es@^4.17.6":
version "4.17.6"
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.6.tgz#c2ed4c8320ffa6f11b43eb89e9eaeec65966a0a0"
integrity sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==
dependencies:
"@types/lodash" "*"
"@types/lodash@*", "@types/lodash@^4.14.181":
version "4.14.181"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.181.tgz#d1d3740c379fda17ab175165ba04e2d03389385d"
integrity sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==
@@ -3563,6 +3598,13 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-div-100vh@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@types/react-div-100vh/-/react-div-100vh-0.4.0.tgz#750e3ac45ee239ec2952089c1516f3b510bd103e"
integrity sha512-mkgDNAqtgoKnTu58DBKT+/AbVxEN1cqc+pNqTKvwPduxgE9WiAVKs4k1wGJ95oJzWX/apnwGM99A560B6c01xw==
dependencies:
react-div-100vh "*"
"@types/react-dom@*", "@types/react-dom@^18.0.0":
version "18.0.0"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.0.tgz#b13f8d098e4b0c45df4f1ed123833143b0c71141"
@@ -3601,19 +3643,10 @@
dependencies:
"@types/react" "*"
"@types/react@*":
version "17.0.19"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.19.tgz#8f2a85e8180a43b57966b237d26a29481dacc991"
integrity sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/react@^18.0.0":
version "18.0.0"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.0.tgz#4be8aa3a2d04afc3ac2cc1ca43d39b0bd412890c"
integrity sha512-7+K7zEQYu7NzOwQGLR91KwWXXDzmTFODRVizJyIALf6RfLv2GDpqpknX64pvRVILXCpXi7O/pua8NGk44dLvJw==
"@types/react@*", "@types/react@^18", "@types/react@^18.0.5":
version "18.0.5"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.5.tgz#1a4d4b705ae6af5aed369dec22800b20f89f5301"
integrity sha512-UPxNGInDCIKlfqBrm8LDXYWNfLHwIdisWcsH5GpMyGjhEDLFgTtlRBaoWuCua9HcyuE0rMkmAeZ3FXV1pYLIYQ==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@@ -4034,7 +4067,7 @@ ajv-keywords@^5.0.0:
dependencies:
fast-deep-equal "^3.1.3"
ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5:
ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@~6.12.6:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -4234,6 +4267,11 @@ async@^2.6.2:
dependencies:
lodash "^4.17.14"
async@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -4538,6 +4576,11 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
btoa@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73"
integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@@ -4609,15 +4652,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001181:
version "1.0.30001191"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001191.tgz#bacb432b6701f690c8c5f7c680166b9a9f0843d9"
integrity sha512-xJJqzyd+7GCJXkcoBiQ1GuxEiOBCLQ0aVW9HMekifZsAVGdj5eJ4mFB9fEhSHipq9IOk/QXFJUiIr9lZT+EsGw==
caniuse-lite@^1.0.30001317:
version "1.0.30001325"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001325.tgz#2b4ad19b77aa36f61f2eaf72e636d7481d55e606"
integrity sha512-sB1bZHjseSjDtijV1Hb7PB2Zd58Kyx+n/9EotvZ4Qcz2K3d0lWB8dB4nb8wN/TsOGFq3UuAm0zQZNQ4SoR7TrQ==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001317:
version "1.0.30001328"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001328.tgz"
integrity sha512-Ue55jHkR/s4r00FLNiX+hGMMuwml/QGqqzVeMQ5thUewznU2EdULFvI3JR7JJid6OrjJNfFvHY2G2dIjmRaDDQ==
case-sensitive-paths-webpack-plugin@^2.4.0:
version "2.4.0"
@@ -5576,6 +5614,13 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
ejs@^3.1.5:
version "3.1.7"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.7.tgz#c544d9c7f715783dd92f0bddcf73a59e6962d006"
integrity sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw==
dependencies:
jake "^10.8.5"
ejs@^3.1.6:
version "3.1.6"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a"
@@ -5727,7 +5772,7 @@ escalade@^3.1.1:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
escape-html@~1.0.3:
escape-html@^1.0.3, escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
@@ -5852,6 +5897,11 @@ eslint-plugin-jsx-a11y@^6.5.1:
language-tags "^1.0.5"
minimatch "^3.0.4"
eslint-plugin-no-relative-import-paths@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-no-relative-import-paths/-/eslint-plugin-no-relative-import-paths-1.2.0.tgz#ae969752ae910f4a92f7d3f467076e3cb19c6acf"
integrity sha512-UQiGNGU8Jite02FLMgB0J1Cg4D9GwpeDPvJo6x1++eKqqghNGRyFA68WVx6p6KjD9THDaenXnDOj/1YAobQ5Ag==
eslint-plugin-react-hooks@^4.3.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.4.0.tgz#71c39e528764c848d8253e1aa2c7024ed505f6c4"
@@ -5884,6 +5934,14 @@ eslint-plugin-testing-library@^5.0.1:
dependencies:
"@typescript-eslint/utils" "^5.13.0"
eslint-plugin-tsdoc@^0.2.16:
version "0.2.16"
resolved "https://registry.yarnpkg.com/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.16.tgz#a3d31fb9c7955faa3c66a43dd43da7635f1c5e0d"
integrity sha512-F/RWMnyDQuGlg82vQEFHQtGyWi7++XJKdYNn0ulIbyMOFqYIjoJOUdE6olORxgwgLkpJxsCJpJbTHgxJ/ggfXw==
dependencies:
"@microsoft/tsdoc" "0.14.1"
"@microsoft/tsdoc-config" "0.16.1"
eslint-scope@5.1.1, eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
@@ -6280,7 +6338,7 @@ firebase@^9.6.10:
"@firebase/storage-compat" "0.1.12"
"@firebase/util" "1.5.1"
firebaseui@^6.0.0, firebaseui@^6.0.1:
firebaseui@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/firebaseui/-/firebaseui-6.0.1.tgz#bdabc69de9c245975a1277d507b2f0ff0044e558"
integrity sha512-SMNCFt/xns3mnvd0hImEDu7di5fqRU3MVyIaXpVEfg6v0bH6f3m+YybivU7KElRUT/47DHMn++D8MrZYYnoN5g==
@@ -6844,6 +6902,13 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"
invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
dependencies:
loose-envify "^1.0.0"
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
@@ -6886,6 +6951,13 @@ is-callable@^1.1.4, is-callable@^1.2.2, is-callable@^1.2.4:
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
is-core-module@^2.1.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
dependencies:
has "^1.0.3"
is-core-module@^2.2.0, is-core-module@^2.8.1:
version "2.8.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
@@ -7042,7 +7114,7 @@ is-weakref@^1.0.2:
dependencies:
call-bind "^1.0.2"
is-wsl@^2.2.0:
is-wsl@^2.1.1, is-wsl@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
@@ -7116,6 +7188,16 @@ jake@^10.6.1:
filelist "^1.0.1"
minimatch "^3.0.4"
jake@^10.8.5:
version "10.8.5"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46"
integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==
dependencies:
async "^3.2.3"
chalk "^4.0.2"
filelist "^1.0.1"
minimatch "^3.0.4"
jest-changed-files@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5"
@@ -7558,10 +7640,15 @@ jest@^27.4.3:
import-local "^3.0.2"
jest-cli "^27.5.1"
jotai@^1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.6.2.tgz#101193b24701ce0621eb017f8dc3d35be3f14da0"
integrity sha512-WglyuXX67f8QPCOX5tDR0N9GhasOV1+IQ3piaF4owr2wBn2CV51T8zbw01ZCme/24JKJilFe6+aA29PJx+jsQw==
jju@~1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a"
integrity sha1-o6vicYryQaKykE+EpiWXDzia4yo=
jotai@^1.6.4:
version "1.6.4"
resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.6.4.tgz#4d9904362c53c4293d32e21fb358d3de34b82912"
integrity sha512-XC0ExLhdE6FEBdIjKTe6kMlHaAUV/QiwN7vZond76gNr/WdcdonJOEW79+5t8u38sR41bJXi26B1dRi7cCRz9A==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
@@ -7935,6 +8022,11 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
@@ -7985,7 +8077,7 @@ long@^4.0.0:
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
loose-envify@^1.1.0, loose-envify@^1.4.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -8180,6 +8272,13 @@ minimist@^1.2.0, minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mkdirp@^0.5.1:
version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
dependencies:
minimist "^1.2.6"
mkdirp@^0.5.5, mkdirp@~0.5.1:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
@@ -8431,6 +8530,14 @@ onetime@^5.1.0, onetime@^5.1.2:
dependencies:
mimic-fn "^2.1.0"
open@^7.3.1:
version "7.4.2"
resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
dependencies:
is-docker "^2.0.0"
is-wsl "^2.1.1"
open@^8.0.9, open@^8.4.0:
version "8.4.0"
resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8"
@@ -9468,6 +9575,11 @@ react-dev-utils@^12.0.0:
strip-ansi "^6.0.1"
text-table "^0.2.0"
react-div-100vh@*, react-div-100vh@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/react-div-100vh/-/react-div-100vh-0.7.0.tgz#b3bec03a833fa40e406f36ed2e23a35a59d1068f"
integrity sha512-I3d77tQyaSlOx/6vurDDLk7upb5GA2ldEtVXkk7Kn5cy+tLlS0KlqDK14xUxlxh7jz4StjgKcwFyrpymsPpomA==
react-dom@^18.0.0:
version "18.0.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.0.0.tgz#26b88534f8f1dbb80853e1eabe752f24100d8023"
@@ -9476,11 +9588,23 @@ react-dom@^18.0.0:
loose-envify "^1.1.0"
scheduler "^0.21.0"
react-error-boundary@^3.1.0, react-error-boundary@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0"
integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==
dependencies:
"@babel/runtime" "^7.12.5"
react-error-overlay@^6.0.10:
version "6.0.10"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==
react-fast-compare@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-firebaseui@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/react-firebaseui/-/react-firebaseui-6.0.0.tgz#97a075e7caa2fb969c8ded9efbbcefccb1f33ce1"
@@ -9488,6 +9612,17 @@ react-firebaseui@^6.0.0:
dependencies:
firebaseui "^6.0.0"
react-helmet-async@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.3.0.tgz#7bd5bf8c5c69ea9f02f6083f14ce33ef545c222e"
integrity sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==
dependencies:
"@babel/runtime" "^7.12.5"
invariant "^2.2.4"
prop-types "^15.7.2"
react-fast-compare "^3.2.0"
shallowequal "^1.1.0"
react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -9595,14 +9730,6 @@ react@^18.0.0:
dependencies:
loose-envify "^1.1.0"
reactfire@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/reactfire/-/reactfire-4.2.1.tgz#48bbd2408247e930658bff58d94863b759381d9a"
integrity sha512-wWATeAPnGmp44rkG+0F2788HnVNgsk0H1vYimeNnNa/RouzkOn58vJmYgKyQhWAOmZl9Smkx9bDsFlt3PevN0w==
dependencies:
rxfire "^6.0.2"
rxjs "^6.6.3 || ^7.0.1"
readable-stream@^2.0.1, readable-stream@~2.3.6:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
@@ -9809,6 +9936,14 @@ resolve@^2.0.0-next.3:
is-core-module "^2.2.0"
path-parse "^1.0.6"
resolve@~1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==
dependencies:
is-core-module "^2.1.0"
path-parse "^1.0.6"
restore-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
@@ -9844,6 +9979,13 @@ rimraf@^3.0.0, rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
rimraf@~2.6.2:
version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
dependencies:
glob "^7.1.3"
rollup-plugin-terser@^7.0.0:
version "7.0.2"
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d"
@@ -9868,20 +10010,6 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
rxfire@^6.0.2:
version "6.0.3"
resolved "https://registry.yarnpkg.com/rxfire/-/rxfire-6.0.3.tgz#556a08e5276563c867daeb8c670f59363d212365"
integrity sha512-77nkyffHh7jgfi1YA/N9RI+kWxYpgKk6GRML1lyersvaqbJt4hkvWwk1rWib9Rb5Lr5mT+Ha45lu7nM79sJCZA==
dependencies:
tslib "^1.9.0 || ~2.1.0"
"rxjs@^6.6.3 || ^7.0.1":
version "7.3.0"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.3.0.tgz#39fe4f3461dc1e50be1475b2b85a0a88c1e938c6"
integrity sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw==
dependencies:
tslib "~2.1.0"
rxjs@^7.5.5:
version "7.5.5"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f"
@@ -10114,6 +10242,11 @@ setprototypeof@1.2.0:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
shallowequal@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -10200,6 +10333,24 @@ source-list-map@^2.0.0, source-list-map@^2.0.1:
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
source-map-explorer@^2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/source-map-explorer/-/source-map-explorer-2.5.2.tgz#857cab5dd9d1d7175e9c5c2739dc9ccfb99f2dc5"
integrity sha512-gBwOyCcHPHcdLbgw6Y6kgoH1uLKL6hN3zz0xJcNI2lpnElZliIlmSYAjUVwAWnc7+HscoTyh1ScR7ITtFuEnxg==
dependencies:
btoa "^1.2.1"
chalk "^4.1.0"
convert-source-map "^1.7.0"
ejs "^3.1.5"
escape-html "^1.0.3"
glob "^7.1.6"
gzip-size "^6.0.0"
lodash "^4.17.20"
open "^7.3.1"
source-map "^0.7.3"
temp "^0.9.4"
yargs "^16.2.0"
source-map-js@^1.0.1, source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
@@ -10629,6 +10780,14 @@ temp-dir@^2.0.0:
resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e"
integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==
temp@^0.9.4:
version "0.9.4"
resolved "https://registry.yarnpkg.com/temp/-/temp-0.9.4.tgz#cd20a8580cb63635d0e4e9d4bd989d44286e7620"
integrity sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==
dependencies:
mkdirp "^0.5.1"
rimraf "~2.6.2"
tempy@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.6.0.tgz#65e2c35abc06f1124a97f387b08303442bde59f3"
@@ -10805,7 +10964,7 @@ tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
"tslib@^1.9.0 || ~2.1.0", tslib@^2.0.3, tslib@~2.1.0:
tslib@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==