Implement figma styles #24

Closed
gilbert wants to merge 1 commits from snowball/testnet-onboarding-app:styling into main
22 changed files with 689 additions and 467 deletions

6
.prettierrc.cjs Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
trailingComma: 'all',
printWidth: 100,
tabWidth: 2,
arrowParens: 'always',
}

View File

@ -31,7 +31,8 @@
"resolve": "^1.20.0",
"semver": "^7.3.5",
"stream-browserify": "^3.0.0",
"tailwindcss": "^3.0.2",
"tailwind-variants": "^0.2.1",
"tailwindcss": "^3.4.8",
"typescript": "^4.9.5"
},
"scripts": {

10
public/img/logo.svg Normal file
View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="116" height="20" viewBox="0 0 116 20" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.41155 10.5194C5.76482 8.19185 7.22144 4.97748 7.22079 1.42853C7.22166 0.94681 7.19477 0.470456 7.14132 0L0 0.000643078L0.000216722 13.5723C-0.0004338 15.2174 0.633716 16.863 1.90213 18.1175C3.17064 19.3721 4.83555 20.0001 6.49904 19.9993L6.49861 19.9997L20.2204 20L20.2199 12.9355C19.7453 12.8838 19.2637 12.857 18.7756 12.8569C15.1884 12.8574 11.9385 14.298 9.58522 16.6255C7.87283 18.2768 5.12731 18.2771 3.43606 16.6043C1.74589 14.9325 1.74513 12.2161 3.41155 10.5194ZM18.7392 1.46863C16.767 -0.481929 13.5629 -0.48268 11.5901 1.46863C9.61732 3.41984 9.61808 6.58895 11.5901 8.53941C13.5633 10.491 16.7663 10.4907 18.7392 8.53941C20.712 6.5882 20.7123 3.42016 18.7392 1.46863Z" fill="#0F0F0F"/>
<path d="M31.8223 18.5836H39.6891V16.33H34.4518V1.41333H31.8223V18.5836Z" fill="#0F0F0F"/>
<path d="M50.3602 1.41333H45.9994L41.4414 18.5836H44.1586L45.2981 14.2911H50.9299L52.0694 18.5836H54.9182L50.3602 1.41333ZM45.846 12.1448L48.125 3.25912H48.2126L50.404 12.1448H45.846Z" fill="#0F0F0F"/>
<path d="M63.6228 8.06818H66.6907C66.6907 3.17467 65.091 1.07129 61.3657 1.07129C57.4432 1.07129 55.7559 3.73274 55.7559 9.97842C55.7559 16.2456 57.4432 18.9284 61.3657 18.9284C65.091 18.9284 66.6907 16.8894 66.7126 12.1461H63.6447C63.6228 15.8593 63.1626 16.7822 61.3657 16.7822C59.3059 16.7822 58.8018 15.43 58.8237 9.97842C58.8237 4.54829 59.3278 3.19611 61.3657 3.21756C63.1626 3.21756 63.6228 4.18346 63.6228 8.06818Z" fill="#0F0F0F"/>
<path d="M74.5924 1.07142C78.5807 1.09297 80.2899 3.77576 80.2899 10C80.2899 16.2242 78.5807 18.9071 74.5924 18.9286C70.5823 18.95 68.873 16.2671 68.873 10C68.873 3.73286 70.5823 1.04997 74.5924 1.07142ZM71.9409 10C71.9409 15.4301 72.4669 16.7823 74.5924 16.7823C76.6961 16.7823 77.2221 15.4301 77.2221 10C77.2221 4.54841 76.6961 3.19624 74.5924 3.2178C72.4669 3.23924 71.9409 4.59142 71.9409 10Z" fill="#0F0F0F"/>
<path d="M86.0202 18.5622L83.3906 18.5836V1.41333H88.0144L92.3314 15.4071H92.3752V1.41333H95.0043V18.5836H90.666L86.0642 3.51671H86.0202V18.5622Z" fill="#0F0F0F"/>
<path d="M101.576 1.41333H98.9473V18.5836H101.576V1.41333Z" fill="#0F0F0F"/>
<path d="M112.365 8.06818H115.433C115.433 3.17467 113.833 1.07129 110.108 1.07129C106.186 1.07129 104.498 3.73274 104.498 9.97842C104.498 16.2456 106.186 18.9284 110.108 18.9284C113.833 18.9284 115.433 16.8894 115.455 12.1461H112.387C112.365 15.8593 111.905 16.7822 110.108 16.7822C108.048 16.7822 107.545 15.43 107.566 9.97842C107.566 4.54829 108.07 3.19611 110.108 3.21756C111.905 3.21756 112.365 4.18346 112.365 8.06818Z" fill="#0F0F0F"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

5
public/img/logomark.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Logo">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M2.6996 8.91554C4.56166 7.05349 5.71417 4.482 5.71371 1.6428C5.71446 1.25743 5.69309 0.876343 5.6508 0.5L0 0.500514L0.000228446 11.3578C-0.00028584 12.6739 0.501486 13.9904 1.5052 14.9941C2.50886 15.9977 3.82634 16.5001 5.14257 16.4994L5.14229 16.4997L16 16.5L15.9997 10.8485C15.6241 10.807 15.243 10.7857 14.8568 10.7856C12.0183 10.7859 9.44674 11.9385 7.58469 13.8005C6.22971 15.1214 4.0572 15.1217 2.71897 13.7835C1.38149 12.4461 1.38097 10.2729 2.6996 8.91554ZM14.8279 1.67491C13.2675 0.114457 10.7321 0.113886 9.17109 1.67491C7.61006 3.23589 7.61063 5.77114 9.17109 7.33154C10.7324 8.8928 13.2669 8.89257 14.8279 7.33154C16.389 5.77057 16.3893 3.23611 14.8279 1.67491Z" fill="#0F0F0F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 875 B

View File

@ -5,10 +5,7 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Testnet Onboarding App"
/>
<meta name="description" content="Testnet Onboarding App" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
@ -24,11 +21,24 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"
rel="stylesheet"
/>
<link
crossorigin="anonymous"
rel="preload"
href="https://laconic.com/fonts/tt-hoves/TTHoves-Regular.woff2"
as="font"
/>
<title>Testnet Onboarding App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="root" class="flex flex-col h-screen"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

View File

@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -22,14 +22,8 @@ function App() {
<Route path="/connect-wallet" element={<ConnectWallet />} />
<Route element={<SignPageLayout />}>
<Route path="/sign-with-nitro-key" element={<SignWithNitroKey />} />
<Route
path="/user-verification"
element={<UserVerification />}
/>
<Route
path="/sign-with-cosmos"
element={<SignWithCosmos />}
/>
<Route path="/user-verification" element={<UserVerification />} />
<Route path="/sign-with-cosmos" element={<SignWithCosmos />} />
<Route
path="/onboarding-success"
element={<OnboardingSuccess />}

View File

@ -1,32 +1,25 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import React from "react";
import { Link, useLocation } from "react-router-dom";
import { AppBar, Toolbar, Avatar, Box, IconButton } from '@mui/material';
import { BodyText } from "./ui/BodyText";
const Header: React.FC = () => {
const location = useLocation()
const location = useLocation();
return (
<AppBar position="static" color="inherit">
<Toolbar>
<Link to={location.pathname === "/" ? "/" : "/connect-wallet"} style={{ color: "inherit", textDecoration: "none" }}>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Avatar
alt="Laconic logo"
src="https://avatars.githubusercontent.com/u/92608123"
/>
<IconButton
edge="start"
color="inherit"
aria-label="menu"
sx={{ ml: 2, mr: 2 }}
>
Testnet Onboarding
</IconButton>
</Box>
</Link>
</Toolbar>
</AppBar>
<div className="px-6 py-2.5 bg-neutral-10">
<Link
to={location.pathname === "/" ? "/" : "/connect-wallet"}
style={{ color: "inherit", textDecoration: "none" }}
>
<div className="flex items-center gap-3">
<img alt="Laconic logomark" className="h-5 md:hidden" src="/img/logomark.svg" />
<img alt="Laconic logo" className="h-5 hidden md:block" src="/img/logo.svg" />
<div className="border-l border-black h-5"></div>
<BodyText>Testnet Onboarding</BodyText>
</div>
</Link>
</div>
);
};

View File

@ -1,82 +1,101 @@
import React, { useState } from 'react';
import React, { useState } from "react";
import { Typography, Button, Box, Paper, Radio, RadioGroup, FormControlLabel, FormControl, FormLabel, Link, Checkbox } from '@mui/material';
import TermsAndConditionsDialog from './TermsAndConditionsDialog';
import TermsAndConditionsDialog from "./TermsAndConditionsDialog";
import { Card } from "./ui/Card";
import { Button } from "./ui/Button";
export enum Role {
Validator = 'validator',
Participant = 'participant'
Validator = "validator",
Participant = "participant",
}
const TermsAndConditionsCard = ({ handleAccept, handleRoleChange }: { handleAccept: () => void, handleRoleChange: (role: Role) => void }) => {
type Props = {
className?: string;
handleAccept: () => void;
handleRoleChange: (role: Role) => void;
};
const TermsAndConditionsCard = ({ className, handleAccept, handleRoleChange }: Props) => {
const [selectedRole, setSelectedRole] = useState<Role>(Role.Participant);
const [checked, setChecked] = useState(false);
const [isHidden, setIsHidden] = useState(false);
const [isDialogOpen, setisDialogOpen] = useState(false)
const [isDialogOpen, setisDialogOpen] = useState(false);
const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setChecked(event.target.checked);
};
const handleContinue = () => {
handleAccept()
setIsHidden(true)
}
handleAccept();
};
const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedRole(event.target.value as Role);
handleRoleChange((event.target as HTMLInputElement).value === Role.Validator ? Role.Validator : Role.Participant);
handleRoleChange(
(event.target as HTMLInputElement).value === Role.Validator
? Role.Validator
: Role.Participant,
);
setChecked(false);
};
return (
<Paper elevation={3} sx={{ padding: 2, marginTop: 2, display: isHidden ? "none" : "block", marginBottom: 3 }} >
<FormControl component="fieldset">
<FormLabel component="legend">Select your role</FormLabel>
<RadioGroup
aria-label="roles"
name="roles"
value={selectedRole}
onChange={handleRadioChange}
>
<FormControlLabel
value={Role.Validator}
control={<Radio />}
label="Validator"
/>
<FormControlLabel
value={Role.Participant}
control={<Radio />}
label="Participant"
/>
</RadioGroup>
</FormControl>
<Box mt={2} display="flex" alignItems="center">
<Checkbox
<Card className={`body-2 ${className || ""}`}>
<div>
<label className="block text-sm">Select your role</label>
<fieldset className="mt-2 flex flex-col">
<label className="inline-flex items-center">
<input
type="radio"
className="h-4 w-4 border-gray-300 text-primary-60 focus:primary-30"
value={Role.Validator}
checked={selectedRole === Role.Validator}
onChange={handleRadioChange}
/>
<span className="ml-2">Validator</span>
</label>
<label className="inline-flex items-center">
<input
type="radio"
className="h-4 w-4 border-gray-300 text-primary-60 focus:primary-30"
value={Role.Participant}
checked={selectedRole === Role.Participant}
onChange={handleRadioChange}
/>
<span className="ml-2">Participant</span>
</label>
</fieldset>
</div>
<fieldset className="flex items-center">
<input
id="toc-check"
type="checkbox"
className="form-checkbox"
checked={checked}
onChange={handleCheckboxChange}
color="primary"
/>
<Typography variant="body1">
I accept the <Link onClick={() => setisDialogOpen(true)} target="_blank" rel="noopener">
terms and conditions
</Link>
</Typography>
</Box>
<Box mt={4} display="flex" justifyContent="end">
<Button
variant="contained"
color="primary"
onClick={handleContinue}
disabled={!checked}
>
Continue
</Button>
</Box>
<TermsAndConditionsDialog isValidator = { selectedRole === Role.Validator } open={isDialogOpen} onClose={() => setisDialogOpen(false)}/>
</Paper>
<label htmlFor="toc-check" className="ml-2">
I accept the{" "}
<button
className="underline"
onClick={() => {
setisDialogOpen(true);
}}
rel="noopener noreferrer"
>
Terms &amp; Conditions
</button>
</label>
</fieldset>
<Button size="lg" className={`py-2 md:self-end`} onClick={handleContinue} disabled={!checked}>
Continue
</Button>
<TermsAndConditionsDialog
isValidator={selectedRole === Role.Validator}
open={isDialogOpen}
onClose={() => setisDialogOpen(false)}
/>
</Card>
);
};

View File

@ -1,8 +1,15 @@
import React from 'react';
import React from "react";
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button, Typography } from '@mui/material';
import {
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from "@mui/material";
import { TNC_PARTICIPANT_CONTENT, TNC_VALIDATOR_CONTENT } from '../constants';
import { TNC_PARTICIPANT_CONTENT, TNC_VALIDATOR_CONTENT } from "../constants";
import { Button } from "./ui/Button";
interface TermsDialogProps {
isValidator: boolean;
@ -13,17 +20,22 @@ interface TermsDialogProps {
const TermsAndConditionsDialog: React.FC<TermsDialogProps> = ({ isValidator, open, onClose }) => {
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Terms and Conditions</DialogTitle>
<DialogTitle>
<h3>Terms and Conditions</h3>
</DialogTitle>
<DialogContent>
<Typography variant="h6" gutterBottom>
Onboard as a {isValidator ? "validator" : "participant"}
</Typography>
<h5 className="mb-4">Onboard as a {isValidator ? "validator" : "participant"}</h5>
<DialogContentText>
<div dangerouslySetInnerHTML={{__html: isValidator ? TNC_VALIDATOR_CONTENT : TNC_PARTICIPANT_CONTENT }} />
<div
dangerouslySetInnerHTML={{
__html: isValidator ? TNC_VALIDATOR_CONTENT : TNC_PARTICIPANT_CONTENT,
}}
/>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
<Button onClick={onClose} className="px-3 py-1">
Close
</Button>
</DialogActions>
@ -31,4 +43,4 @@ const TermsAndConditionsDialog: React.FC<TermsDialogProps> = ({ isValidator, ope
);
};
export default TermsAndConditionsDialog;
export default TermsAndConditionsDialog;

View File

@ -0,0 +1,10 @@
import React from "react";
type Props = {
sizing?: 1 | 2 | 3;
className?: string;
};
export const BodyText = ({ sizing = 1, className, children }: React.PropsWithChildren<Props>) => {
return <div className={`body-${sizing} ${className}`}>{children}</div>;
};

View File

@ -0,0 +1,100 @@
import type { ComponentPropsWithoutRef } from "react";
import React, { forwardRef } from "react";
import { tv, VariantProps } from "tailwind-variants";
type Props = {
loading?: boolean;
className?: string;
} & ButtonTheme &
ComponentPropsWithoutRef<"button">;
export const Button = forwardRef<HTMLButtonElement, Props>(
({ loading, className, children, variant = "primary", size = "md", ...props }, ref) => {
return (
<button {...props} ref={ref} className={buttonTheme({ variant, size, className })}>
<span className={loading ? "invisible" : ""}>{children}</span>
{loading && (
<svg
aria-hidden="true"
className="w-4 h-4 text-primary-40 animate-spin fill-primary-20 absolute top-1/2 left-1/2 -mt-2 -ml-1.5"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
)}
</button>
);
},
);
Button.displayName = "Button";
/**
* Defines the theme for a button component.
*/
export const buttonTheme = tv({
base: [
"relative",
"h-fit",
"inline-flex",
"items-center",
"justify-center",
"whitespace-nowrap",
"focus-ring",
"disabled:cursor-not-allowed",
"transition-colors",
"duration-150",
"font-mono",
"rounded",
"antialiased",
"uppercase",
],
variants: {
size: {
lg: ["px-6", "py-2.5", "text-[18px]"],
md: ["px-4", "py-2", "text-[16px]"],
sm: ["px-3", "py-1", "text-[16px]"],
},
variant: {
primary: [
"text-cream",
"bg-primary-60",
"hover:bg-primary-50",
"active:bg-primary-70",
"disabled:bg-neutral-40",
"disabled:text-neutral-70",
],
success: [
"text-cream",
"bg-success-40",
"pointer-events-none", // Disable button; success state is not actionable
],
"danger-outline": [
"text-danger-50",
"border",
"border-danger-50",
"hover:border-danger-40",
"hover:text-cream",
"hover:bg-danger-40",
"active:bg-danger-60",
"active:border-danger-60",
],
unstyled: [],
},
},
defaultVariants: {
size: "md",
variant: "primary",
},
});
type ButtonTheme = VariantProps<typeof buttonTheme>;

View File

@ -0,0 +1,46 @@
import React, { ComponentPropsWithoutRef, forwardRef } from "react";
import { tv, VariantProps } from "tailwind-variants";
type Props = {} & Theme & ComponentPropsWithoutRef<"div">;
export const Card = forwardRef<HTMLButtonElement, Props>(
({ children, className, type = "generic", ...props }) => {
return (
<div {...props} className={"Card " + theme({ type, className })}>
{children}
</div>
);
},
);
Card.displayName = "Card";
type Theme = VariantProps<typeof theme>;
export const theme = tv({
base: [],
variants: {
type: {
page: [
"max-w-full",
"md:max-w-[752px]",
"mx-auto",
"p-4",
"md:p-8",
"flex",
"flex-col",
"gap-4",
"md:gap-8",
],
generic: [
"px-4",
"md:px-6",
"py-6",
"md:py-8",
"flex",
"flex-col",
"gap-6",
"md:gap-8",
"bg-neutral-10",
],
},
},
});

View File

@ -1,13 +1,49 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
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;
font-weight: 300;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
h1, h2, h3, h4, h5, h6 {
letter-spacing: -0.01em;
}
h1, h2, h3, h4 {
line-height: 1.2;
}
h5, h6 {
line-height: 1.3;
}
h1 { font-size: 36px; @apply md:text-[48px]; }
h2 { font-size: 32px; @apply md:text-[40px]; }
h3 { font-size: 24px; @apply md:text-[32px]; }
h4 { font-size: 20px; @apply md:text-[28px]; }
h5 { font-size: 18px; @apply md:text-[24px]; }
h6 { @apply md:text-[20px]; }
.body-1 { font-size: 16px; @apply md:text-[18px]; line-height: 1.5; }
.body-2 { font-size: 14px; @apply md:text-[16px]; line-height: 1.5; }
.body-3 { font-size: 14px; @apply md:text-[14px]; line-height: 1.5; }
.font-mono {
letter-spacing: 0.01em;
}
/* Global helpers */
.flex-center {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
/* Fonts */
@font-face {
font-family: 'TTHoves';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('https://laconic.com/fonts/tt-hoves/TTHoves-Regular.woff2') format('woff2');
}

View File

@ -1,15 +1,9 @@
import React from "react";
import { Outlet, useNavigate } from "react-router-dom";
import {
Toolbar,
Avatar,
Button,
Typography,
Container
} from "@mui/material";
import { useWalletConnectContext } from "../context/WalletConnectContext";
import { Button } from "../components/ui/Button";
import { Card } from "../components/ui/Card";
const SignPageLayout = () => {
const { disconnect, session } = useWalletConnectContext();
@ -22,52 +16,32 @@ const SignPageLayout = () => {
return (
<>
<Toolbar variant="dense">
<Button
variant="outlined"
style={{
marginLeft: "auto",
}}
color="error"
onClick={disconnectHandler}
>
Disconnect
</Button>
</Toolbar>
<Container maxWidth="md">
<Card type="page">
{session && (
<div style={{ display: "flex", flexDirection: "column" }}>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "flex-end",
}}
>
<Typography variant="body2">
Connected to: <b> {session.peer.metadata.name}</b>{" "}
</Typography>
<Avatar
variant="square"
alt="Peer logo"
src={session.peer.metadata.icons[0]}
sx={{
width: 20,
height: 20,
marginLeft: 1,
paddingBottom: 0.5,
}}
/>
<div className="p-4 flex flex-col md:flex-row md:items-center gap-4 bg-neutral-10 body-3">
<div className="min-w-0">
<div className="flex flex-row items-end">
<div>
<span className="font-semibold">Connected to:</span> {session.peer.metadata.name}{" "}
</div>
</div>
<div className="mt-1 overflow-ellipsis overflow-hidden whitespace-nowrap">
<span className="font-semibold">Session ID:</span> {session.topic}
</div>
</div>
<Typography variant="body2">
Session ID: <b>{session.topic} </b>
</Typography>
<Button
variant="danger-outline"
size="sm"
color="error"
onClick={disconnectHandler}
className="mr-auto"
>
Disconnect
</Button>
</div>
)}
<Outlet />
</Container>
</Card>
</>
);
};

View File

@ -1,64 +1,37 @@
import React, { useEffect } from "react";
import {useNavigate } from "react-router-dom";
import { Button, Box, Container, Typography, colors } from "@mui/material";
import { useNavigate } from "react-router-dom";
import { useWalletConnectContext } from "../context/WalletConnectContext";
import { WALLET_DISCLAIMER_MSG } from "../constants";
import { Button } from "../components/ui/Button";
import { BodyText } from "../components/ui/BodyText";
const ConnectWallet = () => {
const { connect, session } = useWalletConnectContext();
const navigate = useNavigate();
useEffect(() => {
if (session) {
navigate("/sign-with-nitro-key");
}
}, [session, navigate,]);
}, [session, navigate]);
const handler = async () => {
await connect();
};
return (
<Container maxWidth="lg">
<Box
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
marginTop={10}
sx={{
border: 1,
borderColor: 'grey.500',
}}
padding={5}
>
<Typography variant="h5" component="h1" gutterBottom color={colors.red[400]}>
Disclaimer
</Typography>
<Typography variant="body1">
{WALLET_DISCLAIMER_MSG}
</Typography>
</Box>
<Box
display="flex"
flexDirection="column"
alignItems="center"
padding={5}
justifyContent="center"
>
<Button
variant="contained"
onClick={handler}
>
<div className="p-3 md:mt-8 max-w-md mx-auto">
<div className="p-6 flex-center gap-4 bg-neutral-10">
<h3 className="text-danger-50">Disclaimer</h3>
<BodyText>{WALLET_DISCLAIMER_MSG}</BodyText>
<Button size="lg" onClick={handler} className="w-full md:w-auto">
Connect Wallet
</Button>
</Box>
</Container>
</div>
</div>
);
};

View File

@ -7,8 +7,14 @@ import { Registry } from "@cerc-io/registry-sdk";
import SumsubWebSdk from "@sumsub/websdk-react";
import { MessageHandler } from "@sumsub/websdk";
import { config, fetchAccessToken, getAccessTokenExpirationHandler, options } from "../utils/sumsub";
import {
config,
fetchAccessToken,
getAccessTokenExpirationHandler,
options,
} from "../utils/sumsub";
import { ENABLE_KYC } from "../constants";
import { Card } from "../components/ui/Card";
interface Participant {
cosmosAddress: string;
@ -17,22 +23,20 @@ interface Participant {
kycId: string;
}
const registry = new Registry(
process.env.REACT_APP_REGISTRY_GQL_ENDPOINT!
);
const registry = new Registry(process.env.REACT_APP_REGISTRY_GQL_ENDPOINT!);
const OnboardingSuccess = () => {
const location = useLocation();
const { cosmosAddress } = location.state as {
cosmosAddress?: string
}
cosmosAddress?: string;
};
const [participant, setParticipant] = useState<Participant>();
const [token, setToken] = useState<string>('');
const [token, setToken] = useState<string>("");
const [loading, setLoading] = useState<boolean>(true);
const messageHandler: MessageHandler = (event, payload) => {
console.log('sumsubEvent:', event, payload);
console.log("sumsubEvent:", event, payload);
};
useEffect(() => {
@ -65,7 +69,7 @@ const OnboardingSuccess = () => {
};
if (cosmosAddress && ENABLE_KYC) {
getToken(cosmosAddress).catch(error => {
getToken(cosmosAddress).catch((error) => {
console.error(error);
alert("Failed to fetch token");
});
@ -73,38 +77,28 @@ const OnboardingSuccess = () => {
}, [cosmosAddress]);
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
marginTop: 6,
gap: 1,
}}
>
<Typography variant="h5">Transaction Successful</Typography>
<Typography variant="body1">
Participant onboarded: <br />
</Typography>
<Box
sx={{
backgroundColor: "lightgray",
padding: 3,
wordWrap: "break-word",
marginBottom: 6,
}}
>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
{participant && (
<div>
Cosmos Address: {participant.cosmosAddress} <br />
Nitro Address: {participant.nitroAddress} <br />
Role: {participant.role} <br />
KYC ID: {participant.kycId} <br />
<br />
<>
<section>
<h4>Transaction Successful</h4>
<Card className="mt-2">
<div>
<div className="body-2">Participant onboarded:</div>
<div className="mt-2 p-4 break-words bg-cream">
<pre style={{ whiteSpace: "pre-wrap" }}>
{participant && (
<div>
Cosmos Address: {participant.cosmosAddress} <br />
Nitro Address: {participant.nitroAddress} <br />
Role: {participant.role} <br />
KYC ID: {participant.kycId} <br />
<br />
</div>
)}
</pre>
</div>
)}
</pre>
</Box>
</div>
</Card>
</section>
{ENABLE_KYC ? (
<Box>
<Typography variant="h5">KYC Status</Typography>
@ -118,9 +112,10 @@ const OnboardingSuccess = () => {
/>
)}
</Box>
) : ''
}
</Box>
) : (
""
)}
</>
);
};

View File

@ -2,8 +2,6 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { enqueueSnackbar } from "notistack";
import { Box, Card, CardContent, Grid, Typography } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton/LoadingButton";
import {
MsgOnboardParticipantEncodeObject,
typeUrlMsgOnboardParticipant,
@ -11,7 +9,10 @@ import {
import { StargateClient } from "@cosmjs/stargate";
import { useWalletConnectContext } from "../context/WalletConnectContext";
import TermsAndConditionsCard, {Role} from "../components/TermsAndConditionsCard";
import TermsAndConditionsCard, { Role } from "../components/TermsAndConditionsCard";
import { Button } from "../components/ui/Button";
import { BodyText } from "../components/ui/BodyText";
import { Card } from "../components/ui/Card";
const SignWithCosmos = () => {
const { session, signClient } = useWalletConnectContext();
@ -19,7 +20,7 @@ const SignWithCosmos = () => {
const location = useLocation();
const [isLoading, setIsLoading] = useState(false);
const [balance, setBalance] = useState('');
const [isRequesting, setIsRequesting] = useState(false);
const [requestState, setRequestState] = useState<null | "pending" | "success">(null);
const [isTncAccepted, setIsTncAccepted] = useState(false);
const [role, setRole] = useState(Role.Participant);
@ -57,11 +58,11 @@ const SignWithCosmos = () => {
const handleTokenRequest = async () => {
try {
setIsRequesting(true);
setRequestState("pending");
const response = await fetch(`${process.env.REACT_APP_FAUCET_ENDPOINT!}/faucet`, {
method: 'POST',
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify({
address: cosmosAddress,
@ -69,7 +70,8 @@ const SignWithCosmos = () => {
});
if (response.ok) {
enqueueSnackbar('Tokens sent successfully', { variant: "success" });
enqueueSnackbar("Tokens sent successfully", { variant: "success" });
setRequestState("success");
} else {
const errorResponse = await response.json();
if (response.status === 429) {
@ -83,8 +85,7 @@ const SignWithCosmos = () => {
} catch (error) {
console.error(error);
enqueueSnackbar("Error getting tokens from faucet", { variant: "error" });
} finally {
setIsRequesting(false);
setRequestState(null);
}
};
@ -115,8 +116,8 @@ const SignWithCosmos = () => {
} else {
navigate("/onboarding-success", {
state: {
cosmosAddress
}
cosmosAddress,
},
});
}
} catch (error) {
@ -133,7 +134,7 @@ const SignWithCosmos = () => {
const balance = await cosmosClient.getBalance(cosmosAddress!, process.env.REACT_APP_LACONICD_DENOM!);
setBalance(balance.amount);
} catch (error) {
console.error('Error fetching balance:', error);
console.error("Error fetching balance:", error);
throw error;
}
}, [cosmosAddress, createCosmosClient]);
@ -143,69 +144,77 @@ const SignWithCosmos = () => {
}, [getBalances]);
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
marginTop: 6,
gap: 1,
}}
>
<Typography variant="h5" display={`${isTncAccepted ? "none" : "block"}`}>Please accept terms and conditions to continue</Typography>
<TermsAndConditionsCard handleAccept={() => setIsTncAccepted(true)} handleRoleChange={setRole}/>
<Typography variant="h5">Send transaction to chain</Typography>
<Typography>Cosmos Account:</Typography>
<Card className='mt-1 mb-1'>
<CardContent>
<Grid container spacing={2}>
<Grid item xs={9}>
<Typography variant="body1">Address: {cosmosAddress}</Typography>
<Typography variant="body1">Balance: {balance} {process.env.REACT_APP_LACONICD_DENOM}</Typography>
</Grid>
<Grid item xs={3} container justifyContent="flex-end">
<LoadingButton
variant="contained"
onClick={handleTokenRequest}
disabled={isTncAccepted ? isRequesting : !isTncAccepted}
loading={isRequesting}
>
Request tokens from Faucet
</LoadingButton>
</Grid>
</Grid>
</CardContent>
</Card>
<Typography variant="body1">
Onboarding message: <br />
</Typography>
<Box
sx={{
backgroundColor: "lightgray",
padding: 3,
wordWrap: "break-word",
}}
>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
{JSON.stringify(onboardParticipantMsg, null, 2)}{" "}
</pre>
</Box>
<>
{!isTncAccepted && (
<section>
<h4>Please accept terms and conditions to continue</h4>
<TermsAndConditionsCard
className="mt-2"
handleAccept={() => setIsTncAccepted(true)}
handleRoleChange={setRole}
/>
</section>
)}
<Box
sx={{
paddingBottom: 2,
}}>
<LoadingButton
variant="contained"
onClick={async () => {
await sendTransaction(onboardParticipantMsg);
}}
loading={isLoading}
disabled={isTncAccepted ? (balance === '0') : !isTncAccepted}
>
Send transaction
</LoadingButton>
</Box>
</Box>
<section>
<h4>Send transaction to chain</h4>
<Card className="mt-2">
<div className="flex flex-col items-stretch">
<BodyText sizing={3}>Cosmos Account:</BodyText>
<pre
className="mt-2 px-4 py-3 bg-cream font-sans body-3"
style={{
whiteSpace: "pre-wrap",
wordWrap: "break-word",
overflowWrap: "break-word",
}}
>
{`Address: ${cosmosAddress}\nBalance: ${balance || "…"} ${
process.env.REACT_APP_LACONICD_DENOM
}`}
</pre>
<Button
variant={requestState === "success" ? "success" : "primary"}
onClick={handleTokenRequest}
disabled={isTncAccepted ? !!requestState : !isTncAccepted}
loading={requestState === "pending"}
className="mt-6 md:self-start"
>
{requestState === "success" ? "Tokens sent" : "Request tokens from Faucet"}
</Button>
</div>
<div className="border-t border-neutral-30"></div>
<div>
<BodyText sizing={3}>Onboarding message:</BodyText>
<div
className="mt-2 p-4 md:p-6 bg-neutral-30 body-3"
style={{
whiteSpace: "pre-wrap",
wordWrap: "break-word",
overflowWrap: "break-word",
}}
>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
{JSON.stringify(onboardParticipantMsg, null, 2)}{" "}
</pre>
</div>
</div>
<Button
onClick={async () => {
await sendTransaction(onboardParticipantMsg);
}}
loading={isLoading}
disabled={isTncAccepted ? balance === "0" : !isTncAccepted}
className="md:self-start"
>
Send transaction
</Button>
</Card>
</section>
</>
);
};

View File

@ -4,22 +4,16 @@ import { enqueueSnackbar } from "notistack";
import canonicalStringify from "canonical-json";
import { ethers } from "ethers";
import {
Select,
MenuItem,
Box,
Typography,
} from "@mui/material";
import LoadingButton from '@mui/lab/LoadingButton';
import { Select, MenuItem } from "@mui/material";
import { utf8ToHex } from "@walletconnect/encoding";
import { useWalletConnectContext } from "../context/WalletConnectContext";
import { ENABLE_KYC } from "../constants";
import { Button } from "../components/ui/Button";
import { Card } from "../components/ui/Card";
const SignWithNitroKey = () => {
const { session, signClient, checkPersistedState } =
useWalletConnectContext();
const { session, signClient, checkPersistedState } = useWalletConnectContext();
useEffect(() => {
if (signClient && !session) {
@ -45,7 +39,7 @@ const SignWithNitroKey = () => {
const signEth = async () => {
if (session && signClient) {
try {
setIsLoading(true)
setIsLoading(true);
const jsonMessage = canonicalStringify(message);
const hexMsg = utf8ToHex(jsonMessage, true);
const receivedEthSig: string = await signClient!.request({
@ -56,7 +50,7 @@ const SignWithNitroKey = () => {
params: [hexMsg, ethAddress],
},
});
setIsLoading(false)
setIsLoading(false);
setEthSignature(ethSignature);
if (ENABLE_KYC) {
@ -81,7 +75,7 @@ const SignWithNitroKey = () => {
}
} catch (error) {
console.log("err in signing ", error);
setIsLoading(false)
setIsLoading(false);
enqueueSnackbar("Error signing message", { variant: "error" });
}
}
@ -90,69 +84,74 @@ const SignWithNitroKey = () => {
return (
<div>
{session ? (
<Box
sx={{
display: "flex",
flexDirection: "column",
marginTop: 6,
gap: 1,
}}
>
<Typography variant="h5">Sign with Nitro key</Typography>
<Typography variant="body1">Select Laconic account:</Typography>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={cosmosAddress}
onChange={(e: any) => {
setCosmosAddress(e.target.value);
}}
style={{ maxWidth: "600px", display: "block" }}
>
{session?.namespaces.cosmos.accounts.map((address, index) => (
<MenuItem value={address.split(":")[2]} key={index}>
{address.split(":")[2]}
</MenuItem>
))}
</Select>
<Typography variant="body1">Select Nitro account: </Typography>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={ethAddress}
onChange={(e: any) => {
setEthAddress(e.target.value);
}}
style={{ maxWidth: "600px", display: "block" }}
>
{session?.namespaces.eip155.accounts.map((address, index) => (
<MenuItem value={address.split(":")[2]} key={index}>
{address.split(":")[2]}
</MenuItem>
))}
</Select>
<div>
<h4>Sign with Nitro key</h4>
{(Boolean(ethAddress) && Boolean(cosmosAddress)) && (<Box
sx={{
backgroundColor: "lightgray",
padding: 3,
wordWrap: "break-word",
}}
>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>{canonicalStringify(message, null, 2)} </pre>
</Box>)}
<Box>
<LoadingButton
variant="contained"
<Card className="mt-2">
<div className="flex flex-col gap-4">
<div>
<div className="body-3">Select Laconic account:</div>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={cosmosAddress}
onChange={(e: any) => {
setCosmosAddress(e.target.value);
}}
className="mt-2 w-full"
SelectDisplayProps={{
className: "!bg-cream !py-2 !text-[14px] !font-light",
}}
>
{session?.namespaces.cosmos.accounts.map((address, index) => (
<MenuItem value={address.split(":")[2]} key={index}>
{address.split(":")[2]}
</MenuItem>
))}
</Select>
</div>
<div>
<div className="body-3">Select Nitro account:</div>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={ethAddress}
onChange={(e: any) => {
setEthAddress(e.target.value);
}}
className="mt-2 w-full"
SelectDisplayProps={{
className: "!bg-cream !py-2 !text-[14px] !font-light",
}}
>
{session?.namespaces.eip155.accounts.map((address, index) => (
<MenuItem value={address.split(":")[2]} key={index}>
{address.split(":")[2]}
</MenuItem>
))}
</Select>
</div>
</div>
{ethAddress && cosmosAddress && (
<div className="p-4 md:p-6 break-words body-3 bg-neutral-30">
<pre style={{ whiteSpace: "pre-wrap" }}>
{canonicalStringify(message, null, 2)}{" "}
</pre>
</div>
)}
<Button
onClick={signEth}
disabled={!Boolean(ethAddress)}
sx={{ marginTop: 2 }}
loading={isLoading}
className="md:self-start"
>
Sign using Nitro key
</LoadingButton>
</Box>
</Box>
</Button>
</Card>
</div>
) : (
<>Loading...</>
)}

View File

@ -1,47 +1,32 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import React, { useNavigate } from "react-router-dom";
import { Container, Typography, Button, Box, Paper } from '@mui/material';
import { TNC_GENERIC_CONTENT } from '../constants';
import { TNC_GENERIC_CONTENT } from "../constants";
import { BodyText } from "../components/ui/BodyText";
import { Button } from "../components/ui/Button";
const TermsAndConditions = () => {
const navigate = useNavigate();
const handleAccept = () => {
navigate('/connect-wallet');
navigate("/connect-wallet");
};
return (
<Container maxWidth="md">
<Paper elevation={3} style={{ padding: '2rem', marginTop: '2rem', height: '80vh', display: 'flex', flexDirection: 'column' }}>
<Typography variant="h4" gutterBottom>
Terms and Conditions
</Typography>
<Box
style={{
overflowY: 'auto',
flexGrow: 1,
paddingRight: '1rem',
marginBottom: '1rem',
}}
>
<Typography
variant="body1"
gutterBottom
component="div"
>
<div dangerouslySetInnerHTML={{__html: TNC_GENERIC_CONTENT}} />
</Typography>
</Box>
<Box mt={2} display="flex" justifyContent="center">
<Button variant="contained" color="primary" onClick={handleAccept}>
Accept
</Button>
</Box>
</Paper>
</Container>
<div className="flex-1 min-h-0 p-3 md:p-8">
<div className="h-full md:mx-auto max-w-3xl px-4 md:px-6 py-6 md:py-8 flex flex-col gap-6 md:gap-8 bg-neutral-10">
<div className="flex-1 flex flex-col gap-6 min-h-0">
<h4>Terms and Conditions</h4>
<div className="flex-1 overflow-y-auto">
<BodyText sizing={2}>
<div dangerouslySetInnerHTML={{ __html: TNC_GENERIC_CONTENT }} />
</BodyText>
</div>
</div>
<div className="flex justify-center">
<Button onClick={handleAccept}>ACCEPT</Button>
</div>
</div>
</div>
);
};

71
tailwind.config.js Normal file
View File

@ -0,0 +1,71 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.tsx',
'./public/index.html',
],
theme: {
colors: {
cream: '#FBFBFB',
black: '#0F0F0F', // Infinity Black
neutral: {
10: '#F1F1F2',
20: '#E6E6E8',
30: '#DADADE',
40: '#CCCBD0',
50: '#BDBCC3',
60: '#A7A6AF',
70: '#83828F',
80: '#48474F',
90: '#29292E',
100: '#18181A',
},
primary: {
10: '#F2F2FF',
20: '#CACAFF',
30: '#A2A2FF',
40: '#7A7AFF',
50: '#4545FF',
60: '#0000F4',
70: '#0000BE',
80: '#000088',
90: '#000051',
100: '#000036',
},
danger: {
10: '#FFF2F3',
20: '#FFC9CC',
30: '#FFA3A8',
40: '#FF7A81',
50: '#B20710',
60: '#870007',
},
success: {
10: '#F2FFF6',
20: '#C9FFD9',
30: '#7AFFA1',
40: '#24A148',
},
warning: {
10: '#FFFBF2',
20: '#FFEFC9',
30: '#E5AD29',
40: '#A17203',
},
},
fontFamily: {
sans: [`'TT Hoves', sans-serif`, {
fontFeatureSettings: `'liga' off, 'clig' off`,
}],
mono: `"DM Mono", monospace`,
},
extend: {
boxShadow: {
'button-primary': `0px 0px 20px 0px rgba(0, 0, 244, 0.50);`
}
}
},
plugins: [],
}

View File

@ -8763,7 +8763,7 @@ jest@^27.4.3:
import-local "^3.0.2"
jest-cli "^27.5.1"
jiti@^1.19.1, jiti@^1.21.0:
jiti@^1.21.0:
version "1.21.0"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d"
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==
@ -12051,10 +12051,22 @@ system-architecture@^0.1.0:
resolved "https://registry.yarnpkg.com/system-architecture/-/system-architecture-0.1.0.tgz#71012b3ac141427d97c67c56bc7921af6bff122d"
integrity sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==
tailwindcss@^3.0.2:
version "3.4.1"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.1.tgz#f512ca5d1dd4c9503c7d3d28a968f1ad8f5c839d"
integrity sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==
tailwind-merge@^2.2.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.4.0.tgz#1345209dc1f484f15159c9180610130587703042"
integrity sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==
tailwind-variants@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tailwind-variants/-/tailwind-variants-0.2.1.tgz#132f2537b0150819036f6c4f47d5c50b929b758d"
integrity sha512-2xmhAf4UIc3PijOUcJPA1LP4AbxhpcHuHM2C26xM0k81r0maAO6uoUSHl3APmvHZcY5cZCY/bYuJdfFa4eGoaw==
dependencies:
tailwind-merge "^2.2.0"
tailwindcss@^3.4.8:
version "3.4.8"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.8.tgz#74fdfc085732c244ad9ca4ee0d539bc5dddd58fd"
integrity sha512-GkP17r9GQkxgZ9FKHJQEnjJuKBcbFhMFzKu5slmN6NjlCuFnYJMQ8N4AZ6VrUyiRXlDtPKHkesuQ/MS913Nvdg==
dependencies:
"@alloc/quick-lru" "^5.2.0"
arg "^5.0.2"
@ -12064,7 +12076,7 @@ tailwindcss@^3.0.2:
fast-glob "^3.3.0"
glob-parent "^6.0.2"
is-glob "^4.0.3"
jiti "^1.19.1"
jiti "^1.21.0"
lilconfig "^2.1.0"
micromatch "^4.0.5"
normalize-path "^3.0.0"