1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-21 11:37:53 +00:00

Compare commits

..

29 Commits

Author SHA1 Message Date
1e4f6a4b9d increase CLI 2023-01-24 00:10:42 -08:00
a73fc6de19 add cli version check before every command 2023-01-24 00:08:42 -08:00
0bb750488b fix typo in docs 2023-01-23 23:00:24 -08:00
32f98f83c5 update nav name for development instructions 2023-01-23 22:56:41 -08:00
6943785ce5 remove stay alive 2023-01-23 22:55:14 -08:00
86558a8221 simplify contribution docs 2023-01-23 22:55:14 -08:00
f2c35a302d Merge pull request from akhilmhdh/feat/component-update-1
Infisical component library foundations
2023-01-23 21:47:28 -08:00
0794b6132a auto create user for dev mode 2023-01-23 20:57:59 -08:00
062c287e75 feat(ui): changed to interfonts 2023-01-23 22:10:51 +05:30
e67d68a7b9 feat(ui): implemented basic table component 2023-01-23 22:10:51 +05:30
054acc689a feat(ui): implemented dropdown component 2023-01-23 22:10:51 +05:30
9b95d18b85 feat(ui): implemented switch component 2023-01-23 22:10:51 +05:30
7f9bc77253 feat(ui): added checkbox component 2023-01-23 22:10:51 +05:30
b92907aca6 feat(ui): added textarea component 2023-01-23 22:10:51 +05:30
c4ee03c73b featIui): added menu component 2023-01-23 22:10:51 +05:30
89ba80740b feat: added card and modal component 2023-01-23 22:10:50 +05:30
606a5e5317 feat(ui): added card component 2023-01-23 22:10:50 +05:30
f859bf528e feat(ui): added icon button component, updated secondary button style and added select to barrel export 2023-01-23 22:10:50 +05:30
ad504fa84e feat(ui) added select, spinner components 2023-01-23 22:10:50 +05:30
e7ac74c5a0 feat(ui): implemented form control components 2023-01-23 22:10:50 +05:30
b80504ae00 feat(ui): implemented new input component 2023-01-23 22:10:50 +05:30
68f1887d66 feat(ui): implemented button component 2023-01-23 22:10:50 +05:30
201c8352e3 fix typo in k8 self host 2023-01-22 16:50:45 -08:00
a0f0ffe566 improve i-dev command 2023-01-22 14:03:09 -08:00
4b4e8e2bfc Update docker integration 2023-01-22 12:50:06 -08:00
4db4c172c1 Fix the start guide redirect issue 2023-01-22 00:35:36 -08:00
00fee63ff3 Merge pull request from Infisical/more-integrations
Adjust integration bot authorization sequence
2023-01-22 11:17:15 +07:00
6b80cd6590 Modify case where integration bot was authorized but user didn't finish inputting their PAT -> should result in not sharing keys with bot 2023-01-22 11:12:47 +07:00
840efbdc2f Update API_URL to INFISICAL_API_URL 2023-01-21 13:14:39 -08:00
66 changed files with 3535 additions and 1041 deletions

@ -1,3 +1,4 @@
node_modules
built
healthcheck.js
tailwind.config.js

@ -8,7 +8,7 @@ up-dev:
docker-compose -f docker-compose.dev.yml up --build
i-dev:
infisical export && infisical export > .env && docker-compose -f docker-compose.dev.yml up --build
infisical run -- docker-compose -f docker-compose.dev.yml up --build
up-prod:
docker-compose -f docker-compose.yml up --build

@ -8,6 +8,7 @@ import { DatabaseService } from './services';
import { setUpHealthEndpoint } from './services/health';
import { initSmtp } from './services/smtp';
import { setTransporter } from './helpers/nodemailer';
import { createTestUserForDevelopment } from './utils/addDevelopmentUser';
DatabaseService.initDatabase(MONGO_URL);
@ -23,3 +24,5 @@ if (NODE_ENV !== 'test') {
environment: NODE_ENV
});
}
createTestUserForDevelopment()

@ -25,7 +25,6 @@ const requireIntegrationAuthorizationAuth = ({
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const { integrationAuthId } = req[location];
const integrationAuth = await IntegrationAuth.findOne({
_id: integrationAuthId
})

@ -0,0 +1,140 @@
/************************************************************************************************
*
* Attention: The credentials below are only for development purposes, it should never be used for production
*
************************************************************************************************/
import { NODE_ENV } from "../config"
import { Key, Membership, MembershipOrg, Organization, User, Workspace } from "../models";
import { Types } from 'mongoose';
export const createTestUserForDevelopment = async () => {
if (NODE_ENV === "development") {
const testUserEmail = "test@localhost.local"
const testUserPassword = "testInfisical1"
const testUserId = "63cefa6ec8d3175601cfa980"
const testWorkspaceId = "63cefb15c8d3175601cfa989"
const testOrgId = "63cefb15c8d3175601cfa985"
const testMembershipId = "63cefb159185d9aa3ef0cf35"
const testMembershipOrgId = "63cefb159185d9aa3ef0cf31"
const testWorkspaceKeyId = "63cf48f0225e6955acec5eff"
const testUser = {
_id: testUserId,
email: testUserEmail,
refreshVersion: 0,
encryptedPrivateKey: 'ITMdDXtLoxib4+53U/qzvIV/T/UalRwimogFCXv/UsulzEoiKM+aK2aqOb0=',
firstName: 'Jake',
iv: '9fp0dZHI+UuHeKkWMDvD6w==',
lastName: 'Moni',
publicKey: 'cf44BhkybbBfsE0fZHe2jvqtCj6KLXvSq4hVjV0svzk=',
salt: 'd8099dc70958090346910fb9639262b83cf526fc9b4555a171b36a9e1bcd0240',
tag: 'bQ/UTghqcQHRoSMpLQD33g==',
verifier: '12271fcd50937ca4512e1e3166adaf9d9fc7a5cd0e4c4cb3eda89f35572ede4d9eef23f64aef9220367abff9437b0b6fa55792c442f177201d87051cf77dadade254ff667170440327355fb7d6ac4745d4db302f4843632c2ed5919ebdcff343287a4cd552255d9e3ce81177edefe089617b7616683901475d393405f554634b9bf9230c041ac85624f37a60401be20b78044932580ae0868323be3749fbf856df1518153ba375fec628275f0c445f237446ea4aa7f12c1aa1d6b5fd74b7f2e88d062845a19819ec63f2d2ed9e9f37c055149649461d997d2ae1482f53b04f9de7493efbb9686fb19b2d559b9aa2b502c22dec83f9fc43290dfea89a1dc6f03580b3642b3824513853e81a441be9a0b2fde2231bac60f3287872617a36884697805eeea673cf1a351697834484ada0f282e4745015c9c2928d61e6d092f1b9c3a27eda8413175d23bb2edae62f82ccaf52bf5a6a90344a766c7e4ebf65dae9ae90b2ad4ae65dbf16e3a6948e429771cc50307ae86d454f71a746939ed061f080dd3ae369c1a0739819aca17af46a085bac1f2a5d936d198e7951a8ac3bb38b893665fe7312835abd3f61811f81efa2a8761af5070085f9b6adcca80bf9b0d81899c3d41487fba90728bb24eceb98bd69770360a232624133700ceb4d153f2ad702e0a5b7dfaf97d20bc8aa71dc8c20024a58c06a8fecdad18cb5a2f89c51eaf7'
}
const testWorkspaceKey = {
_id: new Types.ObjectId(testWorkspaceKeyId),
workspace: testWorkspaceId,
encryptedKey: '96ZIRSU21CjVzIQ4Yp994FGWQvDdyK3gq+z+NCaJLK0ByTlvUePmf+AYGFJjkAdz',
nonce: '1jhCGqg9Wx3n0OtVxbDgiYYGq4S3EdgO',
sender: '63cefa6ec8d3175601cfa980',
receiver: '63cefa6ec8d3175601cfa980',
}
const testWorkspace = {
_id: new Types.ObjectId(testWorkspaceId),
name: 'Example Project',
organization: testOrgId,
environments: [
{
_id: '63cefb15c8d3175601cfa98a',
name: 'Development',
slug: 'dev'
},
{
_id: '63cefb15c8d3175601cfa98b',
name: 'Test',
slug: 'test'
},
{
_id: '63cefb15c8d3175601cfa98c',
name: 'Staging',
slug: 'staging'
},
{
_id: '63cefb15c8d3175601cfa98d',
name: 'Production',
slug: 'prod'
}
],
}
const testOrg = {
_id: testOrgId,
name: 'Jake\'s organization'
}
const testMembershipOrg = {
_id: testMembershipOrgId,
organization: testOrgId,
role: 'owner',
status: 'accepted',
user: testUserId,
}
const testMembership = {
_id: testMembershipId,
role: 'admin',
user: testUserId,
workspace: testWorkspaceId
}
try {
// create user if not exist
const userInDB = await User.findById(testUserId)
if (!userInDB) {
await User.create(testUser)
}
// create org if not exist
const orgInDB = await Organization.findById(testOrgId)
if (!orgInDB) {
await Organization.create(testOrg)
}
// create membership org if not exist
const membershipOrgInDB = await MembershipOrg.findById(testMembershipOrgId)
if (!membershipOrgInDB) {
await MembershipOrg.create(testMembershipOrg)
}
// create membership
const membershipInDB = await Membership.findById(testMembershipId)
if (!membershipInDB) {
await Membership.create(testMembership)
}
// create workspace if not exist
const workspaceInDB = await Workspace.findById(testWorkspaceId)
if (!workspaceInDB) {
await Workspace.create(testWorkspace)
}
// create workspace key if not exist
const workspaceKeyInDB = await Key.findById(testWorkspaceKeyId)
if (!workspaceKeyInDB) {
await Key.create(testWorkspaceKey)
}
/* eslint-disable no-console */
console.info(`DEVELOPMENT MODE DETECTED: You may login with test user with email: ${testUserEmail} and password: ${testUserPassword}`)
/* eslint-enable no-console */
} catch (e) {
/* eslint-disable no-console */
console.error(`Unable to create test user while booting up [err=${e}]`)
/* eslint-enable no-console */
}
}
}

@ -90,7 +90,7 @@ const INTEGRATION_OPTIONS = [
name: 'Fly.io',
slug: 'flyio',
image: 'Flyio.svg',
isAvailable: true,
isAvailable: false,
type: 'pat',
clientId: '',
docsLink: ''

@ -17,7 +17,7 @@ var rootCmd = &cobra.Command{
Short: "Infisical CLI is used to inject environment variables into any process",
Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`,
CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true},
Version: "0.2.5",
Version: util.CLI_VERSION,
}
// Execute adds all child commands to the root command and sets flags appropriately.
@ -32,14 +32,15 @@ func Execute() {
func init() {
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
rootCmd.PersistentFlags().BoolVarP(&debugLogging, "debug", "d", false, "Enable verbose logging")
rootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", util.INFISICAL_DEFAULT_API_URL, "Point the CLI to your own backend [can also set via environment variable name: API_URL]")
// rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
// }
rootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", util.INFISICAL_DEFAULT_API_URL, "Point the CLI to your own backend [can also set via environment variable name: INFISICAL_API_URL]")
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.CheckForUpdate()
}
// if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment
// this is used to allow overrides of the default value
if !rootCmd.Flag("domain").Changed {
if envInfisicalBackendUrl, ok := os.LookupEnv("API_URL"); ok {
if envInfisicalBackendUrl, ok := os.LookupEnv("INFISICAL_API_URL"); ok {
config.INFISICAL_URL = envInfisicalBackendUrl
}
}

@ -0,0 +1,42 @@
package util
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
func CheckForUpdate() {
latestVersion, err := getLatestTag("infisical", "infisical")
if err != nil {
// do nothing and continue
return
}
if latestVersion != CLI_VERSION {
PrintWarning(fmt.Sprintf("Please update your CLI. You are running version %s but the latest version is %s", CLI_VERSION, latestVersion))
}
}
func getLatestTag(repoOwner string, repoName string) (string, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/tags", repoOwner, repoName)
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
var tags []struct {
Name string `json:"name"`
}
json.Unmarshal(body, &tags)
return tags[0].Name, nil
}

@ -11,4 +11,5 @@ const (
KEYRING_SERVICE_NAME = "infisical"
PERSONAL_SECRET_TYPE_NAME = "personal"
SHARED_SECRET_TYPE_NAME = "shared"
CLI_VERSION = "v0.2.6"
)

@ -93,7 +93,7 @@ services:
smtp-server:
container_name: infisical-dev-smtp-server
image: mailhog/mailhog
image: lytrax/mailhog:latest # https://github.com/mailhog/MailHog/issues/353#issuecomment-821137362
restart: always
logging:
driver: 'none' # disable saving logs

@ -109,11 +109,11 @@ infisical login
The CLI is set to connect to Infisical Cloud by default, but if you're running your own instance of Infisical, you can direct the CLI to it using one of the methods provided below.
#### Export environment variable
You can point the CLI to the self hosted Infisical instance by exporting the environment variable `API_URL` in your terminal.
You can point the CLI to the self hosted Infisical instance by exporting the environment variable `INFISICAL_API_URL` in your terminal.
```bash
# Example
export API_URL="https://your-self-hosted-infisical.com/api"
export INFISICAL_API_URL="https://your-self-hosted-infisical.com/api"
```
#### Set manually on every command

@ -1,13 +1,12 @@
---
title: 'Developing'
title: 'Local development'
description: 'This guide will help you set up and run Infisical in development mode.'
---
## Clone the repo
```bash
# change location to the path you want Infisical to be installed
cd ~
# navigate location to the path you want Infisical to be installed
# clone the repo and cd to Infisical dir
git clone https://github.com/Infisical/infisical
@ -16,57 +15,71 @@ cd infisical
## Set up environment variables
Start by creating a .env file at the root of the Infisical directory. It's best to start with the provided [`.env.example`](https://github.com/Infisical/infisical/blob/main/.env.example) template containing the necessary envars to fill out your .env file — you only have to modify the SMTP parameters.
Start by creating a .env file at the root of the Infisical directory then copy the contents of the file below into the .env file.
<Accordion title=".env file content">
```env
# Keys
# Required key for platform encryption/decryption ops
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# JWT
# Required secrets to sign JWT tokens
JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
# MongoDB
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
# to the MongoDB container instance or Mongo Cloud
# Required
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
# Optional credentials for MongoDB container instance and Mongo-Express
MONGO_USERNAME=root
MONGO_PASSWORD=example
# Website URL
# Required
SITE_URL=http://localhost:8080
# Mail/SMTP
SMTP_HOST='smtp-server'
SMTP_PORT='1025'
SMTP_NAME='local'
SMTP_USERNAME='team@infisical.com'
SMTP_PASSWORD=
```
</Accordion>
<Warning>
The pre-populated environment variable values in the `.env.example` file are meant to be used in development only.
You'll want to fill in your own values in production, especially concerning encryption keys, secrets, and SMTP parameters.
The pre-populated environment variable values above are meant to be used in development only. They should never be used in production.
</Warning>
Refer to the [environment variable list](https://infisical.com/docs/self-hosting/configuration/envars) for guidance on each envar.
View all available [environment variables](https://infisical.com/docs/self-hosting/configuration/envars) and guidance for each.
### Helpful tips for developing with Infisical:
## Starting Infisical for development
<Tip>
Use the `ENCRYPTION_KEY`, JWT-secret envars, `MONGO_URL`, `MONGO_USERNAME`, `MONGO_PASSWORD` provided in the `.env.example` file.
We use use Docker to easily spin up all required services to have Infisical up and running for development. If you are unfamiliar with Docker, don't worry, all you have to do is install [Docker](https://docs.docker.com/get-docker/) for your
machine and run the commands below to start up the development server.
If setting your own values:
- `ENCRYPTION_KEY` should be a [32-byte random hex](https://www.browserling.com/tools/random-hex)
- `MONGO_URL` should take the form: `mongodb://[MONGO_USERNAME]:[MONGO_PASSWORD]@mongo:27017/?authSource=admin`.
</Tip>
<Tip>
Bring and configure your own SMTP server by following our [email configuration guide](https://infisical.com/docs/self-hosting/configuration/email) (we recommend using either SendGrid or Mailgun).
Alternatively, you can use the provided development (Mailhog) SMTP server to send and browse emails sent by the backend on http://localhost:8025; to use this option, set the following `SMTP_HOST`, `SMTP_PORT`, `SMTP_FROM_NAME`, `SMTP_USERNAME`, `SMTP_PASSWORD` below.
</Tip>
```
SMTP_HOST=smtp-server
SMTP_PORT=1025
SMTP_FROM_ADDRESS=team@infisical.com
SMTP_FROM_NAME=Infisical
SMTP_USERNAME=team@infisical.com
SMTP_PASSWORD=
```
<Warning>
If using Mailhog, make sure to leave the `SMTP_PASSWORD` blank so the backend can connect to MailHog.
</Warning>
## Docker for development
#### Start local server
```bash
# build and start the services
docker-compose -f docker-compose.dev.yml up --build --force-recreate
```
#### Access local server
Then browse http://localhost:8080
Once all the services have spun up, browse to http://localhost:8080. To sign in, you may use the default credentials listed below.
Email: `test@localhost.local`
Password: `testInfisical1`
#### Shutdown local server
```bash
# To stop environment use Control+C (on Mac) CTRL+C (on Win) or
docker-compose -f docker-compose.dev.yml down
# start services
docker-compose -f docker-compose.dev.yml up
```

@ -31,25 +31,22 @@ Infisical can be used in a Dockerfile to inject environment variables into a Doc
</Tab>
</Tabs>
## Modify the start command in your Dockerfile
## Modify your Dockerfile start command
```dockerfile
CMD ["infisical", "--env=[env]", "projectId=[projectId]", "run", "---", "[your application start command]"]
To make your Docker container consume Infisical secrets, you can start your application with Infisical.
This will automatically pull the necessary secrets and make them available to your application as if they were natively exposed within the container.
# example
CMD ["infisical", "--env=prod", "projectId=62faf98ae0b05e83239b5da41", "run", "---", "npm run start"]
```
```dockerfile
CMD ["infisical", "run", "---", "[your application start command]"]
Required options:
# example with single single command
CMD ["infisical", "run", "---", "npm run start"]
| Option | Description | Default value |
| ------------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
| `--projectId` | Used to link a local project to the platform | `None` |
# example with multiple commands
CMD ["infisical", "run", "--command" "npm run start && ..."]
```
## Generate an Infisical Token
[Generate an Infisical Token](../../getting-started/dashboard/token) and keep it handy.
View more options for the `run` command [here](../../cli/commands/run)
## Feed Docker your Infisical Token
@ -58,3 +55,8 @@ The CLI looks out for an environment variable called `INFISICAL_TOKEN`. If the t
```bash
docker run --env INFISICAL_TOKEN=[token]...
```
## Generate an Infisical Token
[Generate an Infisical Token](../../getting-started/dashboard/token) and keep it handy.

@ -23,7 +23,7 @@ If you're less technical and looking for a hands-free experience with minimal ov
Before you can deploy the Helm chart, you must fill out the required environment variables. To do so, please copy the below file to a `.yaml` file.
Refer to the available [environment variables](../../self-hosting/configuration/envars) to learn more
<Accordion title="vaules.yaml">
<Accordion title="values.yaml">
```yaml
#####
# INFISICAL K8 DEFAULT VALUES FIL

@ -1,3 +1,4 @@
import { themes } from '@storybook/theming';
import '../src/styles/globals.css';
export const parameters = {
@ -7,5 +8,8 @@ export const parameters = {
color: /(background|color)$/i,
date: /Date$/
}
},
darkMode: {
dark: { ...themes.dark, appContentBg: '#0e1014', appBg: '#0e1014' }
}
};

@ -7,6 +7,7 @@
"dependencies": {
"@emotion/css": "^11.10.0",
"@emotion/server": "^11.10.0",
"@fontsource/inter": "^4.5.15",
"@fortawesome/fontawesome-svg-core": "^6.1.2",
"@fortawesome/free-brands-svg-icons": "^6.1.2",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
@ -15,7 +16,9 @@
"@headlessui/react": "^1.6.6",
"@radix-ui/react-accordion": "^1.1.0",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.1",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-popover": "^1.0.3",
"@radix-ui/react-progress": "^1.0.1",
@ -30,6 +33,7 @@
"axios-auth-refresh": "^3.3.3",
"classnames": "^2.3.1",
"cookies": "^0.8.0",
"cva": "npm:class-variance-authority@^0.4.0",
"fs": "^0.0.1-security",
"gray-matter": "^4.0.3",
"http-proxy": "^1.18.1",
@ -94,7 +98,7 @@
"prettier": "^2.8.3",
"storybook": "^7.0.0-beta.30",
"storybook-dark-mode": "^2.0.5",
"tailwindcss": "^3.1.4",
"tailwindcss": "3.2",
"typescript": "^4.9.3"
}
},
@ -2790,6 +2794,11 @@
"react-dom": ">=16.8.0"
}
},
"node_modules/@fontsource/inter": {
"version": "4.5.15",
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.5.15.tgz",
"integrity": "sha512-FzleM9AxZQK2nqsTDtBiY0PMEVWvnKnuu2i09+p6DHvrHsuucoV2j0tmw+kAT3L4hvsLdAIDv6MdGehsPIdT+Q=="
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.2.tgz",
@ -3659,6 +3668,26 @@
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-checkbox": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.1.tgz",
"integrity": "sha512-TisH0B8hWmYP3ONRduYCyN04rR9yLPIw/Rwyn1RoC1suSoGCa8Wn+YPdSSSarSszeIbcg3p2lBkDp2XXit4sZw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.0",
"@radix-ui/react-use-previous": "1.0.0",
"@radix-ui/react-use-size": "1.0.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-collapsible": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.1.tgz",
@ -3771,6 +3800,25 @@
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-dropdown-menu": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.2.tgz",
"integrity": "sha512-r0kN0fstrSi+uAdK2GkLxnnbhqVBy/9Q4o4PvGOYipW0BldQlYBMSmZprvCNj2i2mAATx16kvzIn12GnaGjbMw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-id": "1.0.0",
"@radix-ui/react-menu": "2.0.2",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz",
@ -3822,6 +3870,36 @@
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-menu": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.2.tgz",
"integrity": "sha512-H5dtBi/k3tc45IMd2Pu+Q2PyONFlsYJ5sWUlflSs8BQRghh5GhJHLRuB1yb88VOywuzzvGkaR/HUJJ65Jf2POA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-collection": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-direction": "1.0.0",
"@radix-ui/react-dismissable-layer": "1.0.2",
"@radix-ui/react-focus-guards": "1.0.0",
"@radix-ui/react-focus-scope": "1.0.1",
"@radix-ui/react-id": "1.0.0",
"@radix-ui/react-popper": "1.1.0",
"@radix-ui/react-portal": "1.0.1",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-roving-focus": "1.0.2",
"@radix-ui/react-slot": "1.0.1",
"@radix-ui/react-use-callback-ref": "1.0.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.3.tgz",
@ -3925,6 +4003,27 @@
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz",
"integrity": "sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-collection": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-direction": "1.0.0",
"@radix-ui/react-id": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-use-callback-ref": "1.0.0",
"@radix-ui/react-use-controllable-state": "1.0.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-select": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-1.2.0.tgz",
@ -9938,6 +10037,23 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz",
"integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA=="
},
"node_modules/cva": {
"name": "class-variance-authority",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.4.0.tgz",
"integrity": "sha512-74enNN8O9ZNieycac/y8FxqgyzZhZbxmCitAtAeUrLPlxjSd5zA7LfpprmxEcOmQBnaGs5hYhiSGnJ0mqrtBLQ==",
"funding": {
"url": "https://joebell.co.uk"
},
"peerDependencies": {
"typescript": ">= 4.5.5 < 5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@ -11993,9 +12109,9 @@
"dev": true
},
"node_modules/fast-glob": {
"version": "3.2.11",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
"integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
"version": "3.2.12",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
"integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
"dev": true,
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
@ -17168,12 +17284,12 @@
}
},
"node_modules/postcss-nested": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz",
"integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz",
"integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==",
"dev": true,
"dependencies": {
"postcss-selector-parser": "^6.0.6"
"postcss-selector-parser": "^6.0.10"
},
"engines": {
"node": ">=12.0"
@ -20385,9 +20501,9 @@
"integrity": "sha512-+fflfPxvHFr81hTJpQ3MIwtqgvefHZFUHFiIHpVIRXvG/nX9+gu2P7JNlFu2bfDMJ+uHhi/pUgzaYacMoXv+Ww=="
},
"node_modules/tailwindcss": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.7.tgz",
"integrity": "sha512-r7mgumZ3k0InfVPpGWcX8X/Ut4xBfv+1O/+C73ar/m01LxGVzWvPxF/w6xIUPEztrCoz7axfx0SMdh8FH8ZvRQ==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz",
"integrity": "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==",
"dev": true,
"dependencies": {
"arg": "^5.0.2",
@ -20396,18 +20512,19 @@
"detective": "^5.2.1",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.2.11",
"fast-glob": "^3.2.12",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"lilconfig": "^2.0.6",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.0.0",
"postcss": "^8.4.14",
"postcss": "^8.4.18",
"postcss-import": "^14.1.0",
"postcss-js": "^4.0.0",
"postcss-load-config": "^3.1.4",
"postcss-nested": "5.0.6",
"postcss-nested": "6.0.0",
"postcss-selector-parser": "^6.0.10",
"postcss-value-parser": "^4.2.0",
"quick-lru": "^5.1.1",
@ -20424,6 +20541,30 @@
"postcss": "^8.0.9"
}
},
"node_modules/tailwindcss/node_modules/postcss": {
"version": "8.4.21",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
"integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
}
],
"dependencies": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@ -21135,7 +21276,7 @@
"version": "4.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
"dev": true,
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -24029,6 +24170,11 @@
"use-isomorphic-layout-effect": "^1.1.1"
}
},
"@fontsource/inter": {
"version": "4.5.15",
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.5.15.tgz",
"integrity": "sha512-FzleM9AxZQK2nqsTDtBiY0PMEVWvnKnuu2i09+p6DHvrHsuucoV2j0tmw+kAT3L4hvsLdAIDv6MdGehsPIdT+Q=="
},
"@fortawesome/fontawesome-common-types": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.2.tgz",
@ -24592,6 +24738,22 @@
"@radix-ui/react-primitive": "1.0.1"
}
},
"@radix-ui/react-checkbox": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.1.tgz",
"integrity": "sha512-TisH0B8hWmYP3ONRduYCyN04rR9yLPIw/Rwyn1RoC1suSoGCa8Wn+YPdSSSarSszeIbcg3p2lBkDp2XXit4sZw==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.0",
"@radix-ui/react-use-previous": "1.0.0",
"@radix-ui/react-use-size": "1.0.0"
}
},
"@radix-ui/react-collapsible": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.1.tgz",
@ -24679,6 +24841,21 @@
"@radix-ui/react-use-escape-keydown": "1.0.2"
}
},
"@radix-ui/react-dropdown-menu": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.2.tgz",
"integrity": "sha512-r0kN0fstrSi+uAdK2GkLxnnbhqVBy/9Q4o4PvGOYipW0BldQlYBMSmZprvCNj2i2mAATx16kvzIn12GnaGjbMw==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-id": "1.0.0",
"@radix-ui/react-menu": "2.0.2",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.0"
}
},
"@radix-ui/react-focus-guards": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz",
@ -24716,6 +24893,32 @@
"@radix-ui/react-primitive": "1.0.1"
}
},
"@radix-ui/react-menu": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.2.tgz",
"integrity": "sha512-H5dtBi/k3tc45IMd2Pu+Q2PyONFlsYJ5sWUlflSs8BQRghh5GhJHLRuB1yb88VOywuzzvGkaR/HUJJ65Jf2POA==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-collection": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-direction": "1.0.0",
"@radix-ui/react-dismissable-layer": "1.0.2",
"@radix-ui/react-focus-guards": "1.0.0",
"@radix-ui/react-focus-scope": "1.0.1",
"@radix-ui/react-id": "1.0.0",
"@radix-ui/react-popper": "1.1.0",
"@radix-ui/react-portal": "1.0.1",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-roving-focus": "1.0.2",
"@radix-ui/react-slot": "1.0.1",
"@radix-ui/react-use-callback-ref": "1.0.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
}
},
"@radix-ui/react-popover": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.3.tgz",
@ -24795,6 +24998,23 @@
"@radix-ui/react-primitive": "1.0.1"
}
},
"@radix-ui/react-roving-focus": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz",
"integrity": "sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-collection": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-direction": "1.0.0",
"@radix-ui/react-id": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-use-callback-ref": "1.0.0",
"@radix-ui/react-use-controllable-state": "1.0.0"
}
},
"@radix-ui/react-select": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-1.2.0.tgz",
@ -29292,6 +29512,12 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz",
"integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA=="
},
"cva": {
"version": "npm:class-variance-authority@0.4.0",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.4.0.tgz",
"integrity": "sha512-74enNN8O9ZNieycac/y8FxqgyzZhZbxmCitAtAeUrLPlxjSd5zA7LfpprmxEcOmQBnaGs5hYhiSGnJ0mqrtBLQ==",
"requires": {}
},
"damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@ -30880,9 +31106,9 @@
"dev": true
},
"fast-glob": {
"version": "3.2.11",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
"integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
"version": "3.2.12",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
"integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
"dev": true,
"requires": {
"@nodelib/fs.stat": "^2.0.2",
@ -34590,12 +34816,12 @@
}
},
"postcss-nested": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz",
"integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz",
"integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==",
"dev": true,
"requires": {
"postcss-selector-parser": "^6.0.6"
"postcss-selector-parser": "^6.0.10"
}
},
"postcss-selector-parser": {
@ -36960,9 +37186,9 @@
"integrity": "sha512-+fflfPxvHFr81hTJpQ3MIwtqgvefHZFUHFiIHpVIRXvG/nX9+gu2P7JNlFu2bfDMJ+uHhi/pUgzaYacMoXv+Ww=="
},
"tailwindcss": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.7.tgz",
"integrity": "sha512-r7mgumZ3k0InfVPpGWcX8X/Ut4xBfv+1O/+C73ar/m01LxGVzWvPxF/w6xIUPEztrCoz7axfx0SMdh8FH8ZvRQ==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz",
"integrity": "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==",
"dev": true,
"requires": {
"arg": "^5.0.2",
@ -36971,22 +37197,36 @@
"detective": "^5.2.1",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.2.11",
"fast-glob": "^3.2.12",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"lilconfig": "^2.0.6",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.0.0",
"postcss": "^8.4.14",
"postcss": "^8.4.18",
"postcss-import": "^14.1.0",
"postcss-js": "^4.0.0",
"postcss-load-config": "^3.1.4",
"postcss-nested": "5.0.6",
"postcss-nested": "6.0.0",
"postcss-selector-parser": "^6.0.10",
"postcss-value-parser": "^4.2.0",
"quick-lru": "^5.1.1",
"resolve": "^1.22.1"
},
"dependencies": {
"postcss": {
"version": "8.4.21",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
"integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
"dev": true,
"requires": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
}
}
}
},
"tapable": {
@ -37538,7 +37778,7 @@
"version": "4.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
"dev": true
"devOptional": true
},
"uc.micro": {
"version": "1.0.6",

@ -8,12 +8,13 @@
"start:docker": "next build && next start",
"lint": "eslint --fix --ext js,ts,tsx ./src",
"type-check": "tsc --project tsconfig.json",
"storybook": "storybook dev -p 6006",
"storybook": "storybook dev -p 6006 -s ./public",
"build-storybook": "storybook build"
},
"dependencies": {
"@emotion/css": "^11.10.0",
"@emotion/server": "^11.10.0",
"@fontsource/inter": "^4.5.15",
"@fortawesome/fontawesome-svg-core": "^6.1.2",
"@fortawesome/free-brands-svg-icons": "^6.1.2",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
@ -22,7 +23,9 @@
"@headlessui/react": "^1.6.6",
"@radix-ui/react-accordion": "^1.1.0",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.1",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-popover": "^1.0.3",
"@radix-ui/react-progress": "^1.0.1",
@ -37,6 +40,7 @@
"axios-auth-refresh": "^3.3.3",
"classnames": "^2.3.1",
"cookies": "^0.8.0",
"cva": "npm:class-variance-authority@^0.4.0",
"fs": "^0.0.1-security",
"gray-matter": "^4.0.3",
"http-proxy": "^1.18.1",
@ -101,7 +105,7 @@
"prettier": "^2.8.3",
"storybook": "^7.0.0-beta.30",
"storybook-dark-mode": "^2.0.5",
"tailwindcss": "^3.1.4",
"tailwindcss": "3.2",
"typescript": "^4.9.3"
}
}

@ -19,7 +19,6 @@ type Props = {
isOpen: boolean;
closeModal: () => void;
selectedIntegrationOption: IntegrationOption | null;
handleBotActivate: () => Promise<void>;
integrationOptionPress: (integrationOption: IntegrationOption) => void;
};
@ -27,20 +26,16 @@ const ActivateBotDialog = ({
isOpen,
closeModal,
selectedIntegrationOption,
handleBotActivate,
integrationOptionPress
}: Props) => {
const { t } = useTranslation();
const submit = async () => {
try {
// 1. activate bot
await handleBotActivate();
// type check
if (!selectedIntegrationOption) return;
// 2. start integration or probe for PAT
// start integration or probe for PAT
integrationOptionPress(selectedIntegrationOption);
} catch (err) {

@ -52,7 +52,6 @@ const IntegrationAccessTokenDialog = ({
<div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={() => {
console.log('onClose');
closeModal();
}}>
<Transition.Child

@ -30,8 +30,8 @@ const ProjectIntegrationSection = ({
setBot,
environments = [],
handleDeleteIntegration
}: Props) =>
integrations.length > 0 ? (
}: Props) => {
return integrations.length > 0 ? (
<div className="mb-12">
<div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2">
<h1 className="font-semibold text-3xl">Current Integrations</h1>
@ -40,7 +40,6 @@ const ProjectIntegrationSection = ({
</p>
</div>
{integrations.map((integration: Integration) => {
console.log('IntegrationSection integration: ', integration);
return (
<IntegrationTile
key={`integration-${integration._id.toString()}`}
@ -58,5 +57,6 @@ const ProjectIntegrationSection = ({
) : (
<div />
);
}
export default ProjectIntegrationSection;

@ -1,3 +1,5 @@
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
@ -6,7 +8,16 @@ const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
tags: ['v2'],
argTypes: {}
argTypes: {
isRounded: {
defaultValue: true,
type: 'boolean'
},
isFullWidth: {
defaultValue: false,
type: 'boolean'
}
}
};
export default meta;
@ -18,3 +29,61 @@ export const Primary: Story = {
children: 'Hello Infisical'
}
};
export const Secondary: Story = {
args: {
children: 'Hello Infisical',
colorSchema: 'secondary',
variant: 'outline'
}
};
export const Danger: Story = {
args: {
children: 'Hello Infisical',
colorSchema: 'danger',
variant: 'solid'
}
};
export const Plain: Story = {
args: {
children: 'Hello Infisical',
variant: 'plain'
}
};
export const Disabled: Story = {
args: {
children: 'Hello Infisical',
disabled: true
}
};
export const FullWidth: Story = {
args: {
children: 'Hello Infisical',
isFullWidth: true
}
};
export const Loading: Story = {
args: {
children: 'Hello Infisical',
isLoading: true
}
};
export const LeftIcon: Story = {
args: {
children: 'Hello Infisical',
leftIcon: <FontAwesomeIcon icon={faPlus} className="pr-0.5" />
}
};
export const RightIcon: Story = {
args: {
children: 'Hello Infisical',
rightIcon: <FontAwesomeIcon icon={faPlus} className="pr-0.5" />
}
};

@ -1,32 +1,166 @@
import { ButtonHTMLAttributes, forwardRef, ReactNode } from 'react';
import { cva, VariantProps } from 'cva';
import { twMerge } from 'tailwind-merge';
type Props = {
children: ReactNode;
isDisabled?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
// loading state
isLoading?: boolean;
// various button sizes
size: 'sm' | 'md' | 'lg';
};
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & Props;
const buttonVariants = cva(
[
'button',
'transition-all',
'font-inter font-medium',
'cursor-pointer',
'inline-flex items-center justify-center',
'relative'
],
{
variants: {
colorSchema: {
primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
danger: ['bg-red', 'text-white', 'border-red']
},
variant: {
solid: '',
outline: ['bg-transparent', 'border-2', 'border-solid'],
plain: ''
},
isDisabled: {
true: 'bg-opacity-70 cursor-not-allowed',
false: ''
},
isFullWidth: {
true: 'w-full',
false: ''
},
isRounded: {
true: 'rounded-md',
false: ''
},
size: {
xs: ['text-xs', 'py-1', 'px-2'],
sm: ['text-sm', 'py-2', 'px-4'],
md: ['text-md', 'py-2', 'px-6'],
lg: ['text-lg', 'py-2', 'px-8']
}
},
compoundVariants: [
{
colorSchema: 'primary',
variant: 'outline',
className: 'text-primary hover:bg-primary hover:text-black'
},
{
colorSchema: 'secondary',
variant: 'outline',
className: 'hover:bg-mineshaft'
},
{
colorSchema: 'danger',
variant: 'outline',
className: 'text-red hover:bg-red hover:text-black'
},
{
colorSchema: 'primary',
variant: 'plain',
className: 'text-primary'
},
{
colorSchema: 'secondary',
variant: 'plain',
className: 'text-mineshaft'
},
{
colorSchema: 'danger',
variant: 'plain',
className: 'text-red'
},
{
colorSchema: ['danger', 'primary', 'secondary'],
variant: ['plain'],
className: 'bg-transparent py-1 px-1'
}
]
}
);
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonVariants> &
Props;
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ children, isDisabled = false, className, ...props }, ref): JSX.Element => {
(
{
children,
isDisabled = false,
className = '',
size = 'md',
variant = 'solid',
isFullWidth,
isRounded = true,
leftIcon,
rightIcon,
isLoading,
colorSchema = 'primary',
...props
},
ref
): JSX.Element => {
const loadingToggleClass = isLoading ? 'opacity-0' : 'opacity-100';
return (
<button
ref={ref}
aria-disabled={isDisabled}
type="button"
className={twMerge(
'bg-primary hover:opacity-80 transition-all text-sm px-4 py-2 font-bold rounded',
className
buttonVariants({
className,
colorSchema,
size,
variant,
isRounded,
isDisabled,
isFullWidth
})
)}
disabled={isDisabled}
{...props}
>
{children}
{isLoading && (
<img
src="/images/loading/loadingblack.gif"
width={36}
alt="loading animation"
className="absolute rounded-xl"
/>
)}
<span
className={twMerge(
'transition-all shrink-0 cursor-pointer',
loadingToggleClass,
size === 'xs' ? 'mr-1' : 'mr-2'
)}
>
{leftIcon}
</span>
<span className={twMerge('transition-all', loadingToggleClass)}>{children}</span>
<span
className={twMerge(
'transition-all shrink-0 cursor-pointer',
loadingToggleClass,
size === 'xs' ? 'ml-1' : 'ml-2'
)}
>
{rightIcon}
</span>
</button>
);
}

@ -1 +1,2 @@
export type { ButtonProps } from './Button';
export { Button } from './Button';

@ -0,0 +1,40 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Card, CardBody, CardFooter, CardProps, CardTitle } from './Card';
const meta: Meta<typeof Card> = {
title: 'Components/Card',
component: Card,
tags: ['v2'],
argTypes: {
isRounded: {
type: 'boolean',
defaultValue: true
}
}
};
export default meta;
type Story = StoryObj<typeof Card>;
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
const Template = (args: CardProps) => (
<div className="w-96">
<Card {...args}>
<CardTitle subTitle="Please add your subtitle here">Title</CardTitle>
<CardBody>Content</CardBody>
<CardFooter>Footer</CardFooter>
</Card>
</div>
);
export const Basic: Story = {
render: (args) => <Template {...args} />
};
export const Hoverable: Story = {
render: (args) => <Template {...args} />,
args: {
isHoverable: true
}
};

@ -0,0 +1,64 @@
import { forwardRef, ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';
export type CardTitleProps = {
children: ReactNode;
subTitle?: string;
className?: string;
};
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
<div className={twMerge('p-6 pb-4 font-sans text-xl font-medium', className)}>
{children}
{subTitle && <p className="py-1 text-sm font-normal text-gray-400">{subTitle}</p>}
</div>
);
export type CardFooterProps = {
children: ReactNode;
className?: string;
};
export const CardFooter = ({ children, className }: CardFooterProps) => (
<div className={twMerge('p-6 pt-0', className)}>{children}</div>
);
export type CardBodyProps = {
children: ReactNode;
className?: string;
};
export const CardBody = ({ children, className }: CardBodyProps) => (
<div className={twMerge('p-6 pt-0', className)}>{children}</div>
);
export type CardProps = {
children: ReactNode;
className?: string;
isFullHeight?: boolean;
isRounded?: boolean;
isPlain?: boolean;
isHoverable?: boolean;
};
export const Card = forwardRef<HTMLDivElement, CardProps>(
({ children, isFullHeight, isRounded, isHoverable, isPlain, className }, ref): JSX.Element => {
return (
<div
ref={ref}
className={twMerge(
'flex flex-col w-full font-inter text-gray-200 bg-mineshaft-700 shadow-md',
isFullHeight && 'h-full',
isRounded && 'rounded-md',
isPlain && 'shadow-none',
isHoverable && 'hover:shadow-xl',
className
)}
>
{children}
</div>
);
}
);
Card.displayName = 'Card';

@ -0,0 +1,2 @@
export type { CardBodyProps, CardFooterProps, CardProps, CardTitleProps } from './Card';
export { Card, CardBody, CardFooter, CardTitle } from './Card';

@ -0,0 +1,34 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Checkbox } from './Checkbox';
const meta: Meta<typeof Checkbox> = {
title: 'Components/Checkbox',
component: Checkbox,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof Checkbox>;
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Simple: Story = {
args: {
children: 'Accept the condition'
}
};
export const Disabled: Story = {
args: {
children: 'Accept the condition',
isDisabled: true
}
};
export const Required: Story = {
args: {
children: 'Accept the condition',
isRequired: true
}
};

@ -0,0 +1,51 @@
import { ReactNode } from 'react';
import { faCheck } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { twMerge } from 'tailwind-merge';
export type CheckboxProps = Omit<
CheckboxPrimitive.CheckboxProps,
'checked' | 'disabled' | 'required'
> & {
children: ReactNode;
id: string;
isDisabled?: boolean;
isChecked?: boolean;
isRequired?: boolean;
};
export const Checkbox = ({
children,
className,
id,
isChecked,
isDisabled,
isRequired,
...props
}: CheckboxProps): JSX.Element => {
return (
<div className="flex items-center font-inter text-bunker-300">
<CheckboxPrimitive.Root
className={twMerge(
'flex items-center justify-center w-5 h-5 mr-3 transition-all rounded shadow hover:bg-bunker-200 bg-bunker-300',
isDisabled && 'bg-bunker-400 hover:bg-bunker-400',
className
)}
required={isRequired}
checked={isChecked}
disabled={isDisabled}
{...props}
id={id}
>
<CheckboxPrimitive.Indicator className="text-bunker-800">
<FontAwesomeIcon icon={faCheck} size="sm" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
<label className="text-sm" htmlFor={id}>
{children}
{isRequired && <span className="pl-1 text-red">*</span>}
</label>
</div>
);
};

@ -0,0 +1,2 @@
export type { CheckboxProps } from './Checkbox';
export { Checkbox } from './Checkbox';

@ -0,0 +1,111 @@
import { faPlus, faTrash, faUndo } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { Meta, StoryObj } from '@storybook/react';
import { IconButton } from '../IconButton';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from './Dropdown';
const meta: Meta<typeof DropdownMenu> = {
title: 'Components/DropdownMenu',
component: DropdownMenu,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof DropdownMenuContent>;
export const Basic: Story = {
render: (args) => (
<div className="flex justify-center w-full">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton ariaLabel="add">
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" {...args}>
<DropdownMenuItem>Delete</DropdownMenuItem>
<DropdownMenuItem>Undo</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
};
export const Icons: Story = {
render: (args) => (
<div className="flex justify-center w-full">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton ariaLabel="add">
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" {...args}>
<DropdownMenuItem icon={<FontAwesomeIcon icon={faTrash} size="sm" />}>
Delete
</DropdownMenuItem>
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUndo} size="sm" />}>
Undo
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
};
export const WithDivider: Story = {
render: (args) => (
<div className="flex justify-center w-full">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton ariaLabel="add">
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" {...args}>
<DropdownMenuItem>Delete</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Undo</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Redo</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
};
export const Group: Story = {
render: (args) => (
<div className="flex justify-center w-full">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton ariaLabel="add">
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" {...args}>
<DropdownMenuGroup>
<DropdownMenuLabel>Group</DropdownMenuLabel>
<DropdownMenuItem>Undo</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuGroup>
<DropdownMenuLabel>Group#2</DropdownMenuLabel>
<DropdownMenuItem>Undo</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
};

@ -0,0 +1,97 @@
import { ComponentPropsWithRef, ElementType, forwardRef, ReactNode, Ref } from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { twMerge } from 'tailwind-merge';
// Main menu or parent container
export type DropdownMenuProps = DropdownMenuPrimitive.DropdownMenuProps;
export const DropdownMenu = DropdownMenuPrimitive.Root;
// trigger
export type DropdownMenuTriggerProps = DropdownMenuPrimitive.DropdownMenuTriggerProps;
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
// item container
export type DropdownMenuContentProps = DropdownMenuPrimitive.MenuContentProps;
export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>(
({ children, className, ...props }, forwardedRef) => {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
sideOffset={10}
{...props}
ref={forwardedRef}
className={twMerge(
'min-w-[220px] bg-bunker will-change-auto text-bunker-300 rounded-md shadow data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade',
className
)}
>
{children}
</DropdownMenuPrimitive.Content>
</DropdownMenuPrimitive.Portal>
);
}
);
DropdownMenuContent.displayName = 'DropdownMenuContent';
// item label component
export type DropdownLabelProps = DropdownMenuPrimitive.MenuLabelProps;
export const DropdownMenuLabel = ({ className, ...props }: DropdownLabelProps) => (
<DropdownMenuPrimitive.Label
{...props}
className={twMerge('text-xs text-bunker-400 px-4 pt-2 pb-1', className)}
/>
);
// dropdown items
export type DropdownMenuItemProps<T extends ElementType> =
DropdownMenuPrimitive.MenuContentProps & {
icon?: ReactNode;
as?: T;
inputRef?: Ref<T>;
};
export const DropdownMenuItem = <T extends ElementType = 'button'>({
children,
inputRef,
className,
icon,
as: Item = 'button',
...props
}: DropdownMenuItemProps<T> & ComponentPropsWithRef<T>) => (
<DropdownMenuPrimitive.Item
{...props}
className={twMerge(
'text-sm block font-inter px-4 py-2 data-[highlighted]:bg-gray-700 outline-none cursor-pointer',
className
)}
>
<Item type="button" role="menuitem" class="flex w-full items-center" ref={inputRef}>
{icon && <span className="flex items-center mr-2">{icon}</span>}
<span className="flex-grow text-left">{children}</span>
</Item>
</DropdownMenuPrimitive.Item>
);
// grouping items into 1
export type DropdownMenuGroupProps = DropdownMenuPrimitive.DropdownMenuGroupProps;
export const DropdownMenuGroup = forwardRef<HTMLDivElement, DropdownMenuGroupProps>(
({ ...props }, ref) => <DropdownMenuPrimitive.Group {...props} ref={ref} />
);
DropdownMenuGroup.displayName = 'DropdownMenuGroup';
// Divider
export const DropdownMenuSeparator = forwardRef<
HTMLDivElement,
DropdownMenuPrimitive.DropdownMenuSeparatorProps
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
{...props}
className={twMerge('h-[1px] bg-gray-700 m-1', className)}
/>
));
DropdownMenuSeparator.displayName = 'DropdownMenuSeperator';

@ -0,0 +1,17 @@
export type {
DropdownLabelProps,
DropdownMenuContentProps,
DropdownMenuGroupProps,
DropdownMenuItemProps,
DropdownMenuProps,
DropdownMenuTriggerProps
} from './Dropdown';
export {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from './Dropdown';

@ -0,0 +1,40 @@
import type { Meta, StoryObj } from '@storybook/react';
// Be careful on dep cycle
import { Input } from '../Input/Input';
import { FormControl } from './FormControl';
const meta: Meta<typeof FormControl> = {
title: 'Components/FormControl',
component: FormControl,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof FormControl>;
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Basic: Story = {
args: {
children: <Input />,
label: 'Email',
id: 'email',
helperText: 'Type something..'
}
};
export const RequiredInput: Story = {
args: {
...Basic.args,
isRequired: true
}
};
export const ErrorInput: Story = {
args: {
...Basic.args,
errorText: 'Some random error',
isError: true
}
};

@ -0,0 +1,72 @@
import { cloneElement, ReactNode } from 'react';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as Label from '@radix-ui/react-label';
import { twMerge } from 'tailwind-merge';
export type FormLabelProps = {
id?: string;
isRequired?: boolean;
label?: ReactNode;
};
export const FormLabel = ({ id, label, isRequired }: FormLabelProps) => (
<Label.Root className="text-mineshaft-300 text-sm font-medium block mb-1 ml-0.5" htmlFor={id}>
{label}
{isRequired && <span className="ml-1 text-red">*</span>}
</Label.Root>
);
export type FormHelperTextProps = {
isError?: boolean;
text?: ReactNode;
};
export const FormHelperText = ({ isError, text }: FormHelperTextProps) => (
<div
className={twMerge(
'text-xs font-inter flex items-center opacity-90 text-mineshaft-300 mt-2',
isError && 'text-red-600'
)}
>
{isError && (
<span>
<FontAwesomeIcon icon={faExclamationTriangle} size="sm" className="mr-1" />
</span>
)}
<span>{text}</span>
</div>
);
export type FormControlProps = {
id?: string;
isRequired?: boolean;
isError?: boolean;
label?: ReactNode;
helperText?: ReactNode;
errorText?: ReactNode;
children: JSX.Element;
};
export const FormControl = ({
children,
isRequired,
label,
helperText,
errorText,
id,
isError
}: FormControlProps): JSX.Element => {
return (
<div>
{typeof label === 'string' ? (
<FormLabel label={label} isRequired={isRequired} id={id} />
) : (
label
)}
{cloneElement(children, { isRequired, 'data-required': isRequired, isError })}
{!isError && helperText && <FormHelperText isError={isError} text={helperText} />}
{isError && errorText && <FormHelperText isError={isError} text={errorText} />}
</div>
);
};

@ -0,0 +1,2 @@
export type { FormControlProps, FormHelperTextProps, FormLabelProps } from './FormControl';
export { FormControl, FormHelperText, FormLabel } from './FormControl';

@ -0,0 +1,60 @@
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { Meta, StoryObj } from '@storybook/react';
import { IconButton } from './IconButton';
const meta: Meta<typeof IconButton> = {
title: 'Components/IconButton',
component: IconButton,
tags: ['v2'],
argTypes: {
isRounded: {
defaultValue: true,
type: 'boolean'
},
ariaLabel: {
defaultValue: 'Some buttons...'
}
}
};
export default meta;
type Story = StoryObj<typeof IconButton>;
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Primary: Story = {
args: {
children: <FontAwesomeIcon icon={faPlus} />
}
};
export const Secondary: Story = {
args: {
children: <FontAwesomeIcon icon={faPlus} />,
colorSchema: 'secondary',
variant: 'outline'
}
};
export const Danger: Story = {
args: {
children: <FontAwesomeIcon icon={faPlus} />,
colorSchema: 'danger',
variant: 'solid'
}
};
export const Plain: Story = {
args: {
children: <FontAwesomeIcon icon={faPlus} />,
variant: 'plain'
}
};
export const Disabled: Story = {
args: {
children: <FontAwesomeIcon icon={faPlus} />,
disabled: true
}
};

@ -0,0 +1,131 @@
import { ButtonHTMLAttributes, forwardRef, ReactNode } from 'react';
import { cva, VariantProps } from 'cva';
import { twMerge } from 'tailwind-merge';
type Props = {
children: ReactNode;
// This is kept as required because by accessibility convention and eslint
// when button doesn't have text an aria-label needs to be passed
ariaLabel: string;
isDisabled?: boolean;
};
const iconButtonVariants = cva(
[
'button',
'transition-all',
'font-inter font-medium',
'cursor-pointer',
'inline-flex items-center justify-center',
'relative'
],
{
variants: {
colorSchema: {
primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
danger: ['bg-red', 'text-white', 'border-red']
},
variant: {
solid: '',
outline: ['bg-transparent', 'border-2', 'border-solid'],
plain: ''
},
isDisabled: {
true: 'bg-opacity-70 cursor-not-allowed',
false: ''
},
isRounded: {
true: 'rounded-md',
false: ''
},
size: {
xs: ['text-xs', 'py-1', 'px-2'],
sm: ['text-sm', 'py-2', 'px-3'],
md: ['text-md', 'py-3', 'px-4'],
lg: ['text-lg', 'py-3', 'px-6']
}
},
compoundVariants: [
{
colorSchema: 'primary',
variant: 'outline',
className: 'text-primary hover:bg-primary hover:text-black'
},
{
colorSchema: 'secondary',
variant: 'outline',
className: 'hover:bg-mineshaft'
},
{
colorSchema: 'danger',
variant: 'outline',
className: 'text-red hover:bg-red hover:text-black'
},
{
colorSchema: 'primary',
variant: 'plain',
className: 'text-primary'
},
{
colorSchema: 'secondary',
variant: 'plain',
className: 'text-mineshaft'
},
{
colorSchema: 'danger',
variant: 'plain',
className: 'text-red'
},
{
colorSchema: ['danger', 'primary', 'secondary'],
variant: ['plain'],
className: 'bg-transparent py-1 px-1'
}
]
}
);
export type IconButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'aria-label'> &
VariantProps<typeof iconButtonVariants> &
Props;
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
(
{
children,
ariaLabel,
isDisabled = false,
className,
size = 'md',
variant = 'solid',
isRounded = true,
colorSchema = 'primary',
...props
},
ref
): JSX.Element => (
<button
ref={ref}
aria-disabled={isDisabled}
type="button"
aria-label={ariaLabel}
className={twMerge(
iconButtonVariants({
className,
colorSchema,
size,
variant,
isRounded,
isDisabled
})
)}
disabled={isDisabled}
{...props}
>
{children}
</button>
)
);
IconButton.displayName = 'IconButton';

@ -0,0 +1,2 @@
export type { IconButtonProps } from './IconButton';
export { IconButton } from './IconButton';

@ -0,0 +1,66 @@
import { faEye, faMailBulk } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { Meta, StoryObj } from '@storybook/react';
import { Input } from './Input';
const meta: Meta<typeof Input> = {
title: 'Components/Input',
component: Input,
tags: ['v2'],
argTypes: {
isRounded: {
defaultValue: true,
type: 'boolean'
},
placeholder: {
defaultValue: 'Type anything',
type: 'string'
}
}
};
export default meta;
type Story = StoryObj<typeof Input>;
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Filled: Story = {
args: {}
};
export const Outline: Story = {
args: {
variant: 'outline'
}
};
export const Plain: Story = {
args: {
variant: 'plain'
}
};
export const Error: Story = {
args: {
isError: true
}
};
export const AutoWidth: Story = {
args: {
isFullWidth: false,
className: 'w-auto'
}
};
export const RightIcon: Story = {
args: {
rightIcon: <FontAwesomeIcon icon={faEye} />
}
};
export const LeftIcon: Story = {
args: {
leftIcon: <FontAwesomeIcon icon={faMailBulk} />
}
};

@ -0,0 +1,102 @@
import { forwardRef, InputHTMLAttributes, ReactNode } from 'react';
import { cva, VariantProps } from 'cva';
import { twMerge } from 'tailwind-merge';
type Props = {
placeholder?: string;
isFullWidth?: boolean;
isRequired?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
};
const inputVariants = cva(
'input w-full py-2 text-gray-400 placeholder-gray-500 placeholder-opacity-50',
{
variants: {
size: {
xs: ['text-xs'],
sm: ['text-sm'],
md: ['text-md'],
lg: ['text-lg']
},
isRounded: {
true: ['rounded-md'],
false: ''
},
variant: {
filled: ['bg-bunker-800', 'text-gray-400'],
outline: ['bg-transparent'],
plain: 'bg-transparent outline-none'
},
isError: {
true: 'focus:ring-red/50 placeholder-red-300',
false: 'focus:ring-primary/50'
}
},
compoundVariants: []
}
);
const inputParentContainerVariants = cva('inline-flex font-inter items-center border relative', {
variants: {
isRounded: {
true: ['rounded-md'],
false: ''
},
isError: {
true: 'border-red',
false: 'border-mineshaft-400'
},
isFullWidth: {
true: 'w-full',
false: ''
},
variant: {
filled: ['bg-bunker-800', 'text-gray-400'],
outline: ['bg-transparent'],
plain: 'border-none'
}
}
});
export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> &
VariantProps<typeof inputVariants> &
Props;
export const Input = forwardRef<HTMLInputElement, InputProps>(
(
{
className,
isRounded = true,
isFullWidth = true,
isError = false,
isRequired,
leftIcon,
rightIcon,
variant = 'filled',
size = 'md',
...props
},
ref
): JSX.Element => {
return (
<div className={inputParentContainerVariants({ isRounded, isError, isFullWidth, variant })}>
{leftIcon && <span className="absolute left-0 ml-2">{leftIcon}</span>}
<input
{...props}
required={isRequired}
ref={ref}
className={twMerge(
leftIcon ? 'pl-9' : 'pl-4',
rightIcon ? 'pr-9' : 'pr-4',
inputVariants({ className, isError, size, isRounded, variant })
)}
/>
{rightIcon && <span className="absolute right-0 mr-2">{rightIcon}</span>}
</div>
);
}
);
Input.displayName = 'Input';

@ -0,0 +1,2 @@
export type { InputProps } from './Input';
export { Input } from './Input';

@ -0,0 +1,107 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { faKey, faUser } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// eslint-disable-next-line import/no-extraneous-dependencies
import { Meta, StoryObj } from '@storybook/react';
import { Menu, MenuGroup, MenuItem } from './Menu';
const meta: Meta<typeof Menu> = {
title: 'Components/Menu',
component: Menu,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof Menu>;
export const Basic: Story = {
render: (args) => (
<Menu {...args}>
<MenuItem>Secrets</MenuItem>
<MenuItem>Members</MenuItem>
<MenuItem>Integrations</MenuItem>
</Menu>
),
args: {}
};
export const SelectedItem: Story = {
render: (args) => (
<Menu {...args}>
<MenuItem isSelected>Secrets</MenuItem>
<MenuItem>Members</MenuItem>
<MenuItem>Integrations</MenuItem>
</Menu>
),
args: {}
};
export const GroupedItem: Story = {
render: (args) => (
<Menu {...args}>
<MenuGroup title="Group 1">
<MenuItem>Secrets</MenuItem>
<MenuItem>Members</MenuItem>
</MenuGroup>
<MenuGroup title="Group 2">
<MenuItem>Secrets</MenuItem>
<MenuItem>Members</MenuItem>
</MenuGroup>
</Menu>
),
args: {}
};
export const DisabledItem: Story = {
render: (args) => (
<Menu {...args}>
<MenuGroup title="Group 1">
<MenuItem isDisabled>Secrets</MenuItem>
<MenuItem>Members</MenuItem>
</MenuGroup>
<MenuGroup title="Group 2">
<MenuItem>Secrets</MenuItem>
<MenuItem>Members</MenuItem>
</MenuGroup>
</Menu>
),
args: {}
};
export const WithIcons: Story = {
render: (args) => (
<Menu {...args}>
<MenuGroup title="Group 1">
<MenuItem isDisabled icon={<FontAwesomeIcon icon={faKey} />}>
Secrets
</MenuItem>
<MenuItem icon={<FontAwesomeIcon icon={faUser} />}>Members</MenuItem>
</MenuGroup>
<MenuGroup title="Group 2">
<MenuItem icon={<FontAwesomeIcon icon={faKey} />}>Secrets</MenuItem>
<MenuItem icon={<FontAwesomeIcon icon={faKey} />}>Members</MenuItem>
</MenuGroup>
</Menu>
),
args: {}
};
export const WithDescription: Story = {
render: (args) => (
<Menu {...args}>
<MenuItem
isDisabled
icon={<FontAwesomeIcon icon={faKey} />}
description="Some random description"
>
Secrets
</MenuItem>
<MenuItem icon={<FontAwesomeIcon icon={faUser} />} description="Some random description">
Members
</MenuItem>
</Menu>
),
args: {}
};

@ -0,0 +1,64 @@
import { ComponentPropsWithRef, ElementType, ReactNode, Ref } from 'react';
import { twMerge } from 'tailwind-merge';
export type MenuProps = {
children: ReactNode;
className?: string;
};
export const Menu = ({ children, className }: MenuProps): JSX.Element => {
return <ul className={twMerge('p-2', className)}>{children}</ul>;
};
export type MenuItemProps<T extends ElementType> = {
// Kudos to https://itnext.io/react-polymorphic-components-with-typescript-f7ce72ea7af2
as?: T;
children: ReactNode;
icon?: ReactNode;
description?: ReactNode;
isDisabled?: boolean;
isSelected?: boolean;
className?: string;
inputRef?: Ref<T>;
};
export const MenuItem = <T extends ElementType = 'button'>({
children,
icon,
className,
isDisabled,
isSelected,
as: Item = 'button',
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => (
<li
className={twMerge(
'px-2 py-3 font-inter flex flex-col text-sm text-white transition-all rounded cursor-pointer hover:bg-gray-700',
isSelected && 'text-primary',
isDisabled && 'text-gray-500 hover:bg-transparent cursor-not-allowed',
className
)}
>
<Item type="button" role="menuitem" class="flex items-center" ref={inputRef}>
{icon && <span className="mr-2">{icon}</span>}
<span className="flex-grow text-left">{children}</span>
</Item>
{description && <span className="mt-2 text-xs">{description}</span>}
</li>
);
MenuItem.displayName = 'MenuItem';
export type MenuGroupProps = {
children: ReactNode;
title: ReactNode;
};
export const MenuGroup = ({ children, title }: MenuGroupProps): JSX.Element => (
<>
<li className="p-2 text-xs text-gray-400">{title}</li>
{children}
</>
);

@ -0,0 +1,2 @@
export type { MenuGroupProps, MenuItemProps, MenuProps } from './Menu';
export { Menu, MenuGroup, MenuItem } from './Menu';

@ -0,0 +1,33 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { Meta, StoryObj } from '@storybook/react';
import { Button } from '../Button';
import { Modal, ModalContent, ModalContentProps, ModalTrigger } from './Modal';
const meta: Meta<typeof Modal> = {
title: 'Components/Modal',
component: Modal,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof ModalContent>;
const Template = (args: ModalContentProps) => (
<Modal>
<ModalTrigger asChild>
<Button>Open</Button>
</ModalTrigger>
<ModalContent {...args}>Hello world</ModalContent>
</Modal>
);
export const Basic: Story = {
render: (args) => <Template {...args} />,
args: {
title: 'Title',
subTitle: 'Something as subtitle',
footerContent: 'footer content'
}
};

@ -0,0 +1,57 @@
import { forwardRef, ReactNode } from 'react';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { twMerge } from 'tailwind-merge';
import { Card, CardBody, CardFooter, CardTitle } from '../Card';
import { IconButton } from '../IconButton';
export type ModalContentProps = DialogPrimitive.DialogContentProps & {
title?: ReactNode;
subTitle?: string;
footerContent?: ReactNode;
};
export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
({ children, title, subTitle, className, footerContent, ...props }, forwardedRef) => (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
className="fixed inset-0 w-full h-full animate-fadeIn"
style={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }}
/>
<DialogPrimitive.Content {...props} ref={forwardedRef}>
<Card
isRounded
className={twMerge(
'fixed max-w-md animate-popIn top-1/2 left-1/2 -translate-y-2/4 -translate-x-2/4',
className
)}
>
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
<CardBody>{children}</CardBody>
{footerContent && <CardFooter>{footerContent}</CardFooter>}
<DialogPrimitive.Close aria-label="Close" asChild>
<IconButton
variant="plain"
ariaLabel="close"
className="absolute top-2.5 right-2.5 text-white hover:bg-gray-600 rounded"
>
<FontAwesomeIcon icon={faTimes} size="sm" className="cursor-pointer" />
</IconButton>
</DialogPrimitive.Close>
</Card>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
)
);
ModalContent.displayName = 'ModalContent';
export type ModalProps = Omit<DialogPrimitive.DialogProps, 'open'> & { isOpen?: boolean };
export const Modal = ({ isOpen, ...props }: ModalProps) => (
<DialogPrimitive.Root open={isOpen} {...props} />
);
export const ModalTrigger = DialogPrimitive.Trigger;
export type ModalTriggerProps = DialogPrimitive.DialogTriggerProps;

@ -0,0 +1,2 @@
export type { ModalContentProps, ModalProps, ModalTriggerProps } from './Modal';
export { Modal, ModalContent, ModalTrigger } from './Modal';

@ -0,0 +1,113 @@
import { useEffect, useState } from 'react';
import { SelectProps } from '@radix-ui/react-select';
// eslint-disable-next-line import/no-extraneous-dependencies
import { Meta, StoryObj } from '@storybook/react';
import { Select, SelectItem } from './Select';
const meta: Meta<typeof Select> = {
title: 'Components/Select',
component: Select,
tags: ['v2'],
argTypes: {
placeholder: {
defaultValue: 'Type something...'
}
}
};
export default meta;
type Story = StoryObj<typeof Select>;
export const Basic: Story = {
render: (args) => (
<div className="">
<Select placeholder="Type anything..." className="w-72" {...args}>
<SelectItem value="1">John</SelectItem>
<SelectItem value="2">Peter</SelectItem>
<SelectItem value="3">Suzy</SelectItem>
</Select>
</div>
)
};
const Controlled = (args: SelectProps) => {
const [isOpen, setIsOpen] = useState(false);
const [selected, setSelected] = useState('');
return (
<div className="">
<Select
defaultValue="1"
className="w-72"
open={isOpen}
onValueChange={(val) => setSelected(val)}
value={selected}
onOpenChange={(open) => setIsOpen(open)}
{...args}
>
<SelectItem value="1">John</SelectItem>
<SelectItem value="2">Peter</SelectItem>
<SelectItem value="3">Suzy</SelectItem>
</Select>
</div>
);
};
export const Control: Story = {
render: (args) => <Controlled {...args} />
};
export const Disabled: Story = {
render: (args) => (
<div className="">
<Select defaultValue="1" className="w-72" {...args}>
<SelectItem value="1">John</SelectItem>
<SelectItem value="2" isDisabled>
Peter
</SelectItem>
<SelectItem value="3">Suzy</SelectItem>
</Select>
</div>
)
};
export const Loading: Story = {
render: (args) => (
<div className="">
<Select defaultValue="1" className="w-72" isLoading {...args}>
<SelectItem value="1">John</SelectItem>
<SelectItem value="2">Peter</SelectItem>
<SelectItem value="3">Suzy</SelectItem>
</Select>
</div>
)
};
const AsyncSelectOptions = () => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// eslint-disable-next-line no-new
new Promise<void>((resolve): void => {
setTimeout(() => {
setIsLoading(false);
resolve();
}, 1000);
});
}, []);
return (
<div className="">
<Select placeholder="Hello" className="w-72" isLoading={isLoading}>
<SelectItem value="1">John</SelectItem>
<SelectItem value="2">Peter</SelectItem>
<SelectItem value="3">Suzy</SelectItem>
</Select>
</div>
);
};
export const Async: Story = {
render: (args) => <AsyncSelectOptions {...args} />
};

@ -0,0 +1,96 @@
import { forwardRef, ReactNode } from 'react';
import { faCheck, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as SelectPrimitive from '@radix-ui/react-select';
import { twMerge } from 'tailwind-merge';
import { Spinner } from '../Spinner';
type Props = {
children: ReactNode;
placeholder?: string;
className?: string;
isLoading?: boolean;
};
export type SelectProps = SelectPrimitive.SelectProps & Props;
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
({ children, placeholder, className, isLoading, ...props }, ref): JSX.Element => {
return (
<SelectPrimitive.Root {...props}>
<SelectPrimitive.Trigger
ref={ref}
className={twMerge(
`inline-flex items-center justify-between data-[placeholder]:text-gray-500
px-4 py-2.5 font-inter text-sm text-white rounded-md bg-mineshaft-800`,
className
)}
>
<SelectPrimitive.Value placeholder={placeholder} />
<SelectPrimitive.Icon className="ml-3">
<FontAwesomeIcon icon={faChevronDown} size="sm" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal>
<SelectPrimitive.Content
position="popper"
sideOffset={5}
className="overflow-hidden text-white rounded-md shadow-md font-inter bg-mineshaft-800"
style={{ width: 'var(--radix-select-trigger-width)' }}
>
<SelectPrimitive.ScrollUpButton>
<FontAwesomeIcon icon={faChevronUp} size="sm" />
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport className="p-2">
{isLoading ? (
<div className="flex items-center justify-center">
<Spinner size="xs" />
<span className="ml-2 text-xs text-gray-500">Loading...</span>
</div>
) : (
children
)}
</SelectPrimitive.Viewport>
<SelectPrimitive.ScrollDownButton>
<FontAwesomeIcon icon={faChevronDown} size="sm" />
</SelectPrimitive.ScrollDownButton>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
</SelectPrimitive.Root>
);
}
);
Select.displayName = 'Select';
export type SelectItemProps = Omit<SelectPrimitive.SelectItemProps, 'disabled'> & {
isDisabled?: boolean;
isSelected?: boolean;
};
export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
({ children, className, isSelected, isDisabled, ...props }, forwardedRef) => {
return (
<SelectPrimitive.Item
{...props}
className={twMerge(
`text-sm rounded-sm transition-all hover:text-primary
hover:bg-mineshaft-700 flex items-center pl-10 pr-4 py-2 cursor-pointer
select-none outline-none relative`,
isSelected && 'text-primary',
isDisabled && 'text-gray-600 hover:bg-transparent cursor-not-allowed hover:text-gray-600',
className
)}
ref={forwardedRef}
>
<SelectPrimitive.ItemIndicator className="absolute left-2">
<FontAwesomeIcon icon={faCheck} size="sm" />
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
);
SelectItem.displayName = 'SelectItem';

@ -0,0 +1,2 @@
export type { SelectItemProps, SelectProps } from './Select';
export { Select, SelectItem } from './Select';

@ -0,0 +1,42 @@
import { twMerge } from 'tailwind-merge';
type SizeOptions = 'xs' | 'sm' | 'md' | 'lg';
type Props = {
className?: string;
size?: SizeOptions;
};
const sizeChart: Record<SizeOptions, string> = {
xs: 'w-4 h-4',
sm: 'w-6 h-6',
md: 'w-8 h-8',
lg: 'w-12 h-12'
};
export const Spinner = ({ className, size = 'md' }: Props): JSX.Element => {
return (
<div role="status">
<svg
aria-hidden="true"
className={twMerge(
' text-gray-200 animate-spin dark:text-gray-600 fill-primary m-1',
sizeChart[size],
className
)}
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>
</div>
);
};

@ -0,0 +1 @@
export { Spinner } from './Spinner';

@ -0,0 +1,34 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Switch } from './Switch';
const meta: Meta<typeof Switch> = {
title: 'Components/Switch',
component: Switch,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof Switch>;
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Simple: Story = {
args: {
children: 'Dark mode'
}
};
export const Disabled: Story = {
args: {
children: 'Dark mode',
isDisabled: true
}
};
export const Required: Story = {
args: {
children: 'Dark mode',
isRequired: true
}
};

@ -0,0 +1,42 @@
import { ReactNode } from 'react';
import * as SwitchPrimitive from '@radix-ui/react-switch';
import { twMerge } from 'tailwind-merge';
export type SwitchProps = Omit<SwitchPrimitive.SwitchProps, 'checked' | 'disabled' | 'required'> & {
children: ReactNode;
id: string;
isChecked?: boolean;
isRequired?: boolean;
isDisabled?: boolean;
};
export const Switch = ({
children,
id,
className,
isChecked,
isDisabled,
isRequired,
...props
}: SwitchProps): JSX.Element => (
<div className="flex items-center text-bunker-300 font-inter">
<label className="text-sm" htmlFor={id}>
{children}
{isRequired && <span className="pl-1 text-red">*</span>}
</label>
<SwitchPrimitive.Root
{...props}
required={isRequired}
checked={isChecked}
disabled={isDisabled}
className={twMerge(
'h-5 ml-3 transition-all rounded-full w-9 bg-bunker-300 data-[state=checked]:bg-bunker-200',
isDisabled && 'bg-bunker-400 hover:bg-bunker-400',
className
)}
id={id}
>
<SwitchPrimitive.Thumb className="w-4 h-4 border-none will-change-transform rounded-full shadow bg-black block transition-all translate-x-0.5 data-[state=checked]:translate-x-[18px]" />
</SwitchPrimitive.Root>
</div>
);

@ -0,0 +1,2 @@
export type { SwitchProps } from './Switch';
export { Switch } from './Switch';

@ -0,0 +1,41 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Table, TableContainer, TBody, Td, Th, THead, Tr } from './Table';
const meta: Meta<typeof Table> = {
title: 'Components/Table',
component: Table,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof Table>;
export const Basic: Story = {
render: (args) => (
<TableContainer>
<Table {...args}>
<THead>
<Tr>
<Th>Head#1</Th>
<Th>Head#2</Th>
<Th>Head#3</Th>
</Tr>
</THead>
<TBody>
<Tr>
<Td>Row#1</Td>
<Td>Row#2</Td>
<Td>Row#3</Td>
</Tr>
<Tr>
<Td>Row#1</Td>
<Td>Row#2</Td>
<Td>Row#3</Td>
</Tr>
</TBody>
</Table>
</TableContainer>
)
};

@ -0,0 +1,93 @@
import { ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';
export type TableContainerProps = {
children: ReactNode;
isRounded?: boolean;
className?: string;
};
export const TableContainer = ({
children,
className,
isRounded = true
}: TableContainerProps): JSX.Element => (
<div
className={twMerge(
'overflow-x-auto font-inter shadow-md relative border border-solid border-mineshaft-700',
isRounded && 'rounded-md',
className
)}
>
{children}
</div>
);
// main parent table
export type TableProps = {
className?: string;
children: ReactNode;
};
export const Table = ({ children, className }: TableProps): JSX.Element => (
<table
className={twMerge(
'bg-bunker-800 p-2 roun text-gray-300 w-full text-sm text-left rounded-md',
className
)}
>
{children}
</table>
);
// table head
export type THeadProps = {
children: ReactNode;
className?: string;
};
export const THead = ({ children, className }: THeadProps): JSX.Element => (
<thead className={twMerge('text-xs bg-bunker text-bunker-300 uppercase', className)}>
{children}
</thead>
);
// table rows
export type TrProps = {
children: ReactNode;
className?: string;
};
export const Tr = ({ children, className }: TrProps): JSX.Element => (
<tr className={twMerge('border border-solid border-mineshaft-700', className)}>{children}</tr>
);
// table head columns
export type ThProps = {
children: ReactNode;
className?: string;
};
export const Th = ({ children, className }: ThProps): JSX.Element => (
<th className={twMerge('px-6 py-3 font-medium', className)}>{children}</th>
);
// table body
export type TBodyProps = {
children: ReactNode;
className?: string;
};
export const TBody = ({ children, className }: TBodyProps): JSX.Element => (
<tbody className={twMerge(className)}>{children}</tbody>
);
// table body columns
export type TdProps = {
children: ReactNode;
className?: string;
};
export const Td = ({ children, className }: TdProps): JSX.Element => (
<td className={twMerge('text-left px-6 py-3', className)}>{children}</td>
);

@ -0,0 +1,10 @@
export type {
TableContainerProps,
TableProps,
TBodyProps,
TdProps,
THeadProps,
ThProps,
TrProps
} from './Table';
export { Table, TableContainer, TBody, Td, Th, THead, Tr } from './Table';

@ -0,0 +1,48 @@
import type { Meta, StoryObj } from '@storybook/react';
import { TextArea } from './TextArea';
const meta: Meta<typeof TextArea> = {
title: 'Components/TextArea',
component: TextArea,
tags: ['v2'],
argTypes: {
placeholder: {
defaultValue: 'Type anything',
type: 'string'
}
}
};
export default meta;
type Story = StoryObj<typeof TextArea>;
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Filled: Story = {
args: {}
};
export const Outline: Story = {
args: {
variant: 'outline'
}
};
export const Plain: Story = {
args: {
variant: 'plain'
}
};
export const Error: Story = {
args: {
isError: true
}
};
export const AutoWidth: Story = {
args: {
isFullWidth: false,
className: 'w-auto'
}
};

@ -0,0 +1,74 @@
import { forwardRef, TextareaHTMLAttributes } from 'react';
import { cva, VariantProps } from 'cva';
import { twMerge } from 'tailwind-merge';
type Props = {
placeholder?: string;
isFullWidth?: boolean;
isRequired?: boolean;
reSize?: 'none' | 'both' | 'vertical' | 'horizontal';
};
const textAreaVariants = cva(
'textarea w-full p-2 border border-solid text-gray-400 font-inter placeholder-gray-500 placeholder-opacity-50',
{
variants: {
size: {
xs: ['text-xs'],
sm: ['text-sm'],
md: ['text-md'],
lg: ['text-lg']
},
isRounded: {
true: ['rounded-md'],
false: ''
},
variant: {
filled: ['bg-bunker-800', 'text-gray-400'],
outline: ['bg-transparent'],
plain: 'bg-transparent outline-none'
},
isError: {
true: 'focus:ring-red/50 placeholder-red-300 border-red',
false: 'focus:ring-primary/50 border-mineshaft-400'
}
},
compoundVariants: [
{
variant: 'plain',
isError: [true, false],
className: 'border-none'
}
]
}
);
export type TextAreaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'size'> &
VariantProps<typeof textAreaVariants> &
Props;
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
(
{
className,
isRounded = true,
isError = false,
isRequired,
variant = 'filled',
size = 'md',
reSize = 'both',
...props
},
ref
): JSX.Element => (
<textarea
{...props}
style={{ resize: reSize }}
required={isRequired}
ref={ref}
className={twMerge(textAreaVariants({ className, isError, size, isRounded, variant }))}
/>
)
);
TextArea.displayName = 'TextArea';

@ -0,0 +1,2 @@
export type { TextAreaProps } from './TextArea';
export { TextArea } from './TextArea';

@ -0,0 +1,14 @@
export * from './Button';
export * from './Card';
export * from './Checkbox';
export * from './Dropdown';
export * from './FormControl';
export * from './IconButton';
export * from './Input';
export * from './Menu';
export * from './Modal';
export * from './Select';
export * from './Spinner';
export * from './Switch';
export * from './Table';
export * from './TextArea';

@ -1,5 +1,4 @@
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { faSlack } from '@fortawesome/free-brands-svg-icons';
@ -40,47 +39,45 @@ const learningItem = ({
}: ItemProps): JSX.Element => {
if (link) {
return (
<Link href={link}>
<a
target={`${link.includes('https') ? '_blank' : '_self'}`}
rel="noopener noreferrer"
className="w-full"
href="#"
<a
target={`${link.includes('https') ? '_blank' : '_self'}`}
rel="noopener noreferrer"
className="w-full"
href={link}
>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={async () => {
if (userAction && userAction !== 'first_time_secrets_pushed') {
await registerUserAction({
action: userAction
});
}
}}
className="relative bg-bunker-700 hover:bg-bunker-500 shadow-xl duration-200 rounded-md border border-dashed border-bunker-400 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden my-1.5 cursor-pointer"
>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={async () => {
if (userAction && userAction !== 'first_time_secrets_pushed') {
await registerUserAction({
action: userAction
});
}
}}
className="relative bg-bunker-700 hover:bg-bunker-500 shadow-xl duration-200 rounded-md border border-dashed border-bunker-400 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden my-1.5 cursor-pointer"
>
<div className="flex flex-row items-center mr-4">
<FontAwesomeIcon icon={icon} className="text-4xl mx-2 w-16" />
{complete && (
<div className="bg-bunker-700 w-7 h-7 rounded-full absolute left-12 top-10 p-2 flex items-center justify-center">
<FontAwesomeIcon icon={faCheckCircle} className="text-4xl w-5 h-5 text-green" />
</div>
)}
<div className="flex flex-col items-start">
<div className="text-xl font-semibold mt-0.5">{text}</div>
<div className="text-sm font-normal">{subText}</div>
<div className="flex flex-row items-center mr-4">
<FontAwesomeIcon icon={icon} className="text-4xl mx-2 w-16" />
{complete && (
<div className="bg-bunker-700 w-7 h-7 rounded-full absolute left-12 top-10 p-2 flex items-center justify-center">
<FontAwesomeIcon icon={faCheckCircle} className="text-4xl w-5 h-5 text-green" />
</div>
)}
<div className="flex flex-col items-start">
<div className="text-xl font-semibold mt-0.5">{text}</div>
<div className="text-sm font-normal">{subText}</div>
</div>
<div
className={`pr-4 font-semibold text-sm w-28 text-right ${complete && 'text-green'}`}
>
{complete ? 'Complete!' : `About ${time}`}
</div>
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
</div>
</a>
</Link>
<div
className={`pr-4 font-semibold text-sm w-28 text-right ${complete && 'text-green'}`}
>
{complete ? 'Complete!' : `About ${time}`}
</div>
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
</div>
</a>
);
}
return (
@ -157,7 +154,7 @@ export default function Home() {
icon: faHandPeace,
time: '3 min',
userAction: 'intro_cta_clicked',
link: 'https://www.youtube.com/watch?v=JS3OKYU2078'
link: 'https://www.youtube.com/watch?v=3F7FNYX94zA'
})}
{learningItem({
text: 'Add your secrets',

@ -183,6 +183,10 @@ export default function Integrations() {
accessToken?: string;
}) => {
try {
if (!bot.isActive) {
await handleBotActivate();
}
if (integrationOption.type === 'oauth') {
// integration is of type OAuth
@ -233,7 +237,7 @@ export default function Integrations() {
return;
}
} catch (err) {
console.log(err);
console.error(err);
}
};
@ -247,6 +251,7 @@ export default function Integrations() {
* @returns
*/
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
// consider: don't start integration until at [handleIntegrationOption] step
try {
const integrationAuthX = integrationAuths.find((integrationAuth) => integrationAuth.integration === integrationOption.slug);
@ -264,12 +269,15 @@ export default function Integrations() {
return;
}
if (!bot.isActive) {
await handleBotActivate();
}
// case: integration has been authorized before
// -> create new integration
const integration = await createIntegration({
integrationAuthId: integrationAuthX._id
});
setIntegrations([...integrations, integration]);
} catch (err) {
console.error(err);
@ -350,7 +358,6 @@ export default function Integrations() {
isOpen={isActivateBotDialogOpen}
closeModal={() => setIsActivateBotDialogOpen(false)}
selectedIntegrationOption={selectedIntegrationOption}
handleBotActivate={handleBotActivate}
integrationOptionPress={integrationOptionPress}
/>
<IntegrationAccessTokenDialog
@ -358,7 +365,6 @@ export default function Integrations() {
closeModal={() => setIntegrationAccessTokenDialogOpen(false)}
selectedIntegrationOption={selectedIntegrationOption}
handleIntegrationOption={handleIntegrationOption}
/>
<IntegrationSection
integrations={integrations}

@ -1,3 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import '@fontsource/inter/400.css';
@import '@fontsource/inter/500.css';
@import '@fontsource/inter/700.css';

File diff suppressed because it is too large Load Diff