Compare commits

...
Author SHA1 Message Date
Eric Lewis af1137b3c3 chore(deployment): 0.1.8 2024-02-29 00:47:19 -07:00
Eric Lewisandnabarun a7810a34c9 feat: one click deployer (#139)
* feat: one click deployer

This adds a script for deploying whatever is the latest on main, essentially automating the changes specified in the readme.

* fix

* repurpose existing script

* clean up

* fix

* fix

* Update latest record version

---------

Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
2024-02-29 13:11:01 +05:30
Vivian Phung ff9b08e91d Merge pull request #143 from snowball-tools/template-card-redirect-bug
Template card redirect bug
2024-02-29 00:38:12 -07:00
nabarun 94a9bf88a9 Remove check for .env in build script (#144) 2024-02-29 12:51:32 +05:30
Vivian Phung dd5f93050d fix navigate 2024-02-29 00:01:56 -07:00
Vivian Phung 5946755749 fix navigate 2024-02-28 23:52:37 -07:00
Vivian Phung 4532a962d8 fix navigate 2024-02-28 23:44:10 -07:00
Sushan Yadav 6a108c1a1b Merge pull request #137 from snowball-tools/sushan/T-4914-project-overview-layout
Project overview layout
2024-02-29 09:57:12 +05:45
Sushan Yadav 0fb378386a chore: update comment 2024-02-28 20:51:58 +05:45
Sushan Yadav 563059e887 chore: comment fixes - tab issues and hardcoded strings 2024-02-28 20:46:08 +05:45
Sushan Yadav f34eb48d1e feat: add layout 2024-02-28 17:35:02 +05:45
Sushan Yadav acf5aab26f Merge branch 'main' into sushan/T-4914-project-overview-layout 2024-02-28 16:57:32 +05:45
Sushan Yadav 6b6582c287 feat: top section 2024-02-28 16:56:28 +05:45
Sushan Yadav b43ee3b7bb feat: add deplyment infos 2024-02-28 16:39:26 +05:45
Wahyu Kurniawan b1bf47d104 [T-4921: feat] Update project card component (#136)
* 🎨 style: add hover interaction to the card

* ️ feat: make the whole card clickable

* 🎨 style: adjust hovered background for wavy border

* ♻️ refactor: move wavy border class to project card theme

* 🎨 style: add transition when hover

* 📝 docs: add todo to experiment using `Link` componnt
2024-02-28 16:50:55 +07:00
Wahyu KurniawanandZachery Ng 8ee61c0c85 [T-4910: feat] Re-styling dashboard, create project layout, project template card (#135)
* 🎨 style: adjust wavy border and add layout wavy border

* ♻️ refactor: change sidebar to use `nav`

* ♻️ refactor: org slug dashboard layout

* ♻️ refactor: create project layout and restyling it

* ♻️ refactor: remove unused style

* ️ feat: restyling template card

* ️ feat: create template icon component

* ️ feat: use `h2` for layout title

* ️ feat: Add isComingSoon property to templates and handle click event in TemplateCard component

* ♻️ refactor: WavyBorder component and update CreateProjectLayout

* 🎨 style: update button medium size padding

* 🎨 style: update layout shadow and add new shadow for the template card

* ️ feat: add wavy border gradient and line svg assets

* refactor: update wavy border svg

* 🎨 style: adjust template card name and arrow also responsive of the list of template

---------

Co-authored-by: Zachery Ng <zachery.ng@gmail.com>
2024-02-28 16:22:54 +07:00
Andre Hadianto 33aa75e341 Merge pull request #133 from snowball-tools/andrehadianto/T-4906-create-project-connect-github
[T-4906] create project connect GitHub
2024-02-28 15:55:59 +08:00
Sushan Yadav a238610522 Reskin Project Overview - Activity section (#134)
* feat: reskin w/ new design system

* feat: add sepeator calc in comment

* chore: limit commit message to 4 line

* comment fixes

* chore: use Heading component
2024-02-28 13:31:41 +05:45
Zachery Ng bd4a51c830 style: update tertiary button 2024-02-28 15:45:57 +08:00
Andre H 4a87c100d6 🔧 chore: use proper type 2024-02-28 14:20:12 +07:00
Andre H 3493d735b9 🔧 chore: replace bgClass with className 2024-02-28 14:19:02 +07:00
Andre H a324d32ebf 🔧 chore: use Heading 2024-02-28 13:51:35 +07:00
Andre H 4519494be8 ♻️ refactor: cherry pick fomr main 2024-02-28 13:49:11 +07:00
Andre H 319822c8e6 🔧 chore: add shadow to button 2024-02-28 13:46:09 +07:00
Andre H c5273ff530 ♻️ refactor: use IconWithFrame 2024-02-28 13:39:50 +07:00
Andre H 2585418ed6 🔧 chore: implement IconWithFrame common component 2024-02-28 13:39:30 +07:00
Wahyu Kurniawan 62734308fc [T-4907: style] Re-styling home page & sidebar component (#132)
* ️ feat: create heading component

* ♻️ refactor: move sidebar inside shared components

* ️ feat: add polymorphic prop type for heading component

* 🎨 style: re-styling project list page

* ♻️ refactor: remove `.env`

* 🎨 style: set default font weight to normal for heading component
2024-02-28 12:50:30 +07:00
Andre H 89306ddcc7 🔧 chore: add toast 2024-02-28 11:19:09 +07:00
Andre H 3b67396c43 ️ feat: reskin connect github 2024-02-28 10:55:10 +07:00
Andre H 3535bd3a58 🔧 chore: add GitTeaIcon 2024-02-28 10:53:05 +07:00
Wahyu Kurniawan 282001c317 [T-4903: chore] Add inter display font (#127)
* 🔧 chore: add `InterDisplay` font families

* 🎨 style: add font `InterDisplay` to tailwind

* 🔧 chore: update `yarn.lock`

* ♻️ refactor: change `InterDisplay` to `inter Display` and `inter-display` to `display
2024-02-28 08:47:43 +07:00
Eric Lewis 2f62d086de Merge pull request #131 from snowball-tools/ericlewis/deploy-2
chore(deployment): 1.1.3
2024-02-27 12:55:54 -05:00
Eric Lewis 4c544e8d15 chore: readme 2024-02-27 12:30:35 -05:00
Eric Lewis a02ee13f9d chore: deploy readme 2024-02-27 12:29:27 -05:00
Eric Lewis e32a51a771 chore(deployment): 1.1.3 2024-02-27 12:25:20 -05:00
Eric Lewis 513ca69d01 Merge pull request #130 from snowball-tools/ericlewis/pretty-login
feat: pretty login
2024-02-27 12:08:40 -05:00
Eric Lewis e34b8d6a2e Merge pull request #129 from snowball-tools/ericlewis/deploy
chore: create new deployment
2024-02-27 12:07:43 -05:00
Eric Lewis f4d5b77784 fix deploy 2024-02-27 11:56:22 -05:00
Eric Lewis 399b80ce3b chore: create new deployment 2024-02-27 11:30:39 -05:00
Eric Lewis fdaec2da94 feat: pretty login 2024-02-27 11:13:14 -05:00
Vivian Phung 13fc92bf0e Merge pull request #128 from snowball-tools/ericlewis/prettify-select 2024-02-27 10:44:59 -05:00
Eric Lewis 8a8ee1d6ae fix centering 2024-02-27 10:40:54 -05:00
Vivian Phung 5680961964 Merge pull request #122 from snowball-tools/repo 2024-02-27 09:18:20 -05:00
Vivian Phung aa0ca8dd21 Merge pull request #121 from snowball-tools/search-area-plus 2024-02-27 09:18:03 -05:00
Wahyu Kurniawan 8280f4c7f7 [T-4902: feat] Update project card (#126)
* ️ feat: create wavy border component

* ️ feat: create project card component

* ️ feat: add new size in avatar

* 🎨 style: add default border radius when the shape is default on the button

* ️ feat: create some icons for project card component

* 🔧 chore: install and setup jetbrains mono font family

* ️ feat: create `getInitials` util function

* 🎨 style: add jetbrains mono font to the tailwind config

* ️ feat: add warning error icon

* ♻️ refactor: change `jetbrains-mono` to `mono`
2024-02-27 21:16:38 +07:00
nabarun ab72bbb03f Update template repo links (#124) 2024-02-27 14:16:36 +05:30
nabarun cc4e5fd627 Add deployer package for frontend app (#123)
* Add script and records for deploying frontend app

* Update readme with steps for new deployment

* Add troubleshooting section

* Move config env update

* Add step to commit updated records

* Bump record version
2024-02-27 11:52:05 +05:30
Vivian Phung 195471b888 Merge branch 'main' into repo 2024-02-27 00:07:22 -05:00
Wahyu Kurniawan c731dd308c [T-4840: feat] Dropdown/select component (#108)
* 🎨 style: adjust z index of the date picker popover

* 🎨 style: add new spacing and rename box shadow from calendar to dropdown

* 🐛 fix: console error becasue button inside button

* ♻️ refactor: rename shadow calendar to shador dropdown on calendar component

* 🚀 perf: remove vscode settings inside `packages/frontend`

* ️ feat: create check radio icon and chevron down icon component

* 🔧 chore: install `downshift`

* ️ feat: create select component

* 🎨 style: adjust the popover position based on the user screen

* ️ feat: separate select item to be a component

* ️ feat: separate select value to be a component

* ♻️ refactor: adjust style and refactor to a new component

* ️ feat: create a type for merge two interface but keep the last value

* 🐛 fix: forward ref the component to fix console error

* ️ feat: add `hideValues` prop to hide the values when on multiple

* 🐛 fix: no result not showing

* ️ feat: make the select to be controller component

* ♻️ refactor: remove console log

* ♻️ refactor: update pr review
2024-02-27 12:05:16 +07:00
Vivian Phung 8ce531274d center items in nav bar of git repos 2024-02-27 00:00:52 -05:00
Vivian Phung 6c38094093 github icon in search repo 2024-02-26 23:56:20 -05:00
Vivian Phung 95285d30dd private repo + repo 2024-02-26 23:50:42 -05:00
Vivian Phung 61c8846400 org slug 2024-02-26 23:24:19 -05:00
Eric Lewis d73c3c7daf fix: select issue 2024-02-26 22:57:05 -05:00
124 changed files with 3765 additions and 618 deletions
+1 -1
View File
@@ -4,4 +4,4 @@ yarn-error.log
.yarn/
.yarnrc
packages/backend/environments/local.toml
packages/backend/environments/local.toml
-8
View File
@@ -9,14 +9,6 @@ if [[ -d "$DEST_DIR" ]]; then
exit 1
fi
if [[ -f "$PKG_DIR/.env" ]]; then
echo "Using existing .env file"
else
mv "$PKG_DIR/.env.example" "$PKG_DIR/.env"
echo "Created .env file. Please populate with the correct values."
exit 1
fi
cat > $PKG_DIR/.env <<EOF
REACT_APP_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_server_url'
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
+99
View File
@@ -0,0 +1,99 @@
# deployer
- Install dependencies
```bash
yarn
```
```bash
brew install jq # if you do not have jq installed already
```
Example of how to make the necessary deploy edits [here](https://github.com/snowball-tools/snowballtools-base/pull/131/files).
- Replace variables in the following files
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
- update the name & application version numbers
- `<CURRENT_DATE_TIME>`: Replace with current time which can be generated by command `date -u`
```yml
# Example
record:
...
meta:
note: Added by Snowball @ Friday 23 February 2024 06:35:50 AM UTC
...
```
- Update record version in [records/application-record.yml](records/application-record.yml)
```yml
record:
type: ApplicationRecord
version: <NEW_VERSION>
...
```
- Update commit hash in the following places:
- [records/application-record.yml](records/application-record.yml)
```yml
record:
...
repository_ref: <COMMIT_HASH>
...
```
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
```yml
record:
...
meta:
...
repository_ref: <COMMIT_HASH>
```
- [deploy-frontend.sh](deploy-frontend.sh)
Also be sure to update the app version
```bash
...
RCD_APP_VERSION="<NEW_VERSION>"
REPO_REF="<COMMIT_HASH>"
...
```
- Run script to deploy app
```
./deploy-frontend.sh
```
- Commit the updated [ApplicationRecord](records/application-record.yml) and [ApplicationDeploymentRequest](records/application-deployment-request.yml) files to the repository
## Notes
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
```yml
record:
...
config:
env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
...
```
- On changing `LACONIC_HOSTED_CONFIG_app_github_clientid`, the GitHub client ID and secret need to be changed in backend config too
## Troubleshoot
- Check deployment status [here](https://console.laconic.com/deployer).
- Check records [here](https://console.laconic.com/#/registry).
- If deployment fails due to low bond balance
- Check balances
```bash
# Account balance
yarn laconic cns account get
# Bond balance
yarn laconic cns bond get --id 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
```
- Command to refill bond
```bash
yarn laconic cns bond refill --id 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac --type aphoton --quantity 10000000
```
+9
View File
@@ -0,0 +1,9 @@
services:
cns:
restEndpoint: http://console.laconic.com:1317
gqlEndpoint: http://console.laconic.com:9473/api
chainId: laconic_9000-1
gas: 1200000
fees: 200000aphoton
userKey: 0524fc22ea0a12e6c5cc4cfe08e73c95dffd0ab5ed72a59f459ed33134fa3b16
bondId: 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
# Repository URL
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
# Get the latest commit hash from the repository
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
# Extract version from ../frontend/package.json
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
# Current date and time for note
CURRENT_DATE_TIME=$(date -u)
# Increment application-record version
APPLICATION_RECORD_FILE="./records/application-record.yml"
if [ -f "$APPLICATION_RECORD_FILE" ]; then
# Extract current version and increment it
CURRENT_VERSION=$(grep 'version:' $APPLICATION_RECORD_FILE | head -1 | awk '{print $2}')
IFS='.' read -ra ADDR <<< "$CURRENT_VERSION"
VERSION_NUMBER=${ADDR[2]}
NEW_VERSION_NUMBER=$((VERSION_NUMBER + 1))
NEW_APPLICATION_VERSION="${ADDR[0]}.${ADDR[1]}.$NEW_VERSION_NUMBER"
else
# If file does not exist, start from version 0.0.1
NEW_APPLICATION_VERSION="0.0.1"
fi
# Generate application-deployment-request.yml
cat > ./records/application-deployment-request.yml <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: snowballtools-base-frontend@$PACKAGE_VERSION
application: crn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
dns: dashboard
config:
env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta:
note: Added by Snowball @ $CURRENT_DATE_TIME
repository: "$REPO_URL"
repository_ref: $LATEST_HASH
EOF
# Generate application-record.yml with incremented version
cat > ./records/application-record.yml <<EOF
record:
type: ApplicationRecord
version: $NEW_APPLICATION_VERSION
repository_ref: $LATEST_HASH
repository: ["$REPO_URL"]
app_type: webapp
name: snowballtools-base-frontend
app_version: $PACKAGE_VERSION
EOF
echo "Files generated successfully."
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
RECORD_FILE=records/application-record.yml
CONFIG_FILE=config.yml
# Publish ApplicationRecord
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationRecord published"
echo $RECORD_ID
# Set name to record
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
# Set name if latest release
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_CRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_CRN."
exit 1
fi
RECORD_FILE=records/application-deployment-request.yml
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID
+9
View File
@@ -0,0 +1,9 @@
{
"name": "deployer",
"version": "1.0.0",
"main": "index.js",
"private": true,
"devDependencies": {
"@cerc-io/laconic-registry-cli": "^0.1.10"
}
}
@@ -0,0 +1,18 @@
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: snowballtools-base-frontend@0.1.8
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
dns: dashboard
config:
env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta:
note: Added by Snowball @ Thu Feb 29 07:46:48 UTC 2024
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
repository_ref: a7810a34c90d5bfe0c6cc0697ad9aaf79b1c41d7
@@ -0,0 +1,8 @@
record:
type: ApplicationRecord
version: 0.0.27
repository_ref: a7810a34c90d5bfe0c6cc0697ad9aaf79b1c41d7
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
app_type: webapp
name: snowballtools-base-frontend
app_version: 0.1.8
-7
View File
@@ -1,7 +0,0 @@
REACT_APP_SERVER_URL = 'http://localhost:8000'
REACT_APP_GITHUB_CLIENT_ID =
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
REACT_APP_WALLET_CONNECT_ID =
-39
View File
@@ -1,39 +0,0 @@
{
// eslint extension options
"eslint.enable": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"css.customData": [".vscode/tailwind.json"],
// prettier extension setting
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.rulers": [80],
"editor.codeActionsOnSave": [
"source.addMissingImports",
"source.fixAll",
"source.organizeImports"
],
// Show in vscode "Problems" tab when there are errors
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
// Use absolute import for typescript files
"typescript.preferences.importModuleSpecifier": "non-relative",
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}
+5 -4
View File
@@ -1,19 +1,20 @@
{
"name": "frontend",
"version": "0.1.0",
"version": "0.1.8",
"private": true,
"dependencies": {
"@fontsource-variable/jetbrains-mono": "^5.0.19",
"@fontsource/inter": "^5.0.16",
"@material-tailwind/react": "^2.1.7",
"@tanstack/react-query": "^5.22.2",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.22.2",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -27,7 +28,7 @@
"axios": "^1.6.7",
"clsx": "^2.1.0",
"date-fns": "^3.3.1",
"downshift": "^8.2.3",
"downshift": "^8.3.2",
"eslint-config-react-app": "^7.0.1",
"gql-client": "^1.0.0",
"luxon": "^3.4.4",
@@ -0,0 +1,4 @@
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M-12.6045 1.27694L-19 3.9698H37.5L34.8955 2.66266L31.6045 1.27694C29.6192 0.441052 27.3808 0.441052 25.3955 1.27694L22.1045 2.66266C20.1192 3.49855 17.8808 3.49855 15.8955 2.66266L12.6045 1.27694C10.6192 0.441052 8.38078 0.441052 6.39555 1.27694L3.10446 2.66266C1.11922 3.49855 -1.11922 3.49855 -3.10445 2.66266L-6.39554 1.27694C-8.38078 0.441052 -10.6192 0.441052 -12.6045 1.27694Z" fill="currentColor"/>
<path d="M25.7836 1.58773L22.4925 2.97346C20.3832 3.86159 18.0049 3.86159 15.8955 2.97346L12.6045 1.58774C10.7433 0.804089 8.64476 0.804088 6.7836 1.58773L3.49251 2.97346C1.3832 3.86159 -0.99514 3.86159 -3.10445 2.97346L-6.39554 1.58774C-8.2567 0.804089 -10.3552 0.804088 -12.2164 1.58773L-18.6119 4.2806L-19 3.35896L-12.6045 0.666099C-10.4951 -0.222033 -8.1168 -0.222033 -6.00749 0.6661L-2.7164 2.05182C-0.855238 2.83547 1.2433 2.83547 3.10446 2.05182L6.39555 0.666099C8.50486 -0.222033 10.8832 -0.222033 12.9925 0.6661L16.2836 2.05182C18.1448 2.83547 20.2433 2.83547 22.1045 2.05182L25.3955 0.666099L25.7836 1.58773Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,15 @@
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1177_19315)">
<path d="M-12.6045 1.27694L-19 3.9698H37.5L34.8955 2.66266L31.6045 1.27694C29.6192 0.441052 27.3808 0.441052 25.3955 1.27694L22.1045 2.66266C20.1192 3.49855 17.8808 3.49855 15.8955 2.66266L12.6045 1.27694C10.6192 0.441052 8.38078 0.441052 6.39555 1.27694L3.10446 2.66266C1.11922 3.49855 -1.11922 3.49855 -3.10445 2.66266L-6.39554 1.27694C-8.38078 0.441052 -10.6192 0.441052 -12.6045 1.27694Z" fill="url(#paint0_linear_1177_19315)"/>
<path d="M25.7836 1.58773L22.4925 2.97346C20.3832 3.86159 18.0049 3.86159 15.8955 2.97346L12.6045 1.58774C10.7433 0.804089 8.64476 0.804088 6.7836 1.58773L3.49251 2.97346C1.3832 3.86159 -0.99514 3.86159 -3.10445 2.97346L-6.39554 1.58774C-8.2567 0.804089 -10.3552 0.804088 -12.2164 1.58773L-18.6119 4.2806L-19 3.35896L-12.6045 0.666099C-10.4951 -0.222033 -8.1168 -0.222033 -6.00749 0.6661L-2.7164 2.05182C-0.855238 2.83547 1.2433 2.83547 3.10446 2.05182L6.39555 0.666099C8.50486 -0.222033 10.8832 -0.222033 12.9925 0.6661L16.2836 2.05182C18.1448 2.83547 20.2433 2.83547 22.1045 2.05182L25.3955 0.666099L25.7836 1.58773Z" fill="#DBEBF9"/>
</g>
<defs>
<linearGradient id="paint0_linear_1177_19315" x1="9.25" y1="0.650024" x2="9.25" y2="3.9698" gradientUnits="userSpaceOnUse">
<stop stop-color="#EBF6FF"/>
<stop offset="1" stop-color="#F7FCFF" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_1177_19315">
<rect width="19" height="4" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,10 @@
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1175_19292)">
<path d="M-9.5 0L-3.10445 2.69286C-1.11922 3.52875 1.11922 3.52875 3.10446 2.69286L6.39555 1.30714C8.38078 0.47125 10.6192 0.47125 12.6045 1.30714L15.8955 2.69286C17.8808 3.52875 20.1192 3.52875 22.1045 2.69286L28.5 0" stroke="#082F56" stroke-opacity="0.1"/>
</g>
<defs>
<clipPath id="clip0_1175_19292">
<rect width="19" height="4" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 507 B

+2 -2
View File
@@ -1,7 +1,6 @@
import React from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import OrgSlug from './pages/OrgSlug';
import Projects from './pages/org-slug';
import Settings from './pages/org-slug/Settings';
import {
@@ -11,11 +10,12 @@ import {
import ProjectSearchLayout from './layouts/ProjectSearch';
import Index from './pages';
import Login from './pages/Login';
import { DashboardLayout } from 'pages/org-slug/layout';
const router = createBrowserRouter([
{
path: ':orgSlug',
element: <OrgSlug />,
element: <DashboardLayout />,
children: [
{
element: <ProjectSearchLayout />,
+10 -5
View File
@@ -2,31 +2,36 @@ export default [
{
id: '1',
name: 'Progressive Web App (PWA)',
icon: '^',
icon: 'pwa',
repoFullName: `${process.env.REACT_APP_GITHUB_PWA_TEMPLATE_REPO}`,
isComingSoon: false,
},
{
id: '2',
name: 'Image Upload PWA',
icon: '^',
icon: 'pwa',
repoFullName: `${process.env.REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
isComingSoon: false,
},
{
id: '3',
name: 'Kotlin',
icon: '^',
icon: 'kotlin',
repoFullName: '',
isComingSoon: true,
},
{
id: '4',
name: 'React Native',
icon: '^',
icon: 'react-native',
repoFullName: '',
isComingSoon: true,
},
{
id: '5',
name: 'Swift',
icon: '^',
icon: 'swift',
repoFullName: '',
isComingSoon: true,
},
];
@@ -1,71 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Project } from 'gql-client';
import {
Menu,
MenuHandler,
MenuList,
MenuItem,
Typography,
Avatar,
} from '@material-tailwind/react';
import { relativeTimeMs } from '../../utils/time';
interface ProjectCardProps {
project: Project;
}
const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
return (
<div className="bg-white border border-gray-200 rounded-lg shadow">
<div className="flex gap-2 p-2 items-center">
<Avatar
variant="rounded"
src={project.icon || '/gray.png'}
placeholder={''}
/>
<div className="grow">
<Link to={`projects/${project.id}`}>
<Typography placeholder={''}>{project.name}</Typography>
<Typography color="gray" variant="small" placeholder={''}>
{project.deployments[0]?.domain?.name ??
'No Production Deployment'}
</Typography>
</Link>
</div>
<Menu placement="bottom-end">
<MenuHandler>
<button>...</button>
</MenuHandler>
<MenuList placeholder={''}>
<MenuItem placeholder={''}>^ Project settings</MenuItem>
<MenuItem className="text-red-500" placeholder={''}>
^ Delete project
</MenuItem>
</MenuList>
</Menu>
</div>
<div className="border-t-2 border-solid p-4 bg-gray-50">
{project.deployments.length > 0 ? (
<>
<Typography variant="small" color="gray" placeholder={''}>
^ {project.deployments[0].commitMessage}
</Typography>
<Typography variant="small" color="gray" placeholder={''}>
{relativeTimeMs(project.deployments[0].createdAt)} on ^&nbsp;
{project.deployments[0].branch}
</Typography>
</>
) : (
<Typography variant="small" color="gray" placeholder={''}>
No Production deployment
</Typography>
)}
</div>
</div>
);
};
export default ProjectCard;
@@ -0,0 +1,80 @@
import { VariantProps, tv } from 'tailwind-variants';
export const projectCardTheme = tv({
slots: {
wrapper: [
'bg-surface-card',
'shadow-card',
'rounded-2xl',
'flex',
'flex-col',
'group',
],
upperContent: ['px-4', 'py-4', 'flex', 'items-start', 'gap-3', 'relative'],
content: ['flex', 'flex-col', 'gap-1', 'flex-1'],
title: [
'text-sm',
'font-medium',
'text-elements-high-em',
'tracking-[-0.006em]',
],
description: ['text-xs', 'text-elements-low-em'],
icons: ['flex', 'items-center', 'gap-1'],
lowerContent: [
'transition-colors',
'duration-150',
'px-4',
'py-4',
'flex',
'flex-col',
'gap-2',
'rounded-b-2xl',
'group-hover:bg-surface-card-hovered',
],
latestDeployment: ['flex', 'items-center', 'gap-2'],
deploymentStatusContainer: [
'h-3',
'w-3',
'flex',
'items-center',
'justify-center',
],
deploymentStatus: ['w-1', 'h-1', 'rounded-full'],
deploymentName: ['text-xs', 'text-elements-low-em'],
deploymentText: [
'text-xs',
'text-elements-low-em',
'font-mono',
'flex',
'items-center',
'gap-2',
],
wavyBorder: [
'bg-surface-card',
'transition-colors',
'duration-150',
'group-hover:bg-surface-card-hovered',
],
},
variants: {
status: {
success: {
deploymentStatus: ['bg-emerald-500'],
},
'in-progress': {
deploymentStatus: ['bg-orange-400'],
},
failure: {
deploymentStatus: ['bg-rose-500'],
},
pending: {
deploymentStatus: ['bg-gray-500'],
},
},
},
defaultVariants: {
status: 'pending',
},
});
export type ProjectCardTheme = VariantProps<typeof projectCardTheme>;
@@ -0,0 +1,139 @@
import React, {
ComponentPropsWithoutRef,
MouseEvent,
useCallback,
} from 'react';
import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
import { Project } from 'gql-client';
import { Button } from 'components/shared/Button';
import { WavyBorder } from 'components/shared/WavyBorder';
import {
BranchIcon,
ClockIcon,
GitHubLogo,
HorizontalDotIcon,
WarningDiamondIcon,
} from 'components/shared/CustomIcon';
import { relativeTimeMs } from 'utils/time';
import { useNavigate } from 'react-router-dom';
import { Avatar } from 'components/shared/Avatar';
import { getInitials } from 'utils/geInitials';
import {
Menu,
MenuHandler,
MenuItem,
MenuList,
} from '@material-tailwind/react';
export interface ProjectCardProps
extends ComponentPropsWithoutRef<'div'>,
ProjectCardTheme {
project: Project;
}
// TODO: Update the whole component to use `Link` from `react-router-dom` and remove the `useNavigate` hook,
// currently it's not possible to use `Link` because the dot menu is not a direct child of the `Link` component
export const ProjectCard = ({
className,
project,
status = 'failure',
...props
}: ProjectCardProps) => {
const theme = projectCardTheme();
const hasDeployment = project.deployments.length > 0;
// TODO: Update this to use the actual status from the API
const hasError = status === 'failure';
const navigate = useNavigate();
const handleOptionsClick = (
e: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>,
) => {
e.stopPropagation();
};
const handleClick = useCallback(() => {
navigate(`projects/${project.id}`);
}, [project.id, navigate]);
return (
<div
{...props}
className={theme.wrapper({ className })}
onClick={handleClick}
>
{/* Upper content */}
<div className={theme.upperContent()}>
{/* Icon container */}
<Avatar
size={48}
imageSrc={project.icon}
initials={getInitials(project.name)}
/>
{/* Title and website */}
<div className={theme.content()}>
<p className={theme.title()}>{project.name}</p>
<p className={theme.description()}>
{project.deployments[0]?.domain?.name ?? 'No domain'}
</p>
</div>
{/* Icons */}
<div className={theme.icons()}>
{hasError && <WarningDiamondIcon className="text-elements-danger" />}
<Menu placement="bottom-end">
<MenuHandler>
<Button
shape="default"
size="xs"
variant="ghost"
iconOnly
onClick={handleOptionsClick}
>
<HorizontalDotIcon />
</Button>
</MenuHandler>
<MenuList placeholder={''}>
<MenuItem placeholder={''}>Project settings</MenuItem>
<MenuItem className="text-red-500" placeholder={''}>
Delete project
</MenuItem>
</MenuList>
</Menu>
</div>
</div>
{/* Wave */}
<WavyBorder variant="stroke-and-fill" className={theme.wavyBorder()} />
{/* Lower content */}
<div className={theme.lowerContent()}>
{/* Latest deployment */}
<div className={theme.latestDeployment()}>
{/* Dot icon */}
<div className={theme.deploymentStatusContainer()}>
<div className={theme.deploymentStatus({ status })} />
</div>
<p className={theme.deploymentText()}>
{hasDeployment
? project.deployments[0]?.commitMessage
: 'No production deployment'}
</p>
</div>
{/* Deployment and branch name */}
<div className={theme.deploymentText()}>
{hasDeployment ? (
<>
<GitHubLogo />
<span>{relativeTimeMs(project.deployments[0].createdAt)} on</span>
<BranchIcon />
<span>{project.deployments[0].branch}</span>
</>
) : (
<>
<ClockIcon />
<span>Created {relativeTimeMs(project.createdAt)}</span>
</>
)}
</div>
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from './ProjectCard';
@@ -7,7 +7,12 @@ import {
GitIcon,
EllipsesIcon,
SnowballIcon,
GithubIcon,
GitTeaIcon,
} from 'components/shared/CustomIcon';
import { useToast } from 'components/shared/Toast';
import { IconWithFrame } from 'components/shared/IconWithFrame';
import { Heading } from 'components/shared/Heading';
const SCOPES = 'repo user';
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
@@ -23,47 +28,68 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
}: ConnectAccountInterface) => {
const client = useGQLClient();
const { toast, dismiss } = useToast();
const handleCode = async (code: string) => {
// Pass code to backend and get access token
const {
authenticateGitHub: { token },
} = await client.authenticateGitHub(code);
onToken(token);
toast({
onDismiss: dismiss,
id: 'connected-to-github',
title: 'The Git account is connected.',
variant: 'success',
});
};
// TODO: Use correct height
return (
<div className="bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl">
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex">
<div className="w-16 h-16 bg-blue-100 rounded-2xl shadow-inner border border-sky-950 border-opacity-10 justify-center items-center gap-2.5 inline-flex">
<GitIcon />
<div className="flex flex-col items-center max-w-[420px]">
{/** Icons */}
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex mb-7">
<IconWithFrame icon={<GitIcon />} />
<EllipsesIcon className="items-center gap-1.5 flex" />
<IconWithFrame className="bg-blue-400" icon={<SnowballIcon />} />
</div>
<EllipsesIcon className="items-center gap-1.5 flex" />
<div className="w-16 h-16 bg-blue-400 rounded-2xl shadow-inner border border-sky-950 border-opacity-10 justify-center items-center gap-2.5 flex">
<SnowballIcon />
{/** Text */}
<div className="flex flex-col gap-1.5 mb-6">
<Heading className="text-xl font-medium">
Connect to your Git account
</Heading>
<p className="text-center text-elements-mid-em">
Once connected, you can import a repository from your account or
start with one of our templates.
</p>
</div>
</div>
<div>
<div className="text-center text-slate-900 text-xl font-medium leading-7">
Connect to your Git account
{/** CTA Buttons */}
<div className="flex flex-col w-full sm:w-auto sm:flex-row gap-2 sm:gap-3">
<OauthPopup
url={GITHUB_OAUTH_URL}
onCode={handleCode}
onClose={() => {}}
title="Snowball"
width={1000}
height={1000}
>
<Button
className="w-full sm:w-auto"
leftIcon={<GithubIcon />}
variant="tertiary"
>
Connect to GitHub
</Button>
</OauthPopup>
<Button
className="w-full sm:w-auto"
leftIcon={<GitTeaIcon />}
variant="tertiary"
>
Connect to GitTea
</Button>
</div>
<div className="text-center text-slate-600 text-base font-normal leading-normal">
Once connected, you can import a repository from your account or start
with one of our templates.
</div>
</div>
<div className="mt-2 flex gap-3">
<OauthPopup
url={GITHUB_OAUTH_URL}
onCode={handleCode}
onClose={() => {}}
title="Snowball"
width={1000}
height={1000}
>
<Button>Connect to GitHub</Button>
</OauthPopup>
<Button>Connect to Gitea</Button>
</div>
{/* TODO: Add ConnectAccountTabPanel */}
@@ -7,6 +7,7 @@ import { Chip, IconButton, Spinner } from '@material-tailwind/react';
import { relativeTimeISO } from '../../../utils/time';
import { GitRepositoryDetails } from '../../../types';
import { useGQLClient } from '../../../context/GQLClientContext';
import { GithubIcon, LockIcon } from 'components/shared/CustomIcon';
interface ProjectRepoCardProps {
repository: GitRepositoryDetails;
@@ -47,16 +48,18 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer"
onClick={createProject}
>
<div>^</div>
<div className="w-10 h-10 bg-white rounded-md justify-center items-center gap-1.5 inline-flex">
<GithubIcon />
</div>
<div className="grow">
<div>
<span className="text-black">{repository.full_name}</span>
{repository.visibility === 'private' && (
<Chip
className="normal-case inline ml-6 font-normal"
className="normal-case inline ml-6 font-normal text-xs text-xs bg-orange-50 border border-orange-200 text-orange-600 items-center gap-1 inline-flex"
size="sm"
value="Private"
icon={'^'}
icon={<LockIcon />}
/>
)}
</div>
@@ -9,6 +9,7 @@ import SearchBar from '../../SearchBar';
import ProjectRepoCard from './ProjectRepoCard';
import { GitOrgDetails, GitRepositoryDetails } from '../../../types';
import AsyncSelect from '../../shared/AsyncSelect';
import { GithubIcon } from 'components/shared/CustomIcon';
const DEFAULT_SEARCHED_REPO = '';
const REPOS_PER_PAGE = 5;
@@ -108,7 +109,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
return (
<div className="p-4">
<div className="flex gap-2 mb-2">
<div className="flex gap-2 mb-2 items-center">
<div className="basis-1/3">
<AsyncSelect
value={selectedAccount}
@@ -116,12 +117,14 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
>
{accounts.map((account) => (
<Option key={account.id} value={account.login}>
^ {account.login}
<div className="flex items-center gap-2 justify-start">
<GithubIcon /> {account.login}
</div>
</Option>
))}
</AsyncSelect>
</div>
<div className="basis-2/3">
<div className="basis-2/3 flex-grow flex items-center">
<SearchBar
value={searchedRepo}
onChange={(event) => setSearchedRepo(event.target.value)}
@@ -1,53 +0,0 @@
import React from 'react';
import toast from 'react-hot-toast';
import { IconButton, Typography } from '@material-tailwind/react';
import { Link } from 'react-router-dom';
interface TemplateDetails {
id: string;
name: string;
icon: string;
}
interface TemplateCardProps {
template: TemplateDetails;
isGitAuth: boolean;
}
const CardDetails = ({ template }: { template: TemplateDetails }) => {
return (
<div className="h-14 group bg-gray-200 border-gray-200 rounded-lg shadow p-4 flex items-center justify-between">
<Typography className="grow" placeholder={''}>
{template.icon} {template.name}
</Typography>
<div>
<IconButton
size="sm"
className="rounded-full hidden group-hover:block"
placeholder={''}
>
{'>'}
</IconButton>
</div>
</div>
);
};
const TemplateCard: React.FC<TemplateCardProps> = ({ template, isGitAuth }) => {
return isGitAuth ? (
<Link to={`template?templateId=${template.id}`}>
<CardDetails template={template} />
</Link>
) : (
<a
onClick={() =>
toast.error('Connect Git account to start with a template')
}
>
<CardDetails template={template} />
</a>
);
};
export default TemplateCard;
@@ -0,0 +1,91 @@
import React, { ComponentPropsWithoutRef, useCallback } from 'react';
import { Button } from 'components/shared/Button';
import {
ArrowRightCircleIcon,
ClockOutlineIcon,
TemplateIcon,
TemplateIconType,
} from 'components/shared/CustomIcon';
import { Tag } from 'components/shared/Tag';
import { useNavigate, useParams } from 'react-router-dom';
import { useToast } from 'components/shared/Toast';
import { cn } from 'utils/classnames';
export interface TemplateDetail {
id: string;
name: string;
icon: string;
repoFullName?: string;
isComingSoon?: boolean;
}
export interface TemplateCardProps extends ComponentPropsWithoutRef<'div'> {
template: TemplateDetail;
isGitAuth: boolean;
}
export const TemplateCard: React.FC<TemplateCardProps> = ({
template,
isGitAuth,
}: TemplateCardProps) => {
const { toast, dismiss } = useToast();
const navigate = useNavigate();
const { orgSlug } = useParams();
const handleClick = useCallback(() => {
if (template?.isComingSoon) {
return toast({
id: 'coming-soon',
title: 'This template is coming soon',
variant: 'info',
onDismiss: dismiss,
});
}
if (isGitAuth) {
return navigate(
`/${orgSlug}/projects/create/template?templateId=${template.id}`,
);
}
return toast({
id: 'connect-git-account',
title: 'Connect Git account to start with a template',
variant: 'error',
onDismiss: dismiss,
});
}, [orgSlug, dismiss, isGitAuth, navigate, template, toast]);
return (
<button
className={cn(
'flex items-center gap-3 px-3 py-3 bg-base-bg-alternate hover:bg-base-bg-emphasized rounded-2xl group relative',
{
'cursor-default': template?.isComingSoon,
},
)}
onClick={handleClick}
>
{/* Icon */}
<div className="px-1 py-1 rounded-xl bg-base-bg border border-border-interactive/10 shadow-card-sm">
<TemplateIcon type={template.icon as TemplateIconType} />
</div>
{/* Name */}
<p className="flex-1 text-left text-sm tracking-tighter text-elements-high-em">
{template.name}
</p>
{template?.isComingSoon ? (
<Tag size="xs" type="neutral" leftIcon={<ClockOutlineIcon />}>
Soon
</Tag>
) : (
<Button
variant="tertiary"
size="sm"
iconOnly
className="group-hover:flex hidden absolute right-3"
>
<ArrowRightCircleIcon />
</Button>
)}
</button>
);
};
@@ -0,0 +1 @@
export * from './TemplateCard';
@@ -1,46 +0,0 @@
import React from 'react';
import { Typography, IconButton, Avatar } from '@material-tailwind/react';
import { relativeTimeISO } from '../../../utils/time';
import { GitCommitWithBranch } from '../../../types';
interface ActivityCardProps {
activity: GitCommitWithBranch;
}
const ActivityCard = ({ activity }: ActivityCardProps) => {
return (
<div className="group flex gap-2 hover:bg-gray-200 rounded mt-1">
<div className="w-8">
<Avatar
src={activity.author?.avatar_url}
variant="rounded"
size="sm"
placeholder={''}
/>
</div>
<div className="grow">
<Typography placeholder={''}>{activity.commit.author?.name}</Typography>
<Typography variant="small" color="gray" placeholder={''}>
{relativeTimeISO(activity.commit.author!.date!)} ^{' '}
{activity.branch.name}
</Typography>
<Typography variant="small" color="gray" placeholder={''}>
{activity.commit.message}
</Typography>
</div>
<div className="mr-2 self-center hidden group-hover:block">
<IconButton
size="sm"
className="rounded-full bg-gray-600"
placeholder={''}
>
{'>'}
</IconButton>
</div>
</div>
);
};
export default ActivityCard;
@@ -0,0 +1,28 @@
import React from 'react';
import { GitCommitWithBranch } from 'types';
import { Heading } from 'components/shared/Heading';
import ActivityCard from './ActivityCard';
import { Button } from 'components/shared/Button';
export const Activity = ({
activities,
}: {
activities: GitCommitWithBranch[];
}) => {
return (
<div className="col-span-2 mr-1">
<div className="flex items-center justify-between">
<Heading className="text-lg leading-6 font-medium">Activity</Heading>
<Button variant="tertiary" size="sm">
See all
</Button>
</div>
<div className="mt-5">
{activities.map((activity, index) => {
return <ActivityCard activity={activity} key={`activity-${index}`} />;
})}
</div>
</div>
);
};
@@ -0,0 +1,86 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { GitCommitWithBranch } from '../../../../../types';
import { Avatar } from 'components/shared/Avatar';
import { Button } from 'components/shared/Button';
import {
ArrowRightCircleFilledIcon,
BranchStrokeIcon,
} from 'components/shared/CustomIcon';
import { formatDistance } from 'date-fns';
import { getInitials } from 'utils/geInitials';
interface ActivityCardProps {
activity: GitCommitWithBranch;
}
const ActivityCard = ({ activity }: ActivityCardProps) => {
const formattedDate = formatDistance(
new Date(activity.commit.author!.date!),
new Date(),
{
addSuffix: true,
},
);
return (
<>
<Link
to={activity.html_url}
target="_blank"
className="p-3 gap-2 focus-within:ring-2 focus-within:ring-controls-primary/40 focus:outline-none rounded-xl transition-colors hover:bg-base-bg-alternate flex group"
>
<div>
<Avatar
type="orange"
size={36}
initials={getInitials(activity.commit.author?.name ?? '')}
imageSrc={activity.author?.avatar_url}
/>
</div>
<div className="flex-1">
<span className="text-elements-high-em text-sm font-medium tracking-tight">
{activity.commit.author?.name}
</span>
<span className="text-elements-low-em text-xs flex items-center gap-2">
<span title={formattedDate} className="whitespace-nowrap">
{formattedDate}
</span>
{/* Dot */}
<span className="w-0.5 h-0.5 rounded-full bg-border-interactive-hovered"></span>
<span className="flex justify-center items-center gap-1.5">
<div>
<BranchStrokeIcon className="w-3 h-3" />
</div>
<span
title={activity.branch.name}
// pseudo to increase hover area
className="before:absolute relative before:h-5 before:-top-4 before:inset-x-0"
>
<span className="line-clamp-1">{activity.branch.name}</span>
</span>
</span>
</span>
<p
title={activity.commit.message}
className="text-sm line-clamp-4 tracking-tight text-elements-mid-em mt-2"
>
{activity.commit.message}
</p>
</div>
<Button
aria-label="Go to commit"
variant="unstyled"
tabIndex={-1}
className="p-0 text-elements-low-em group-focus-within:opacity-100 group-hover:opacity-100 opacity-0 transition-all"
leftIcon={<ArrowRightCircleFilledIcon className="w-5 h-5" />}
/>
</Link>
{/* Separator calc => 100% - 36px (avatar) - 12px (padding-left) - 8px (gap)
*/}
<div className="pt-2 mb-2 ml-auto h-1 w-[calc(100%-36px-12px-8px)] border-b border-border-separator last:border-b-transparent"></div>
</>
);
};
export default ActivityCard;
@@ -0,0 +1 @@
export * from './Activity';
@@ -0,0 +1,29 @@
import React from 'react';
import { PropsWithChildren } from 'react';
import { cloneElement } from 'utils/cloneElement';
interface OverviewInfoProps {
label: string;
icon: React.ReactNode;
}
export const OverviewInfo = ({
label,
icon,
children,
}: PropsWithChildren<OverviewInfoProps>) => {
const styledIcon = cloneElement({
element: icon,
className: 'w-4 h-4',
});
return (
<div className="flex justify-between gap-2 py-3 text-sm items-center">
<div className="flex gap-2 items-center text-elements-high-em">
{styledIcon}
{label}
</div>
<div>{children}</div>
</div>
);
};
@@ -58,6 +58,9 @@ export const avatarTheme = tv(
44: {
base: ['rounded-xl', 'h-[44px]', 'w-[44px]', 'text-sm'],
},
48: {
base: ['rounded-xl', 'h-[48px]', 'w-[48px]', 'text-sm'],
},
},
},
defaultVariants: {
@@ -14,6 +14,8 @@ export const buttonTheme = tv(
'whitespace-nowrap',
'focus-ring',
'disabled:cursor-not-allowed',
'transition-colors',
'duration-150',
],
variants: {
size: {
@@ -26,7 +28,7 @@ export const buttonTheme = tv(
true: 'w-full',
},
shape: {
default: '',
default: 'rounded-lg',
rounded: 'rounded-full',
},
iconOnly: {
@@ -61,14 +63,13 @@ export const buttonTheme = tv(
tertiary: [
'text-elements-on-tertiary',
'border',
'border-border-interactive/10',
'border-border-interactive',
'bg-controls-tertiary',
'shadow-button',
'hover:bg-controls-tertiary-hovered',
'hover:border-border-interactive-hovered',
'hover:border-border-interactive-hovered/[0.14]',
'focus-visible:bg-controls-tertiary-hovered',
'focus-visible:border-border-interactive-hovered',
'focus-visible:border-border-interactive-hovered/[0.14]',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
@@ -81,10 +82,8 @@ export const buttonTheme = tv(
'bg-transparent',
'hover:bg-controls-tertiary-hovered',
'hover:border-border-interactive-hovered',
'hover:border-border-interactive-hovered/[0.14]',
'focus-visible:bg-controls-tertiary-hovered',
'focus-visible:border-border-interactive-hovered',
'focus-visible:border-border-interactive-hovered/[0.14]',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
@@ -109,10 +108,8 @@ export const buttonTheme = tv(
'bg-transparent',
'hover:bg-controls-tertiary-hovered',
'hover:border-border-interactive-hovered',
'hover:border-border-interactive-hovered/[0.14]',
'focus-visible:bg-controls-tertiary-hovered',
'focus-visible:border-border-interactive-hovered',
'focus-visible:border-border-interactive-hovered/[0.14]',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
@@ -153,7 +150,7 @@ export const buttonTheme = tv(
{
size: 'md',
iconOnly: true,
class: ['py-3.25', 'px-3.25'],
class: ['py-3', 'px-3'],
},
{
size: 'sm',
@@ -5,7 +5,7 @@ export const calendarTheme = tv({
wrapper: [
'max-w-[352px]',
'bg-surface-floating',
'shadow-calendar',
'shadow-dropdown',
'rounded-xl',
],
calendar: ['flex', 'flex-col', 'py-2', 'px-2', 'gap-2'],
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const ArrowRightCircleFilledIcon = (props: CustomIconProps) => {
return (
<CustomIcon
{...props}
fill="none"
height="32"
viewBox="0 0 32 32"
width="32"
>
<path
clipRule="evenodd"
d="M29.3334 16C29.3334 8.63619 23.3638 2.66666 16 2.66666C8.63622 2.66666 2.66669 8.63619 2.66669 16C2.66669 23.3638 8.63622 29.3333 16 29.3333C23.3638 29.3333 29.3334 23.3638 29.3334 16ZM18.1144 17.3333L16.3905 19.0572C15.8698 19.5779 15.8698 20.4221 16.3905 20.9428C16.9112 21.4635 17.7555 21.4635 18.2762 20.9428L21.3334 17.8856C22.3748 16.8442 22.3748 15.1558 21.3334 14.1144L18.2762 11.0572C17.7555 10.5365 16.9112 10.5365 16.3905 11.0572C15.8698 11.5779 15.8698 12.4221 16.3905 12.9428L18.1144 14.6667H10.6667C9.93031 14.6667 9.33335 15.2636 9.33335 16C9.33335 16.7364 9.93031 17.3333 10.6667 17.3333H18.1144Z"
fill="currentColor"
fillRule="evenodd"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const ArrowRightCircleIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="17"
height="16"
viewBox="0 0 17 16"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.3333 8C15.3333 4.3181 12.3486 1.33333 8.66667 1.33333C4.98477 1.33333 2 4.3181 2 8C2 11.6819 4.98477 14.6667 8.66667 14.6667C12.3486 14.6667 15.3333 11.6819 15.3333 8ZM10.2929 8.5L8.97978 9.81311C8.78452 10.0084 8.78452 10.325 8.97978 10.5202C9.17504 10.7155 9.49162 10.7155 9.68689 10.5202L11.3821 8.82495C11.8378 8.36934 11.8378 7.63065 11.3821 7.17504L9.68689 5.47977C9.49162 5.28451 9.17504 5.28451 8.97978 5.47977C8.78452 5.67504 8.78452 5.99162 8.97978 6.18688L10.2929 7.5H5.83333C5.55719 7.5 5.33333 7.72385 5.33333 8C5.33333 8.27614 5.55719 8.5 5.83333 8.5H10.2929Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,19 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const BranchIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
{...props}
>
<path
d="M3.5 1C2.67157 1 2 1.67157 2 2.5C2 3.24325 2.54057 3.86024 3.25 3.97926V8.02074C2.54057 8.13976 2 8.75675 2 9.5C2 10.3284 2.67157 11 3.5 11C4.32843 11 5 10.3284 5 9.5C5 8.75675 4.45943 8.13976 3.75 8.02074V7C3.75 6.58579 4.08579 6.25 4.5 6.25H7.5C8.19036 6.25 8.75 5.69036 8.75 5V3.97926C9.45943 3.86024 10 3.24325 10 2.5C10 1.67157 9.32843 1 8.5 1C7.67157 1 7 1.67157 7 2.5C7 3.24325 7.54057 3.86024 8.25 3.97926V5C8.25 5.41421 7.91421 5.75 7.5 5.75H4.5C4.2186 5.75 3.95892 5.84299 3.75 5.99991V3.97926C4.45943 3.86024 5 3.24325 5 2.5C5 1.67157 4.32843 1 3.5 1Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,20 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const BranchStrokeIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
d="M4.66667 4.99984C5.58714 4.99984 6.33333 4.25365 6.33333 3.33317C6.33333 2.4127 5.58714 1.6665 4.66667 1.6665C3.74619 1.6665 3 2.4127 3 3.33317C3 4.25365 3.74619 4.99984 4.66667 4.99984ZM4.66667 4.99984V10.9998M4.66667 10.9998C3.74619 10.9998 3 11.746 3 12.6665C3 13.587 3.74619 14.3332 4.66667 14.3332C5.58714 14.3332 6.33333 13.587 6.33333 12.6665C6.33333 11.746 5.58714 10.9998 4.66667 10.9998ZM4.66667 10.9998V9.33317C4.66667 8.59679 5.26362 7.99984 6 7.99984H10C10.7364 7.99984 11.3333 7.40288 11.3333 6.6665V4.99984M11.3333 4.99984C12.2538 4.99984 13 4.25365 13 3.33317C13 2.4127 12.2538 1.6665 11.3333 1.6665C10.4129 1.6665 9.66667 2.4127 9.66667 3.33317C9.66667 4.25365 10.4129 4.99984 11.3333 4.99984Z"
stroke="currentColor"
strokeLinejoin="round"
/>
</CustomIcon>
);
};
@@ -0,0 +1,41 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const CalendarDaysIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
d="M2.3335 5.00016H13.6668M4.33349 13.6668H11.6668C12.7714 13.6668 13.6668 12.7714 13.6668 11.6668V4.3335C13.6668 3.22893 12.7714 2.3335 11.6668 2.3335H4.3335C3.22893 2.3335 2.3335 3.22893 2.3335 4.33349V11.6668C2.3335 12.7714 3.22893 13.6668 4.33349 13.6668Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4.6665 7.8335C4.6665 7.55735 4.89036 7.3335 5.1665 7.3335C5.44265 7.3335 5.6665 7.55735 5.6665 7.8335C5.6665 8.10964 5.44265 8.3335 5.1665 8.3335C4.89036 8.3335 4.6665 8.10964 4.6665 7.8335Z"
fill="currentColor"
/>
<path
d="M4.6665 10.8335C4.6665 10.5574 4.89036 10.3335 5.1665 10.3335C5.44265 10.3335 5.6665 10.5574 5.6665 10.8335C5.6665 11.1096 5.44265 11.3335 5.1665 11.3335C4.89036 11.3335 4.6665 11.1096 4.6665 10.8335Z"
fill="currentColor"
/>
<path
d="M7.49984 10.8335C7.49984 10.5574 7.72369 10.3335 7.99984 10.3335C8.27598 10.3335 8.49984 10.5574 8.49984 10.8335C8.49984 11.1096 8.27598 11.3335 7.99984 11.3335C7.72369 11.3335 7.49984 11.1096 7.49984 10.8335Z"
fill="currentColor"
/>
<path
d="M7.49984 7.8335C7.49984 7.55735 7.72369 7.3335 7.99984 7.3335C8.27598 7.3335 8.49984 7.55735 8.49984 7.8335C8.49984 8.10964 8.27598 8.3335 7.99984 8.3335C7.72369 8.3335 7.49984 8.10964 7.49984 7.8335Z"
fill="currentColor"
/>
<path
d="M10.3332 7.8335C10.3332 7.55735 10.557 7.3335 10.8332 7.3335C11.1093 7.3335 11.3332 7.55735 11.3332 7.8335C11.3332 8.10964 11.1093 8.3335 10.8332 8.3335C10.557 8.3335 10.3332 8.10964 10.3332 7.8335Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const CheckRadioIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9 16.5C13.1421 16.5 16.5 13.1421 16.5 9C16.5 4.85786 13.1421 1.5 9 1.5C4.85786 1.5 1.5 4.85786 1.5 9C1.5 13.1421 4.85786 16.5 9 16.5ZM7.93079 12.3663L12.3055 7.01944L11.1446 6.06958L7.81944 10.1336L6.37511 8.68932L5.31445 9.74998L7.93079 12.3663Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,22 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const ChevronDownIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
d="M20 9L13.4142 15.5858C12.6332 16.3668 11.3669 16.3668 10.5858 15.5858L4 9"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</CustomIcon>
);
};
@@ -0,0 +1,19 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const ClockIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
{...props}
>
<path
d="M6 1.125C5.03582 1.125 4.09329 1.41091 3.2916 1.94659C2.48991 2.48226 1.86507 3.24363 1.49609 4.13442C1.12711 5.02521 1.03057 6.00541 1.21867 6.95107C1.40678 7.89672 1.87108 8.76536 2.55286 9.44715C3.23464 10.1289 4.10328 10.5932 5.04894 10.7813C5.99459 10.9694 6.97479 10.8729 7.86558 10.5039C8.75637 10.1349 9.51775 9.51009 10.0534 8.7084C10.5891 7.90671 10.875 6.96418 10.875 6C10.8736 4.70749 10.3596 3.46831 9.44564 2.55436C8.5317 1.64042 7.29251 1.12636 6 1.125ZM8.625 6.375H6C5.90055 6.375 5.80516 6.33549 5.73484 6.26517C5.66451 6.19484 5.625 6.09946 5.625 6V3.375C5.625 3.27554 5.66451 3.18016 5.73484 3.10984C5.80516 3.03951 5.90055 3 6 3C6.09946 3 6.19484 3.03951 6.26517 3.10984C6.33549 3.18016 6.375 3.27554 6.375 3.375V5.625H8.625C8.72446 5.625 8.81984 5.66451 8.89017 5.73484C8.96049 5.80516 9 5.90054 9 6C9 6.09946 8.96049 6.19484 8.89017 6.26517C8.81984 6.33549 8.72446 6.375 8.625 6.375Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const ClockOutlineIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
d="M7.99984 5V8L9.99984 10M14.3332 8C14.3332 11.4978 11.4976 14.3333 7.99984 14.3333C4.50203 14.3333 1.6665 11.4978 1.6665 8C1.6665 4.5022 4.50203 1.66667 7.99984 1.66667C11.4976 1.66667 14.3332 4.5022 14.3332 8Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</CustomIcon>
);
};
@@ -0,0 +1,22 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const CursorBoxIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
d="M13.5 7V3.83333C13.5 3.09695 12.903 2.5 12.1667 2.5H3.83333C3.09695 2.5 2.5 3.09695 2.5 3.83333V12.1667C2.5 12.903 3.09695 13.5 3.83333 13.5H7M9.43391 14.0852L7.5218 7.9391C7.44202 7.68269 7.68269 7.44202 7.9391 7.5218L14.0852 9.43391C14.3658 9.52122 14.4043 9.90262 14.1468 10.0443L11.5848 11.4534C11.5294 11.4838 11.4838 11.5294 11.4534 11.5848L10.0443 14.1468C9.90262 14.4043 9.52122 14.3658 9.43391 14.0852Z"
stroke="currentColor"
strokeWidth="1.05263"
strokeLinecap="round"
strokeLinejoin="round"
/>
</CustomIcon>
);
};
@@ -0,0 +1,19 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const GitHubLogo = (props: CustomIconProps) => {
return (
<CustomIcon
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
{...props}
>
<path
d="M10.125 4.87501V5.25001C10.1242 5.88451 9.89387 6.4973 9.47652 6.97522C9.05917 7.45314 8.48299 7.76392 7.85437 7.85016C8.11096 8.1785 8.25024 8.5833 8.25 9.00001V10.875C8.25 10.9745 8.21049 11.0698 8.14017 11.1402C8.06984 11.2105 7.97446 11.25 7.875 11.25H4.875C4.77554 11.25 4.68016 11.2105 4.60984 11.1402C4.53951 11.0698 4.5 10.9745 4.5 10.875V10.125H3.375C2.87772 10.125 2.40081 9.92746 2.04917 9.57583C1.69754 9.2242 1.5 8.74729 1.5 8.25001C1.5 7.95164 1.38147 7.66549 1.1705 7.45451C0.959517 7.24353 0.673369 7.12501 0.375 7.12501C0.275544 7.12501 0.180161 7.0855 0.109835 7.01517C0.0395088 6.94484 0 6.84946 0 6.75001C0 6.65055 0.0395088 6.55517 0.109835 6.48484C0.180161 6.41451 0.275544 6.37501 0.375 6.37501C0.621229 6.37501 0.865046 6.4235 1.09253 6.51773C1.32002 6.61196 1.52672 6.75007 1.70083 6.92418C1.87494 7.09829 2.01305 7.30499 2.10727 7.53247C2.2015 7.75996 2.25 8.00378 2.25 8.25001C2.25 8.54837 2.36853 8.83452 2.5795 9.0455C2.79048 9.25648 3.07663 9.37501 3.375 9.37501H4.5V9.00001C4.49976 8.5833 4.63904 8.1785 4.89563 7.85016C4.26701 7.76392 3.69083 7.45314 3.27348 6.97522C2.85613 6.4973 2.62579 5.88451 2.625 5.25001V4.87501C2.62967 4.40896 2.75378 3.9519 2.98547 3.54751C2.87085 3.17775 2.83408 2.78826 2.87748 2.40359C2.92089 2.01892 3.04352 1.64742 3.23766 1.31251C3.27058 1.25549 3.31793 1.20814 3.37495 1.17523C3.43198 1.14231 3.49666 1.12499 3.5625 1.12501C3.99929 1.12409 4.43023 1.22535 4.82091 1.42069C5.21159 1.61602 5.55116 1.90003 5.8125 2.25001H6.9375C7.19884 1.90003 7.53841 1.61602 7.92909 1.42069C8.31977 1.22535 8.75071 1.12409 9.1875 1.12501C9.25334 1.12499 9.31802 1.14231 9.37505 1.17523C9.43207 1.20814 9.47942 1.25549 9.51234 1.31251C9.70651 1.64741 9.82912 2.01894 9.87245 2.40362C9.91577 2.78831 9.87887 3.1778 9.76406 3.54751C9.99619 3.95175 10.1205 4.40888 10.125 4.87501Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,27 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const GitTeaIcon: React.FC<CustomIconProps> = (props) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
d="M12.372 15.1312L8.40635 13.225C8.01573 13.0375 7.84698 12.5625 8.0376 12.1687L9.94385 8.20308C10.1314 7.81245 10.6064 7.6437 11.0001 7.83433C11.5376 8.0937 11.847 8.24058 11.847 8.24058L11.8439 4.82808L12.3657 4.82495L12.3689 8.48433C12.3689 8.48433 14.1626 9.24058 14.9657 9.73745C15.0814 9.80933 15.2845 9.94995 15.3689 10.1875C15.4345 10.3781 15.4314 10.5968 15.3376 10.7906L13.4314 14.7562C13.2376 15.1531 12.7626 15.3218 12.372 15.1312Z"
fill="white"
/>
<path
d="M19.4595 4.68123C19.3314 4.5531 19.1595 4.55623 19.1595 4.55623C19.1595 4.55623 15.497 4.76248 13.6001 4.80623C13.1845 4.8156 12.772 4.82498 12.3626 4.8281C12.3626 6.04998 12.3626 7.27185 12.3626 8.4906C12.1907 8.40935 12.0157 8.32498 11.8439 8.24373C11.8439 7.10623 11.8407 4.83123 11.8407 4.83123C10.9345 4.84373 9.05324 4.76248 9.05324 4.76248C9.05324 4.76248 4.63449 4.5406 4.15324 4.49685C3.84699 4.4781 3.45011 4.43123 2.93449 4.54373C2.66261 4.59998 1.88761 4.77498 1.25324 5.38435C-0.153014 6.63748 0.206361 8.63123 0.250111 8.93123C0.303236 9.29685 0.465736 10.3125 1.24074 11.1969C2.67199 12.95 5.75324 12.9094 5.75324 12.9094C5.75324 12.9094 6.13136 13.8125 6.70949 14.6437C7.49074 15.6781 8.29386 16.4844 9.07511 16.5812C11.0439 16.5812 14.9782 16.5781 14.9782 16.5781C14.9782 16.5781 15.3532 16.5812 15.8626 16.2562C16.3001 15.9906 16.6907 15.525 16.6907 15.525C16.6907 15.525 17.0939 15.0937 17.6564 14.1094C17.8282 13.8062 17.972 13.5125 18.097 13.2344C18.097 13.2344 19.822 9.57498 19.822 6.01248C19.7876 4.93435 19.522 4.74373 19.4595 4.68123ZM3.92511 11.0594C3.11574 10.7937 2.77199 10.475 2.77199 10.475C2.77199 10.475 2.17511 10.0562 1.87511 9.23123C1.35949 7.84998 1.83136 7.00623 1.83136 7.00623C1.83136 7.00623 2.09386 6.3031 3.03449 6.06873C3.46574 5.9531 4.00324 5.97185 4.00324 5.97185C4.00324 5.97185 4.22511 7.8281 4.49386 8.9156C4.71886 9.8281 5.26886 11.3437 5.26886 11.3437C5.26886 11.3437 4.45324 11.2469 3.92511 11.0594ZM13.3095 14.4219C13.3095 14.4219 13.1189 14.875 12.697 14.9031C12.5157 14.9156 12.3751 14.8656 12.3751 14.8656C12.3751 14.8656 12.3657 14.8625 12.2095 14.8L8.68136 13.0812C8.68136 13.0812 8.34074 12.9031 8.28136 12.5937C8.21261 12.3406 8.36574 12.0281 8.36574 12.0281L10.0626 8.53123C10.0626 8.53123 10.2126 8.2281 10.4439 8.12498C10.4626 8.1156 10.5157 8.09373 10.5845 8.0781C10.8376 8.01248 11.147 8.1656 11.147 8.1656L14.6064 9.84373C14.6064 9.84373 15.0001 10.0219 15.0845 10.35C15.1439 10.5812 15.0689 10.7875 15.0282 10.8875C14.8314 11.3687 13.3095 14.4219 13.3095 14.4219Z"
fill="#609926"
/>
<path
d="M10.2126 11.8781C9.95632 11.8813 9.73132 12.0594 9.67195 12.3094C9.61257 12.5594 9.73445 12.8187 9.95632 12.9344C10.1969 13.0594 10.5032 12.9906 10.6657 12.7656C10.8251 12.5437 10.8001 12.2375 10.6094 12.0437L11.3594 10.5094C11.4063 10.5125 11.4751 10.5156 11.5532 10.4937C11.6813 10.4656 11.7751 10.3813 11.7751 10.3813C11.9063 10.4375 12.0438 10.5 12.1876 10.5719C12.3376 10.6469 12.4782 10.725 12.6063 10.8C12.6344 10.8156 12.6626 10.8344 12.6938 10.8594C12.7438 10.9 12.8001 10.9562 12.8407 11.0312C12.9001 11.2031 12.7813 11.4969 12.7813 11.4969C12.7094 11.7344 12.2063 12.7656 12.2063 12.7656C11.9532 12.7594 11.7282 12.9219 11.6532 13.1562C11.5719 13.4094 11.6876 13.6969 11.9313 13.8219C12.1751 13.9469 12.4751 13.875 12.6344 13.6562C12.7907 13.4437 12.7782 13.1469 12.6001 12.95C12.6594 12.8344 12.7157 12.7188 12.7751 12.5969C12.9313 12.2719 13.1969 11.6469 13.1969 11.6469C13.2251 11.5938 13.3751 11.325 13.2813 10.9812C13.2032 10.625 12.8876 10.4594 12.8876 10.4594C12.5063 10.2125 11.9751 9.98438 11.9751 9.98438C11.9751 9.98438 11.9751 9.85625 11.9407 9.7625C11.9063 9.66562 11.8532 9.60313 11.8188 9.56563C11.9657 9.2625 12.1126 8.9625 12.2594 8.65938C12.1313 8.59688 12.0063 8.53437 11.8782 8.46875C11.7282 8.775 11.5751 9.08438 11.4251 9.39062C11.2157 9.3875 11.0219 9.5 10.9219 9.68437C10.8157 9.88125 10.8376 10.125 10.9813 10.3031C10.7251 10.8281 10.4688 11.3531 10.2126 11.8781Z"
fill="#609926"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const GithubIcon: React.FC<CustomIconProps> = (props) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.9702 0.206024C4.45694 0.206024 0 4.69582 0 10.2503C0 14.6903 2.85571 18.4487 6.81735 19.7789C7.31265 19.8789 7.49408 19.5628 7.49408 19.2968C7.49408 19.064 7.47776 18.2658 7.47776 17.4342C4.70429 18.033 4.12674 16.2368 4.12674 16.2368C3.68102 15.0728 3.02061 14.7736 3.02061 14.7736C2.11286 14.1583 3.08673 14.1583 3.08673 14.1583C4.09367 14.2248 4.62204 15.1893 4.62204 15.1893C5.51327 16.7191 6.94939 16.2868 7.52714 16.0207C7.60959 15.3721 7.87388 14.9232 8.15449 14.6738C5.94245 14.4409 3.6151 13.5762 3.6151 9.71807C3.6151 8.62051 4.01102 7.72256 4.63837 7.02419C4.53939 6.7748 4.19265 5.74358 4.73755 4.36337C4.73755 4.36337 5.57939 4.09725 7.47755 5.39439C8.29022 5.17453 9.12832 5.06268 9.9702 5.06174C10.812 5.06174 11.6702 5.17827 12.4627 5.39439C14.361 4.09725 15.2029 4.36337 15.2029 4.36337C15.7478 5.74358 15.4008 6.7748 15.3018 7.02419C15.9457 7.72256 16.3253 8.62051 16.3253 9.71807C16.3253 13.5762 13.998 14.4242 11.7694 14.6738C12.1327 14.9897 12.4461 15.5883 12.4461 16.5362C12.4461 17.8832 12.4298 18.9642 12.4298 19.2966C12.4298 19.5628 12.6114 19.8789 13.1065 19.7791C17.0682 18.4485 19.9239 14.6903 19.9239 10.2503C19.9402 4.69582 15.4669 0.206024 9.9702 0.206024Z"
fill="#0B1D2E"
/>
</CustomIcon>
);
};
@@ -0,0 +1,19 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const GithubStrokeIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
d="M13.0194 4.73001C13.1722 4.23701 13.2212 3.71768 13.1634 3.20479C13.1055 2.6919 12.942 2.19656 12.6831 1.75001C12.6392 1.67398 12.5761 1.61085 12.5001 1.56697C12.424 1.52308 12.3378 1.49999 12.25 1.50001C11.6676 1.49879 11.093 1.6338 10.5721 1.89425C10.0512 2.1547 9.59845 2.53337 9.25 3.00001H7.75C7.40155 2.53337 6.94878 2.1547 6.42788 1.89425C5.90698 1.6338 5.33238 1.49879 4.75 1.50001C4.66221 1.49999 4.57597 1.52308 4.49994 1.56697C4.4239 1.61085 4.36077 1.67398 4.31687 1.75001C4.05803 2.19656 3.89452 2.6919 3.83664 3.20479C3.77877 3.71768 3.8278 4.23701 3.98062 4.73001C3.6717 5.26921 3.50623 5.87862 3.5 6.50001V7.00001C3.50105 7.84601 3.80817 8.66306 4.36464 9.30029C4.92111 9.93752 5.68935 10.3519 6.5275 10.4669C6.18538 10.9047 5.99968 11.4444 6 12V12.5H4.5C4.10218 12.5 3.72064 12.342 3.43934 12.0607C3.15804 11.7794 3 11.3978 3 11C3 10.6717 2.93534 10.3466 2.8097 10.0433C2.68406 9.73999 2.49991 9.46439 2.26777 9.23224C2.03562 9.00009 1.76002 8.81595 1.45671 8.69031C1.15339 8.56467 0.828305 8.50001 0.5 8.50001C0.367392 8.50001 0.240215 8.55269 0.146447 8.64645C0.0526784 8.74022 0 8.8674 0 9.00001C0 9.13262 0.0526784 9.25979 0.146447 9.35356C0.240215 9.44733 0.367392 9.50001 0.5 9.50001C0.897825 9.50001 1.27936 9.65804 1.56066 9.93935C1.84196 10.2207 2 10.6022 2 11C2 11.663 2.26339 12.2989 2.73223 12.7678C3.20107 13.2366 3.83696 13.5 4.5 13.5H6V14.5C6 14.6326 6.05268 14.7598 6.14645 14.8536C6.24021 14.9473 6.36739 15 6.5 15C6.63261 15 6.75979 14.9473 6.85355 14.8536C6.94732 14.7598 7 14.6326 7 14.5V12C7 11.6022 7.15804 11.2207 7.43934 10.9393C7.72064 10.658 8.10218 10.5 8.5 10.5C8.89782 10.5 9.27936 10.658 9.56066 10.9393C9.84196 11.2207 10 11.6022 10 12V14.5C10 14.6326 10.0527 14.7598 10.1464 14.8536C10.2402 14.9473 10.3674 15 10.5 15C10.6326 15 10.7598 14.9473 10.8536 14.8536C10.9473 14.7598 11 14.6326 11 14.5V12C11.0003 11.4444 10.8146 10.9047 10.4725 10.4669C11.3107 10.3519 12.0789 9.93752 12.6354 9.30029C13.1918 8.66306 13.4989 7.84601 13.5 7.00001V6.50001C13.4938 5.87862 13.3283 5.26921 13.0194 4.73001ZM12.5 7.00001C12.5 7.66305 12.2366 8.29893 11.7678 8.76778C11.2989 9.23662 10.663 9.50001 10 9.50001H7C6.33696 9.50001 5.70107 9.23662 5.23223 8.76778C4.76339 8.29893 4.5 7.66305 4.5 7.00001V6.50001C4.50613 6.00002 4.65582 5.51233 4.93125 5.09501C4.98259 5.02733 5.01585 4.94769 5.02788 4.8636C5.03991 4.77951 5.03031 4.69374 5 4.61438C4.86976 4.27851 4.80709 3.92023 4.81556 3.56009C4.82403 3.19995 4.90349 2.84501 5.04938 2.51563C5.45857 2.55965 5.85262 2.6952 6.2023 2.91224C6.55198 3.12928 6.84832 3.42223 7.06938 3.76938C7.11442 3.83982 7.17641 3.89784 7.24968 3.93813C7.32294 3.97842 7.40514 3.9997 7.48875 4.00001H9.51062C9.59455 4.00001 9.67713 3.97889 9.75075 3.93858C9.82437 3.89828 9.88666 3.84009 9.93188 3.76938C10.1529 3.4222 10.4492 3.12922 10.7989 2.91218C11.1486 2.69514 11.5427 2.55961 11.9519 2.51563C12.0976 2.8451 12.1768 3.20007 12.1851 3.56021C12.1933 3.92036 12.1304 4.27859 12 4.61438C11.9698 4.69298 11.9597 4.77788 11.9706 4.86138C11.9815 4.94487 12.0131 5.02432 12.0625 5.09251C12.3407 5.50984 12.4926 5.99853 12.5 6.50001V7.00001Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,19 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const HorizontalDotIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
d="M10.9375 10C10.9375 10.1854 10.8825 10.3667 10.7795 10.5208C10.6765 10.675 10.5301 10.7952 10.3588 10.8661C10.1875 10.9371 9.99896 10.9557 9.8171 10.9195C9.63525 10.8833 9.4682 10.794 9.33709 10.6629C9.20598 10.5318 9.11669 10.3648 9.08051 10.1829C9.04434 10.001 9.06291 9.81254 9.13386 9.64123C9.20482 9.46993 9.32498 9.32351 9.47915 9.2205C9.63332 9.11748 9.81458 9.0625 10 9.0625C10.2486 9.0625 10.4871 9.16127 10.6629 9.33709C10.8387 9.5129 10.9375 9.75136 10.9375 10ZM15.3125 9.0625C15.1271 9.0625 14.9458 9.11748 14.7917 9.2205C14.6375 9.32351 14.5173 9.46993 14.4464 9.64123C14.3754 9.81254 14.3568 10.001 14.393 10.1829C14.4292 10.3648 14.5185 10.5318 14.6496 10.6629C14.7807 10.794 14.9477 10.8833 15.1296 10.9195C15.3115 10.9557 15.5 10.9371 15.6713 10.8661C15.8426 10.7952 15.989 10.675 16.092 10.5208C16.195 10.3667 16.25 10.1854 16.25 10C16.25 9.75136 16.1512 9.5129 15.9754 9.33709C15.7996 9.16127 15.5611 9.0625 15.3125 9.0625ZM4.6875 9.0625C4.50208 9.0625 4.32082 9.11748 4.16665 9.2205C4.01248 9.32351 3.89232 9.46993 3.82136 9.64123C3.75041 9.81254 3.73184 10.001 3.76801 10.1829C3.80419 10.3648 3.89348 10.5318 4.02459 10.6629C4.1557 10.794 4.32275 10.8833 4.5046 10.9195C4.68646 10.9557 4.87496 10.9371 5.04627 10.8661C5.21757 10.7952 5.36399 10.675 5.467 10.5208C5.57002 10.3667 5.625 10.1854 5.625 10C5.625 9.75136 5.52623 9.5129 5.35041 9.33709C5.1746 9.16127 4.93614 9.0625 4.6875 9.0625Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const LinkIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.99634 4.3125C5.99634 4.00184 6.24818 3.75 6.55884 3.75H12.1875C13.3266 3.75 14.25 4.67341 14.25 5.8125V12.1844C14.25 12.495 13.9982 12.7469 13.6875 12.7469C13.3769 12.7469 13.125 12.495 13.125 12.1844V5.8125C13.125 5.76745 13.1218 5.72315 13.1157 5.6798L5.27275 13.5227C5.05308 13.7424 4.69692 13.7424 4.47725 13.5227C4.25758 13.3031 4.25758 12.9469 4.47725 12.7273L12.3202 4.88432C12.2768 4.87818 12.2326 4.875 12.1875 4.875H6.55884C6.24818 4.875 5.99634 4.62316 5.99634 4.3125Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const LockIcon: React.FC<CustomIconProps> = (props) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.99984 1.5C6.15889 1.5 4.6665 2.99238 4.6665 4.83333V6H4.49984C3.48732 6 2.6665 6.82081 2.6665 7.83333V12.8333C2.6665 13.8459 3.48732 14.6667 4.49984 14.6667H11.4998C12.5124 14.6667 13.3332 13.8459 13.3332 12.8333V7.83333C13.3332 6.82081 12.5124 6 11.4998 6H11.3332V4.83333C11.3332 2.99238 9.84079 1.5 7.99984 1.5ZM10.3332 6V4.83333C10.3332 3.54467 9.2885 2.5 7.99984 2.5C6.71117 2.5 5.6665 3.54467 5.6665 4.83333V6H10.3332ZM7.99984 8.83333C8.27598 8.83333 8.49984 9.05719 8.49984 9.33333V11.3333C8.49984 11.6095 8.27598 11.8333 7.99984 11.8333C7.72369 11.8333 7.49984 11.6095 7.49984 11.3333V9.33333C7.49984 9.05719 7.72369 8.83333 7.99984 8.83333Z"
fill="#EA580C"
/>
</CustomIcon>
);
};
@@ -0,0 +1,19 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const PencilIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
{...props}
>
<path
d="M8.625 3.125C8.90114 3.125 9.125 2.90114 9.125 2.625C9.125 2.34886 8.90114 2.125 8.625 2.125V3.125ZM15.875 9.375C15.875 9.09886 15.6511 8.875 15.375 8.875C15.0989 8.875 14.875 9.09886 14.875 9.375H15.875ZM3.44401 15.2115L3.67101 14.766L3.67101 14.766L3.44401 15.2115ZM2.78849 14.556L3.23399 14.329H3.23399L2.78849 14.556ZM14.556 15.2115L14.329 14.766L14.329 14.766L14.556 15.2115ZM15.2115 14.556L14.766 14.329L14.766 14.329L15.2115 14.556ZM2.78849 3.44401L2.34299 3.21702L2.78849 3.44401ZM3.44401 2.78849L3.21702 2.34299V2.34299L3.44401 2.78849ZM9.375 6.375L9.02145 6.02145C8.92768 6.11521 8.875 6.24239 8.875 6.375H9.375ZM9.375 8.625H8.875C8.875 8.90114 9.09886 9.125 9.375 9.125V8.625ZM11.625 8.625V9.125C11.7576 9.125 11.8848 9.07232 11.9786 8.97855L11.625 8.625ZM13.3768 2.37316L13.7304 2.72671V2.72671L13.3768 2.37316ZM15.4982 2.37316L15.8517 2.01961L15.8517 2.01961L15.4982 2.37316ZM15.6268 2.50184L15.2733 2.85539V2.85539L15.6268 2.50184ZM15.6268 4.62316L15.9804 4.97671V4.97671L15.6268 4.62316ZM12.975 14.875H5.025V15.875H12.975V14.875ZM3.125 12.975V5.025H2.125V12.975H3.125ZM5.025 3.125H8.625V2.125H5.025V3.125ZM14.875 9.375V12.975H15.875V9.375H14.875ZM5.025 14.875C4.59671 14.875 4.30556 14.8746 4.08052 14.8562C3.86131 14.8383 3.74921 14.8059 3.67101 14.766L3.21702 15.657C3.45969 15.7807 3.71804 15.8299 3.99909 15.8529C4.2743 15.8754 4.61321 15.875 5.025 15.875V14.875ZM2.125 12.975C2.125 13.3868 2.12461 13.7257 2.1471 14.0009C2.17006 14.282 2.21934 14.5403 2.34299 14.783L3.23399 14.329C3.19415 14.2508 3.16169 14.1387 3.14378 13.9195C3.12539 13.6944 3.125 13.4033 3.125 12.975H2.125ZM3.67101 14.766C3.48285 14.6701 3.32987 14.5172 3.23399 14.329L2.34299 14.783C2.53473 15.1593 2.8407 15.4653 3.21702 15.657L3.67101 14.766ZM12.975 15.875C13.3868 15.875 13.7257 15.8754 14.0009 15.8529C14.282 15.8299 14.5403 15.7807 14.783 15.657L14.329 14.766C14.2508 14.8059 14.1387 14.8383 13.9195 14.8562C13.6944 14.8746 13.4033 14.875 12.975 14.875V15.875ZM14.875 12.975C14.875 13.4033 14.8746 13.6944 14.8562 13.9195C14.8383 14.1387 14.8059 14.2508 14.766 14.329L15.657 14.783C15.7807 14.5403 15.8299 14.282 15.8529 14.0009C15.8754 13.7257 15.875 13.3868 15.875 12.975H14.875ZM14.783 15.657C15.1593 15.4653 15.4653 15.1593 15.657 14.783L14.766 14.329C14.6701 14.5172 14.5172 14.6701 14.329 14.766L14.783 15.657ZM3.125 5.025C3.125 4.59671 3.12539 4.30556 3.14378 4.08052C3.16169 3.86131 3.19415 3.74921 3.23399 3.67101L2.34299 3.21702C2.21934 3.45969 2.17006 3.71804 2.1471 3.99909C2.12461 4.2743 2.125 4.61321 2.125 5.025H3.125ZM5.025 2.125C4.61321 2.125 4.2743 2.12461 3.99909 2.1471C3.71804 2.17006 3.45969 2.21934 3.21702 2.34299L3.67101 3.23399C3.74921 3.19415 3.86131 3.16169 4.08052 3.14378C4.30556 3.12539 4.59671 3.125 5.025 3.125V2.125ZM3.23399 3.67101C3.32987 3.48285 3.48285 3.32987 3.67101 3.23399L3.21702 2.34299C2.84069 2.53473 2.53473 2.84069 2.34299 3.21702L3.23399 3.67101ZM8.875 6.375V8.625H9.875V6.375H8.875ZM9.375 9.125H11.625V8.125H9.375V9.125ZM9.72855 6.72855L13.7304 2.72671L13.0233 2.01961L9.02145 6.02145L9.72855 6.72855ZM15.1446 2.72671L15.2733 2.85539L15.9804 2.14829L15.8517 2.01961L15.1446 2.72671ZM15.2733 4.26961L11.2714 8.27145L11.9786 8.97855L15.9804 4.97671L15.2733 4.26961ZM15.2733 2.85539C15.6638 3.24592 15.6638 3.87908 15.2733 4.26961L15.9804 4.97671C16.7614 4.19566 16.7614 2.92933 15.9804 2.14829L15.2733 2.85539ZM13.7304 2.72671C14.1209 2.33619 14.7541 2.33619 15.1446 2.72671L15.8517 2.01961C15.0707 1.23856 13.8043 1.23856 13.0233 2.01961L13.7304 2.72671Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const StorageIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
d="M13 3.42576C13 4.39738 10.7614 5.18502 8 5.18502C5.23858 5.18502 3 4.39738 3 3.42576M13 3.42576C13 2.45415 10.7614 1.6665 8 1.6665C5.23858 1.6665 3 2.45415 3 3.42576M13 3.42576V12.5739C13 13.5455 10.7614 14.3332 8 14.3332C5.23858 14.3332 3 13.5455 3 12.5739V3.42576M13 7.92562C13 8.89723 10.7614 9.68488 8 9.68488C5.23858 9.68488 3 8.89723 3 7.92562"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</CustomIcon>
);
};
@@ -0,0 +1,19 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const WarningDiamondIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
d="M18.3851 9.11879L10.8812 1.6141C10.647 1.38129 10.3302 1.25061 9.99999 1.25061C9.66976 1.25061 9.35294 1.38129 9.11874 1.6141L1.61874 9.11879C1.38593 9.35299 1.25525 9.66981 1.25525 10C1.25525 10.3303 1.38593 10.6471 1.61874 10.8813L9.12265 18.386C9.35685 18.6188 9.67367 18.7495 10.0039 18.7495C10.3341 18.7495 10.6509 18.6188 10.8851 18.386L18.3891 10.8813C18.6219 10.6471 18.7525 10.3303 18.7525 10C18.7525 9.66981 18.6219 9.35299 18.3891 9.11879H18.3851ZM9.37499 6.25004C9.37499 6.08428 9.44084 5.92531 9.55805 5.8081C9.67526 5.69089 9.83423 5.62504 9.99999 5.62504C10.1658 5.62504 10.3247 5.69089 10.4419 5.8081C10.5591 5.92531 10.625 6.08428 10.625 6.25004V10.625C10.625 10.7908 10.5591 10.9498 10.4419 11.067C10.3247 11.1842 10.1658 11.25 9.99999 11.25C9.83423 11.25 9.67526 11.1842 9.55805 11.067C9.44084 10.9498 9.37499 10.7908 9.37499 10.625V6.25004ZM9.99999 14.375C9.81457 14.375 9.63332 14.3201 9.47914 14.217C9.32497 14.114 9.20481 13.9676 9.13385 13.7963C9.0629 13.625 9.04433 13.4365 9.0805 13.2546C9.11668 13.0728 9.20597 12.9057 9.33708 12.7746C9.46819 12.6435 9.63524 12.5542 9.81709 12.5181C9.99895 12.4819 10.1875 12.5004 10.3588 12.5714C10.5301 12.6424 10.6765 12.7625 10.7795 12.9167C10.8825 13.0709 10.9375 13.2521 10.9375 13.4375C10.9375 13.6862 10.8387 13.9246 10.6629 14.1005C10.4871 14.2763 10.2486 14.375 9.99999 14.375Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -22,3 +22,25 @@ export * from './EllipseIcon';
export * from './EllipsesIcon';
export * from './SnowballIcon';
export * from './NotificationBellIcon';
export * from './GithubIcon';
export * from './GitTeaIcon';
export * from './LockIcon';
export * from './PencilIcon';
export * from './CheckRadioIcon';
export * from './ChevronDownIcon';
export * from './BranchIcon';
export * from './GitHubLogo';
export * from './ClockIcon';
export * from './HorizontalDotIcon';
export * from './WarningDiamondIcon';
export * from './ArrowRightCircleIcon';
export * from './ClockOutlineIcon';
export * from './ArrowRightCircleFilledIcon';
export * from './GithubStrokeIcon';
export * from './BranchStrokeIcon';
export * from './StorageIcon';
export * from './LinkIcon';
export * from './CursorBoxIcon';
// Templates
export * from './templates';
@@ -0,0 +1,130 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from '../CustomIcon';
export const KotlinIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="41"
height="40"
viewBox="0 0 41 40"
fill="none"
{...props}
>
<g filter="url(#filter0_ii_417_1111)">
<rect
x="0.333496"
width="40"
height="40"
rx="8"
fill="url(#paint0_linear_417_1111)"
/>
<rect
x="0.74421"
y="0.410714"
width="39.1786"
height="39.1786"
rx="7.58929"
stroke="#082F56"
strokeOpacity="0.1"
strokeWidth="0.821429"
/>
<g clipPath="url(#clip0_417_1111)">
<path
d="M30.1908 29.8591H10.4766V10.1448H30.1908L20.3337 20.0019L30.1908 29.8591Z"
fill="url(#paint1_linear_417_1111)"
/>
<path
d="M30.1908 29.8591H10.4766V10.1448H30.1908L20.3337 20.0019L30.1908 29.8591Z"
fill="white"
/>
</g>
</g>
<defs>
<filter
id="filter0_ii_417_1111"
x="0.333496"
y="0"
width="40"
height="40"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="-2" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"
/>
<feBlend
mode="normal"
in2="shape"
result="effect1_innerShadow_417_1111"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.25 0"
/>
<feBlend
mode="normal"
in2="effect1_innerShadow_417_1111"
result="effect2_innerShadow_417_1111"
/>
</filter>
<linearGradient
id="paint0_linear_417_1111"
x1="40.3335"
y1="-1.19209e-06"
x2="0.333497"
y2="40"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.00343514" stopColor="#E44857" />
<stop offset="0.4689" stopColor="#C711E1" />
<stop offset="1" stopColor="#7F52FF" />
</linearGradient>
<linearGradient
id="paint1_linear_417_1111"
x1="30.1908"
y1="10.1448"
x2="10.4766"
y2="29.8591"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.00343514" stopColor="#E44857" />
<stop offset="0.4689" stopColor="#C711E1" />
<stop offset="1" stopColor="#7F52FF" />
</linearGradient>
<clipPath id="clip0_417_1111">
<rect
width="19.7143"
height="19.7143"
fill="white"
transform="translate(10.4766 10.1429)"
/>
</clipPath>
</defs>
</CustomIcon>
);
};
@@ -0,0 +1,88 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from '../CustomIcon';
export const PWAIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
{...props}
>
<g filter="url(#filter0_ii_417_1121)">
<rect width="40" height="40" rx="8" fill="#12B785" />
<rect
x="0.410714"
y="0.410714"
width="39.1786"
height="39.1786"
rx="7.58929"
stroke="#082F56"
strokeOpacity="0.1"
strokeWidth="0.821429"
/>
<path
d="M24.5832 26.25H26.2498M12.9165 24.5833V14.5833C12.9165 13.6628 13.6627 12.9167 14.5832 12.9167H25.4165C26.337 12.9167 27.0832 13.6628 27.0832 14.5833V17.0833M12.9165 24.5833H11.6665V25.4167C11.6665 26.3371 12.4127 27.0833 13.3332 27.0833H22.0832M12.9165 24.5833H22.0832M27.0832 17.0833H23.7498C22.8294 17.0833 22.0832 17.8295 22.0832 18.75V26.25C22.0832 27.1705 22.8294 27.9167 23.7498 27.9167H27.0832C28.0036 27.9167 28.7498 27.1705 28.7498 26.25V18.75C28.7498 17.8295 28.0036 17.0833 27.0832 17.0833Z"
stroke="white"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<filter
id="filter0_ii_417_1121"
x="0"
y="0"
width="40"
height="40"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="-2" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"
/>
<feBlend
mode="normal"
in2="shape"
result="effect1_innerShadow_417_1121"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.25 0"
/>
<feBlend
mode="normal"
in2="effect1_innerShadow_417_1121"
result="effect2_innerShadow_417_1121"
/>
</filter>
</defs>
</CustomIcon>
);
};
@@ -0,0 +1,98 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from '../CustomIcon';
export const ReactNativeIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
{...props}
>
<g filter="url(#filter0_ii_417_1095)">
<rect width="40" height="40" rx="8" fill="#00C0F5" />
<rect
x="0.410714"
y="0.410714"
width="39.1786"
height="39.1786"
rx="7.58929"
stroke="#082F56"
strokeOpacity="0.1"
strokeWidth="0.821429"
/>
<path
d="M20.0004 21.7819C20.9849 21.7819 21.783 20.9838 21.783 19.9993C21.783 19.0148 20.9849 18.2167 20.0004 18.2167C19.0159 18.2167 18.2178 19.0148 18.2178 19.9993C18.2178 20.9838 19.0159 21.7819 20.0004 21.7819Z"
fill="white"
/>
<path
d="M19.9998 23.6515C25.2825 23.6515 29.565 22.0164 29.565 19.9993C29.565 17.9823 25.2825 16.3472 19.9998 16.3472C14.7171 16.3472 10.4346 17.9823 10.4346 19.9993C10.4346 22.0164 14.7171 23.6515 19.9998 23.6515Z"
stroke="white"
/>
<path
d="M16.8372 21.8246C19.4786 26.3996 23.0359 29.2908 24.7827 28.2822C26.5295 27.2737 25.8043 22.7474 23.163 18.1724C20.5216 13.5975 16.9643 10.7063 15.2175 11.7148C13.4707 12.7233 14.1959 17.2496 16.8372 21.8246Z"
stroke="white"
/>
<path
d="M16.837 18.173C14.1956 22.7479 13.4704 27.2742 15.2172 28.2828C16.964 29.2913 20.5213 26.4001 23.1627 21.8251C25.8041 17.2502 26.5292 12.7238 24.7824 11.7153C23.0356 10.7068 19.4783 13.598 16.837 18.173Z"
stroke="white"
/>
</g>
<defs>
<filter
id="filter0_ii_417_1095"
x="0"
y="0"
width="40"
height="40"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="-2" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"
/>
<feBlend
mode="normal"
in2="shape"
result="effect1_innerShadow_417_1095"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.25 0"
/>
<feBlend
mode="normal"
in2="effect1_innerShadow_417_1095"
result="effect2_innerShadow_417_1095"
/>
</filter>
</defs>
</CustomIcon>
);
};
File diff suppressed because one or more lines are too long
@@ -0,0 +1,42 @@
import React, { useMemo } from 'react';
import { CustomIconProps } from '../CustomIcon';
import { ReactNativeIcon } from './ReactNativeIcon';
import { cloneIcon } from 'utils/cloneIcon';
import { PWAIcon } from './PWAIcon';
import { WebAppIcon } from './WebAppIcon';
import { KotlinIcon } from './KotlinIcon';
import { SwitfIcon } from './SwiftIcon';
const TEMPLATE_ICONS = [
'react-native',
'pwa',
'web',
'kotlin',
'swift',
] as const;
export type TemplateIconType = (typeof TEMPLATE_ICONS)[number];
export interface TemplateIconProps extends CustomIconProps {
type: TemplateIconType;
}
export const TemplateIcon = ({ type, ...props }: TemplateIconProps) => {
const renderIcon = useMemo(() => {
switch (type) {
case 'react-native':
return <ReactNativeIcon />;
case 'pwa':
return <PWAIcon />;
case 'web':
return <WebAppIcon />;
case 'kotlin':
return <KotlinIcon />;
case 'swift':
return <SwitfIcon />;
default:
throw new Error(`Invalid template icon type: ${type}`);
}
}, [type]);
return cloneIcon(renderIcon, props) as JSX.Element;
};
@@ -0,0 +1,87 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from '../CustomIcon';
export const WebAppIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="41"
height="40"
viewBox="0 0 41 40"
fill="none"
{...props}
>
<g filter="url(#filter0_ii_417_1126)">
<rect x="0.666504" width="40" height="40" rx="8" fill="#F63184" />
<rect
x="1.07722"
y="0.410714"
width="39.1786"
height="39.1786"
rx="7.58929"
stroke="#082F56"
strokeOpacity="0.1"
strokeWidth="0.821429"
/>
<path
d="M20.6667 27.9167C25.0389 27.9167 28.5833 24.3723 28.5833 20C28.5833 15.6278 25.0389 12.0833 20.6667 12.0833M20.6667 27.9167C16.2944 27.9167 12.75 24.3723 12.75 20C12.75 15.6278 16.2944 12.0833 20.6667 12.0833M20.6667 27.9167C18.8257 27.9167 17.3333 24.3723 17.3333 20C17.3333 15.6278 18.8257 12.0833 20.6667 12.0833M20.6667 27.9167C22.5076 27.9167 24 24.3723 24 20C24 15.6278 22.5076 12.0833 20.6667 12.0833M28.1667 20H13.1667"
stroke="white"
strokeLinecap="square"
/>
</g>
<defs>
<filter
id="filter0_ii_417_1126"
x="0.666504"
y="0"
width="40"
height="40"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="-2" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"
/>
<feBlend
mode="normal"
in2="shape"
result="effect1_innerShadow_417_1126"
/>
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
<feColorMatrix
type="matrix"
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.25 0"
/>
<feBlend
mode="normal"
in2="effect1_innerShadow_417_1126"
result="effect2_innerShadow_417_1126"
/>
</filter>
</defs>
</CustomIcon>
);
};
@@ -0,0 +1 @@
export * from './TemplateIcon';
@@ -85,7 +85,10 @@ export const DatePicker = ({
/>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content onInteractOutside={() => setOpen(false)}>
<Popover.Content
className="z-10"
onInteractOutside={() => setOpen(false)}
>
<Calendar
{...calendarProps}
selectRange={selectRange}
@@ -0,0 +1,7 @@
import { tv, type VariantProps } from 'tailwind-variants';
export const headingTheme = tv({
base: ['text-elements-high-em', 'font-display', 'font-normal'],
});
export type HeadingVariants = VariantProps<typeof headingTheme>;
@@ -0,0 +1,30 @@
import React, { type PropsWithChildren } from 'react';
import { headingTheme, type HeadingVariants } from './Heading.theme';
import { PolymorphicProps } from 'types/common';
export type HeadingElementType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export type HeadingProps<Element extends HeadingElementType> =
PropsWithChildren<PolymorphicProps<Element, HeadingVariants>>;
export const Heading = <Element extends HeadingElementType>({
children,
as,
className,
...props
}: HeadingProps<Element>) => {
// Component is the element that will be rendered
const Component = as ?? 'h1';
return (
<Component
{...props}
className={headingTheme({
className,
})}
>
{children}
</Component>
);
};
@@ -0,0 +1 @@
export * from './Heading';
@@ -0,0 +1,32 @@
import React, { ComponentPropsWithoutRef, ReactNode } from 'react';
import { cn } from 'utils/classnames';
interface IconWithFrameProps extends ComponentPropsWithoutRef<'div'> {
icon: ReactNode;
hasHighlight?: boolean;
}
export const IconWithFrame = ({
icon,
className,
hasHighlight = true,
...props
}: IconWithFrameProps) => {
return (
<div
className={cn(
'relative justify-center items-center gap-2.5 inline-flex',
'w-16 h-16 rounded-2xl shadow-inner',
'border border-b-[3px] border-border-interactive border-opacity-10',
'bg-controls-secondary',
className,
)}
{...props}
>
{hasHighlight && (
<div className="bottom-0 absolute w-[37px] h-px bg-gradient-to-r from-gray-0/0 via-gray-0/50 to-gray-0/0" />
)}
{icon}
</div>
);
};
@@ -0,0 +1 @@
export * from './IconWithFrame';
@@ -0,0 +1,153 @@
import { VariantProps, tv } from 'tailwind-variants';
export const selectTheme = tv({
slots: {
container: ['flex', 'flex-col', 'relative', 'gap-2'],
label: ['text-sm', 'text-elements-high-em'],
description: ['text-xs', 'text-elements-low-em'],
inputWrapper: [
'relative',
'flex',
'flex-wrap',
'gap-1',
'min-w-[200px]',
'w-full',
'rounded-lg',
'bg-transparent',
'text-elements-mid-em',
'shadow-sm',
'border',
'border-border-interactive',
'focus-ring',
'disabled:shadow-none',
'disabled:border-none',
],
input: ['outline-none'],
iconContainer: [
'absolute',
'inset-y-0',
'flex',
'items-center',
'gap-2',
'z-10',
'cursor-pointer',
],
icon: ['text-elements-mid-em'],
helperIcon: [],
helperText: ['flex', 'gap-2', 'items-center', 'text-elements-low-em'],
popover: [
'z-20',
'absolute',
'px-1',
'py-1',
'flex-col',
'gap-0.5',
'min-w-full',
'bg-surface-floating',
'shadow-dropdown',
'w-auto',
'max-h-60',
'overflow-auto',
'border',
'border-gray-200',
'rounded-xl',
],
},
variants: {
orientation: {
horizontal: {
container: [],
},
vertical: {
container: [],
},
},
variant: {
default: {
container: [],
},
danger: {
container: [],
},
},
error: {
true: {
inputWrapper: [
'outline',
'outline-offset-0',
'outline-border-danger',
'shadow-none',
'focus:outline-border-danger',
],
helperText: ['text-elements-danger'],
},
},
size: {
md: {
container: ['min-h-11'],
inputWrapper: ['min-h-11', 'text-sm', 'pl-4', 'pr-4', 'py-1'],
icon: ['h-[18px]', 'w-[18px]'],
helperText: 'text-sm',
helperIcon: ['h-5', 'w-5'],
popover: ['mt-12'],
},
sm: {
container: ['min-h-8'],
inputWrapper: ['min-h-8', 'text-xs', 'pl-3', 'pr-3', 'py-0.5'],
icon: ['h-4', 'w-4'],
helperText: 'text-xs',
helperIcon: ['h-4', 'w-4'],
popover: ['mt-9'],
},
},
isOpen: {
true: {
popover: ['flex'],
},
false: {
popover: ['hidden'],
},
},
hasValue: {
true: '',
},
searchable: {
true: '',
false: {
input: ['cursor-pointer'],
},
},
hideValues: {
true: {
input: ['placeholder:text-elements-mid-em'],
},
},
},
compoundVariants: [
{
size: 'md',
hasValue: true,
class: {
inputWrapper: ['pl-1'],
},
},
{
size: 'sm',
hasValue: true,
class: {
inputWrapper: ['pl-0.5'],
},
},
],
defaultVariants: {
orientation: 'horizontal',
variant: 'default',
size: 'md',
state: 'default',
error: false,
isOpen: false,
hasValue: false,
},
});
export type SelectTheme = VariantProps<typeof selectTheme>;
@@ -0,0 +1,438 @@
import React, {
ReactNode,
useState,
ComponentPropsWithoutRef,
useMemo,
useCallback,
MouseEvent,
useRef,
useEffect,
} from 'react';
import { useMultipleSelection, useCombobox } from 'downshift';
import { SelectTheme, selectTheme } from './Select.theme';
import {
ChevronDownIcon,
CrossIcon,
WarningIcon,
} from 'components/shared/CustomIcon';
import { cloneIcon } from 'utils/cloneIcon';
import { cn } from 'utils/classnames';
import { SelectItem, EmptySelectItem } from './SelectItem';
import { SelectValue } from './SelectValue';
export type SelectOption = {
/**
* The value of the option
*/
value: string;
/**
* The label of the option
*/
label: string;
/**
* The description of the option
*/
description?: string;
/**
* Custom left icon for the option
*/
leftIcon?: ReactNode;
/**
* Custom right icon for the option
*/
rightIcon?: ReactNode;
/**
* Whether the option is disabled
*/
disabled?: boolean;
};
/**
* The orientation of the select
*/
export type SelectOrientation = 'horizontal' | 'vertical';
interface SelectProps
extends Omit<
ComponentPropsWithoutRef<'input'>,
'size' | 'value' | 'onChange'
>,
SelectTheme {
/**
* The options of the select
*/
options: SelectOption[];
/**
* The label of the select
*/
label?: string;
/**
* The description of the select
*/
description?: string;
/**
* Wheter the select is multiple or not
*/
multiple?: boolean;
/**
* Wheter the select is searchable or not
*/
searchable?: boolean;
/**
* Wheter the select is clearable or not
*/
clearable?: boolean;
/**
* Custom left icon for the select
*/
leftIcon?: ReactNode;
/**
* Custom right icon for the select
*/
rightIcon?: ReactNode;
/**
* The helper text of the select
*/
helperText?: string;
/**
* Show the values of the select if it's multiple
*/
hideValues?: boolean;
/**
* The value of the select
*/
value?: SelectOption | SelectOption[];
/**
* Callback function when reset the select
*/
onClear?: () => void;
/**
* Callback function when the value of the select changes
*/
onChange?: (value: SelectOption | SelectOption[]) => void;
}
export const Select = ({
options,
multiple = false,
searchable = false,
clearable,
size,
error,
orientation = 'horizontal',
variant,
label,
description,
leftIcon,
rightIcon,
helperText,
hideValues = false,
placeholder: placeholderProp = 'Select an option',
value,
onChange,
onClear,
}: SelectProps) => {
const theme = selectTheme({ size, error, variant, orientation });
const [inputValue, setInputValue] = useState('');
const [selectedItem, setSelectedItem] = useState<SelectOption | null>(null);
const [dropdownOpen, setDropdownOpen] = useState(false);
const [dropdownPosition, setDropdownPosition] = useState<'top' | 'bottom'>(
'bottom',
);
const popoverRef = useRef(null); // Ref for the popover
const inputWrapperRef = useRef(null); // Ref for the input wrapper
// Calculate and update popover position
useEffect(() => {
if (dropdownOpen && popoverRef.current && inputWrapperRef.current) {
const popover = popoverRef.current;
// @ts-expect-error we know it's not null lol
const input = inputWrapperRef.current.getBoundingClientRect();
const spaceBelow = window.innerHeight - input.bottom;
const spaceAbove = input.top;
// @ts-expect-error we know it's not null lol
const popoverHeight = popover.offsetHeight;
// Determine if there's enough space below
if (spaceBelow >= popoverHeight) {
setDropdownPosition('bottom');
} else if (spaceAbove >= popoverHeight) {
setDropdownPosition('top');
} else {
// Default to bottom if neither has enough space, but you could also set logic to choose the side with more space
setDropdownPosition('bottom');
}
}
}, [dropdownOpen]); // Re-calculate whenever the dropdown is opened
useEffect(() => {
// If multiple selection is enabled, ensure the internal state is an array
if (multiple) {
if (Array.isArray(value)) {
// Directly use the provided array
setSelectedItems(value);
} else {
// Reset or set to empty array if the value is not an array
setSelectedItems([]);
}
} else {
// For single selection, directly set the selected item
setSelectedItem(value as SelectOption);
}
}, [value, multiple]);
const handleSelectedItemChange = (selectedItem: SelectOption | null) => {
setSelectedItem(selectedItem);
setInputValue(selectedItem ? selectedItem.label : '');
onChange?.(selectedItem as SelectOption);
};
const {
getSelectedItemProps,
getDropdownProps,
addSelectedItem,
removeSelectedItem,
selectedItems,
setSelectedItems,
reset,
} = useMultipleSelection<SelectOption>({
onSelectedItemsChange: multiple
? undefined
: ({ selectedItems }) => {
handleSelectedItemChange(selectedItems?.[0] || null);
},
});
const filteredItems = useMemo(() => {
// Show all items if the dropdown is not multiple and not searchable
if (!multiple && !searchable) return options;
// Show only the items that match the input value
if (!multiple && searchable) {
return options.filter((item) =>
item.label.toLowerCase().includes(inputValue.toLowerCase()),
);
}
// Show only the items that match the input value and are not already selected
return options.filter(
(item) =>
item.label.toLowerCase().includes(inputValue.toLowerCase()) &&
!(
multiple && searchable // If the dropdown is multiple and searchable, show filtered items
? selectedItems
: []
).includes(item),
);
}, [options, inputValue, selectedItem, selectedItems, multiple, searchable]);
const {
isOpen,
getMenuProps,
getInputProps,
highlightedIndex,
getItemProps,
openMenu,
} = useCombobox({
items: filteredItems,
// @ts-expect-error there are two params but we don't need the second one
isItemDisabled: (item) => item.disabled,
onInputValueChange: ({ inputValue = '' }) => setInputValue(inputValue),
onSelectedItemChange: ({ selectedItem }) => {
if (!multiple && selectedItem) {
handleSelectedItemChange(selectedItem);
} else if (multiple && selectedItem) {
// If the item is not already selected, add it to the selected items
if (!selectedItems.includes(selectedItem)) {
addSelectedItem(selectedItem);
// Callback for `onChange`
const newSelectedItems = [...selectedItems, selectedItem];
onChange?.(newSelectedItems);
} else {
// If the item is already selected, remove it from the selected items
removeSelectedItem(selectedItem);
// Callback for `onChange`
const newSelectedItems = selectedItems.filter(
(item) => selectedItem !== item,
);
onChange?.(newSelectedItems);
}
setInputValue('');
}
},
onIsOpenChange: ({ isOpen }) => {
setDropdownOpen(isOpen ?? false);
if (isOpen && !multiple && selectedItem && searchable) {
setInputValue('');
}
},
selectedItem: multiple ? null : selectedItem,
// TODO: Make the input value empty when the dropdown is open, has a value, it is not multiple, and searchable
itemToString: (item) => (item && !multiple ? item.label : ''),
});
const isSelected = useCallback(
(item: SelectOption) =>
multiple ? selectedItems.includes(item) : selectedItem === item,
[selectedItems, selectedItem, multiple],
);
const handleClear = (e: MouseEvent<SVGSVGElement, globalThis.MouseEvent>) => {
e.stopPropagation();
reset();
setSelectedItem(null);
setInputValue('');
onClear?.();
};
const renderLabels = useMemo(
() => (
<div className="space-y-1">
<p className={theme.label()}>{label}</p>
<p className={theme.description()}>{description}</p>
</div>
),
[theme, label, description],
);
const renderLeftIcon = useMemo(() => {
return (
<div className={theme.iconContainer({ class: 'left-0 pl-4' })}>
{cloneIcon(leftIcon, { className: theme.icon(), 'aria-hidden': true })}
</div>
);
}, [cloneIcon, theme, leftIcon]);
const renderRightIcon = useMemo(() => {
return (
<div className={theme.iconContainer({ class: 'pr-4 right-0' })}>
{clearable && (selectedItems.length > 0 || selectedItem) && (
<CrossIcon
className={theme.icon({ class: 'h-4 w-4' })}
onClick={handleClear}
/>
)}
{rightIcon ? (
cloneIcon(rightIcon, { className: theme.icon(), 'aria-hidden': true })
) : (
<ChevronDownIcon className={theme.icon()} />
)}
</div>
);
}, [cloneIcon, theme, rightIcon]);
const renderHelperText = useMemo(
() => (
<div className={theme.helperText()}>
{error &&
cloneIcon(<WarningIcon className={theme.helperIcon()} />, {
'aria-hidden': true,
})}
<p>{helperText}</p>
</div>
),
[cloneIcon, error, theme, helperText],
);
const isMultipleHasValue = multiple && selectedItems.length > 0;
const isMultipleHasValueButNotSearchable =
multiple && !searchable && selectedItems.length > 0;
const displayPlaceholder = useMemo(() => {
if (hideValues && isMultipleHasValue) {
return `${selectedItems.length} selected`;
}
if (isMultipleHasValueButNotSearchable) {
return '';
}
return placeholderProp;
}, [hideValues, multiple, selectedItems.length, placeholderProp]);
return (
<div className={theme.container()}>
{/* Label & description */}
{renderLabels}
{/* Input */}
<div
ref={inputWrapperRef}
className={theme.inputWrapper({
hasValue: isMultipleHasValue && !hideValues,
})}
onClick={() => !dropdownOpen && openMenu()}
>
{/* Left icon */}
{leftIcon && renderLeftIcon}
{/* Multiple input values */}
{isMultipleHasValue &&
!hideValues &&
selectedItems.map((item, index) => (
<SelectValue
key={`selected-item-${index}`}
{...getSelectedItemProps({ selectedItem: item, index })}
option={item}
size={size}
onDelete={removeSelectedItem}
/>
))}
{/* Single input value or searchable area */}
<input
{...getInputProps(getDropdownProps())}
placeholder={displayPlaceholder}
// Control readOnly based on searchable
readOnly={!searchable || hideValues}
className={cn(
theme.input({
searchable,
hideValues: hideValues && selectedItems.length > 0,
}),
{
// Make the input width smaller because we don't need it (not searchable)
'w-6': isMultipleHasValueButNotSearchable && !hideValues,
// Add margin to the X icon
'ml-6': isMultipleHasValueButNotSearchable && clearable,
},
)}
/>
{/* Right icon */}
{renderRightIcon}
</div>
{/* Helper text */}
{renderHelperText}
{/* Popover */}
<ul
{...getMenuProps({ ref: popoverRef }, { suppressRefError: true })}
id="popover"
ref={popoverRef}
className={cn(theme.popover({ isOpen }), {
// Position the popover based on the dropdown position
'top-[27.5%]': dropdownPosition === 'bottom' && !label,
'top-[35%]': dropdownPosition === 'bottom' && label,
'top-[42.5%]': dropdownPosition === 'bottom' && label && description,
'bottom-[92.5%]': dropdownPosition === 'top' && !label,
'bottom-[75%]': dropdownPosition === 'top' && label,
'bottom-[65%]': dropdownPosition === 'top' && label && description,
})}
>
{isOpen && filteredItems.length !== 0 ? (
filteredItems.map((item, index) => (
<SelectItem
{...getItemProps({ item, index })}
key={item.value}
selected={isSelected(item)}
option={item}
hovered={highlightedIndex === index}
orientation={orientation}
variant={variant}
/>
))
) : (
<EmptySelectItem />
)}
</ul>
</div>
);
};
@@ -0,0 +1,56 @@
import { tv, VariantProps } from 'tailwind-variants';
export const selectItemTheme = tv({
slots: {
wrapper: [
'p-2',
'gap-3',
'flex',
'items-start',
'justify-between',
'rounded-lg',
'group',
'data-[disabled]:cursor-not-allowed',
],
icon: ['h-4.5', 'w-4.5', 'text-elements-high-em'],
content: ['flex', 'flex-1', 'whitespace-nowrap'],
label: [
'text-sm',
'text-elements-high-em',
'tracking-[-0.006em]',
'data-[disabled]:text-elements-disabled',
],
description: [
'text-xs',
'text-elements-low-em',
'data-[disabled]:text-elements-disabled',
],
dot: ['h-1', 'w-1', 'rounded-full', 'bg-border-interactive-hovered/[0.14]'],
},
variants: {
orientation: {
horizontal: {
wrapper: ['items-center'],
content: ['flex-row', 'items-center', 'gap-2'],
},
vertical: {
content: ['flex-col', 'gap-0.5'],
},
},
variant: {
default: {
wrapper: [],
},
danger: {
wrapper: [],
},
},
active: {
true: {
wrapper: ['bg-base-bg-emphasized', 'data-[disabled]:bg-transparent'],
},
},
},
});
export type SelectItemTheme = VariantProps<typeof selectItemTheme>;
@@ -0,0 +1,95 @@
import React, { forwardRef, ComponentPropsWithoutRef, useMemo } from 'react';
import { Overwrite, UseComboboxGetItemPropsReturnValue } from 'downshift';
import { SelectOption, SelectOrientation } from 'components/shared/Select';
import { selectItemTheme, SelectItemTheme } from './SelectItem.theme';
import { cloneIcon } from 'utils/cloneIcon';
import { cn } from 'utils/classnames';
import { CheckRadioIcon } from 'components/shared/CustomIcon';
import { OmitCommon } from 'types/common';
/**
* Represents a type that merges ComponentPropsWithoutRef<'li'> with certain exclusions.
* @type {MergedComponentPropsWithoutRef}
*/
type MergedComponentPropsWithoutRef = OmitCommon<
ComponentPropsWithoutRef<'li'>,
Omit<
Overwrite<UseComboboxGetItemPropsReturnValue, SelectOption[]>,
'index' | 'item'
>
>;
export interface SelectItemProps
extends MergedComponentPropsWithoutRef,
SelectItemTheme {
selected: boolean;
option: SelectOption;
orientation?: SelectOrientation;
hovered?: boolean;
}
const SelectItem = forwardRef<HTMLLIElement, SelectItemProps>(
(
{ className, selected, orientation, hovered, variant, option, ...props },
ref,
) => {
const theme = selectItemTheme({ active: hovered, orientation, variant });
const { label, description, leftIcon, rightIcon, disabled } = option;
const renderRightIcon = useMemo(() => {
if (rightIcon) {
return cloneIcon(rightIcon, { className: theme.icon() });
} else if (selected) {
return (
<CheckRadioIcon
className={cn(theme.icon(), 'text-controls-primary')}
/>
);
}
return null;
}, [rightIcon, theme, cloneIcon, cn, selected]);
return (
<li
{...props}
ref={ref}
className={theme.wrapper({ className })}
data-disabled={disabled}
>
{leftIcon && cloneIcon(leftIcon, { className: theme.icon() })}
<div className={theme.content()}>
<p className={theme.label()} data-disabled={disabled}>
{label}
</p>
{orientation === 'horizontal' && <span className={theme.dot()} />}
{description && (
<p className={theme.description()} data-disabled={disabled}>
{description}
</p>
)}
</div>
{renderRightIcon}
</li>
);
},
);
SelectItem.displayName = 'SelectItem';
/**
* Represents an empty select item.
* @returns {JSX.Element} - A JSX element representing the empty select item.
*/
export const EmptySelectItem = () => {
const theme = selectItemTheme();
return (
<li className={theme.wrapper()}>
<p className={theme.label({ class: 'text-elements-disabled' })}>
No results found
</p>
</li>
);
};
export { SelectItem };
@@ -0,0 +1 @@
export * from './SelectItem';
@@ -0,0 +1,30 @@
import { tv, VariantProps } from 'tailwind-variants';
export const selectValueTheme = tv({
slots: {
wrapper: [
'flex',
'items-center',
'gap-1',
'pl-2',
'pr-2',
'rounded-md',
'text-elements-mid-em',
'bg-base-bg-emphasized',
'hover:bg-base-bg-emphasized/80',
],
icon: ['h-3.5', 'w-3.5'],
},
variants: {
size: {
sm: {
wrapper: ['pl-1', 'pr-0.5', 'gap-0.5'],
},
md: {
wrapper: ['pl-2', 'pr-1', 'gap-1'],
},
},
},
});
export type SelectValueTheme = VariantProps<typeof selectValueTheme>;
@@ -0,0 +1,60 @@
import React, { forwardRef, ComponentPropsWithoutRef } from 'react';
import {
Overwrite,
UseMultipleSelectionGetSelectedItemReturnValue,
} from 'downshift';
import { SelectValueTheme, selectValueTheme } from './SelectValue.theme';
import { OmitCommon } from 'types/common';
import { SelectOption } from 'components/shared/Select';
import { Button } from 'components/shared/Button';
import { CrossIcon } from 'components/shared/CustomIcon';
type MergedComponentPropsWithoutRef = OmitCommon<
ComponentPropsWithoutRef<'span'>,
Omit<
Overwrite<UseMultipleSelectionGetSelectedItemReturnValue, SelectOption[]>,
'index' | 'selectedItem'
>
>;
export interface SelectValueProps
extends MergedComponentPropsWithoutRef,
SelectValueTheme {
/**
* The option of the select value
*/
option: SelectOption;
/**
* The function to call when the delete button is clicked
* @param item
* @returns none;
*/
onDelete?: (item: SelectOption) => void;
}
/**
* The SelectValue component is used to display the selected value of a select component
*/
const SelectValue = forwardRef<HTMLSpanElement, SelectValueProps>(
({ className, size, option, onDelete, ...props }, ref) => {
const theme = selectValueTheme();
return (
<span {...props} ref={ref} className={theme.wrapper({ className, size })}>
{option.label}
<Button
onClick={() => onDelete?.(option)}
iconOnly
variant="unstyled"
size="xs"
>
<CrossIcon className={theme.icon()} />
</Button>
</span>
);
},
);
SelectValue.displayName = 'SelectValue';
export { SelectValue };
@@ -0,0 +1 @@
export * from './SelectValue';
@@ -0,0 +1 @@
export * from './Select';
@@ -5,8 +5,8 @@ import { Organization } from 'gql-client';
import { Option } from '@material-tailwind/react';
import { useDisconnect } from 'wagmi';
import { useGQLClient } from '../context/GQLClientContext';
import AsyncSelect from './shared/AsyncSelect';
import { useGQLClient } from 'context/GQLClientContext';
import AsyncSelect from 'components/shared/AsyncSelect';
import {
ChevronGrabberHorizontal,
FolderIcon,
@@ -14,10 +14,11 @@ import {
LifeBuoyIcon,
QuestionMarkRoundIcon,
SettingsSlidersIcon,
} from './shared/CustomIcon';
} from 'components/shared/CustomIcon';
import { Tabs } from 'components/shared/Tabs';
import { Heading } from 'components/shared/Heading';
const Sidebar = () => {
export const Sidebar = () => {
const { orgSlug } = useParams();
const navigate = useNavigate();
const client = useGQLClient();
@@ -42,20 +43,20 @@ const Sidebar = () => {
}, [disconnect, navigate]);
return (
<div className="flex flex-col h-full p-4 pt-10">
<div className="grow">
<Link to={`/${orgSlug}`}>
<div className="flex items-center space-x-3 mb-10 ml-2">
<img
src="/logo.svg"
alt="Snowball Logo"
className="h-8 w-8 rounded-lg"
/>
<span className="text-2xl font-bold text-snowball-900">
Snowball
</span>
</div>
</Link>
<nav className="flex flex-col h-full px-6 py-8 gap-9">
{/* Logo */}
<Link to={`/${orgSlug}`}>
<div className="flex items-center gap-3 px-2">
<img
src="/logo.svg"
alt="Snowball Logo"
className="h-10 w-10 rounded-lg"
/>
<Heading className="text-[24px] font-semibold">Snowball</Heading>
</div>
</Link>
{/* Switch organization */}
<div className="flex flex-1 flex-col gap-4">
<AsyncSelect
containerProps={{ className: 'h-14 border-none' }}
labelProps={{ className: 'before:border-none after:border-none' }}
@@ -82,7 +83,7 @@ const Sidebar = () => {
)}
arrow={<ChevronGrabberHorizontal className="h-4 w-4 text-gray-500" />}
>
{/* TODO: Show label organization and manage in option */}
{/* // TODO: Show label organization and manage in option */}
{organizations.map((org) => (
<Option key={org.id} value={org.slug}>
<div className="flex items-center space-x-3">
@@ -99,13 +100,17 @@ const Sidebar = () => {
</Option>
))}
</AsyncSelect>
<Tabs defaultValue="Projects" orientation="vertical" className="mt-10">
<Tabs defaultValue="Projects" orientation="vertical">
<Tabs.List>
{[
{ title: 'Projects', icon: <FolderIcon /> },
{ title: 'Settings', icon: <SettingsSlidersIcon /> },
].map(({ title, icon }, index) => (
<NavLink to={`/${orgSlug}/${title}`} key={index}>
{ title: 'Projects', url: `/${orgSlug}/`, icon: <FolderIcon /> },
{
title: 'Settings',
url: `/${orgSlug}/settings`,
icon: <SettingsSlidersIcon />,
},
].map(({ title, icon, url }, index) => (
<NavLink to={url} key={index}>
<Tabs.Trigger icon={icon} value={title}>
{title}
</Tabs.Trigger>
@@ -114,9 +119,10 @@ const Sidebar = () => {
</Tabs.List>
</Tabs>
</div>
<div className="grow flex flex-col justify-end mb-8">
{/* Bottom navigation */}
<div className="flex flex-col justify-end">
<Tabs defaultValue="Projects" orientation="vertical">
{/* TODO: use proper link buttons */}
{/* // TODO: use proper link buttons */}
<Tabs.List>
<Tabs.Trigger icon={<GlobeIcon />} value="">
<a className="cursor-pointer" onClick={handleLogOut}>
@@ -132,8 +138,6 @@ const Sidebar = () => {
</Tabs.List>
</Tabs>
</div>
</div>
</nav>
);
};
export default Sidebar;
@@ -0,0 +1 @@
export * from './Sidebar';
@@ -8,7 +8,6 @@ export const tabsTheme = tv({
triggerWrapper: [
// Horizontal default
'px-1',
'pb-5',
'cursor-default',
'select-none',
'text-elements-low-em',
@@ -23,7 +22,6 @@ export const tabsTheme = tv({
// Vertical
'data-[orientation=vertical]:px-3',
'data-[orientation=vertical]:py-3',
'data-[orientation=vertical]:min-w-[240px]',
'data-[orientation=vertical]:w-full',
'data-[orientation=vertical]:focus-ring',
'data-[orientation=vertical]:rounded-xl',
@@ -56,11 +54,16 @@ export const tabsTheme = tv({
'outline-none',
'leading-none',
'tracking-[-0.006em]',
'text-sm',
'rounded-md',
// Horizontal default
'data-[orientation=horizontal]:focus-ring',
'data-[orientation=horizontal]:h-10',
// select direct child of data-[orientation=horizontal]
'[&[data-orientation=horizontal]_>_*]:h-full',
// Vertical
'data-[orientation=vertical]:gap-2',
'data-[orientation=vertical]:justify-start',
],
triggerList: [
'flex',
@@ -35,20 +35,23 @@ const TabsTrigger = forwardRef<
// Disabled focus state for horizontal tabs
tabIndex={orientation === 'horizontal' ? -1 : undefined}
className={triggerWrapper({ className })}
asChild
{...props}
>
{/* Need to add button in the trigger children because there's focus state inside the children */}
<button
data-orientation={orientation}
// Disabled focus state for vertical tabs
tabIndex={orientation === 'vertical' ? -1 : undefined}
className={trigger()}
>
{/* Put the icon on the left of the text for veritcal tab */}
{orientation === 'vertical' && icon}
{children}
{/* Put the icon on the right of the text for horizontal tab */}
{orientation === 'horizontal' && icon}
<button className={triggerWrapper({ className })}>
<span
// Disabled focus state for vertical tabs
tabIndex={orientation === 'horizontal' ? 0 : -1}
data-orientation={orientation}
className={trigger()}
>
{/* Put the icon on the left of the text for veritcal tab */}
{orientation === 'vertical' && icon}
{children}
{/* Put the icon on the right of the text for horizontal tab */}
{orientation === 'horizontal' && icon}
</span>
</button>
</Trigger>
);
@@ -5,25 +5,25 @@ export const tagTheme = tv(
{
slots: {
wrapper: ['flex', 'gap-1.5', 'rounded-lg', 'border'],
icon: ['h-4', 'w-4'],
icon: [],
label: ['font-inter', 'text-xs'],
},
variants: {
type: {
attention: {
icon: ['text-elements-warning'],
wrapper: ['text-elements-warning'],
},
negative: {
icon: ['text-elements-danger'],
wrapper: ['text-elements-danger'],
},
positive: {
icon: ['text-elements-success'],
wrapper: ['text-elements-success'],
},
emphasized: {
icon: ['text-elements-on-secondary'],
wrapper: ['text-elements-on-secondary'],
},
neutral: {
icon: ['text-elements-mid-em'],
wrapper: ['text-elements-mid-em'],
},
},
style: {
@@ -36,9 +36,11 @@ export const tagTheme = tv(
size: {
sm: {
wrapper: ['px-2', 'py-2'],
icon: ['h-4', 'w-4'],
},
xs: {
wrapper: ['px-2', 'py-1.5'],
wrapper: ['px-2', 'py-1'],
icon: ['h-3', 'w-3'],
},
},
},

Some files were not shown because too many files have changed in this diff Show More