48
.github/workflows/ci-chrome-extension.yaml
vendored
|
|
@ -1,48 +0,0 @@
|
|||
name: CI Chrome Extension
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-chrome-extension/**
|
||||
chrome-extension-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VITE_SERVER_BASE_URL: http://localhost:3000
|
||||
VITE_FRONT_BASE_URL: http://localhost:3001
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Chrome Extension / Run build
|
||||
run: npx nx build twenty-chrome-extension
|
||||
ci-chrome-extension-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, chrome-extension-build]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
4
.vscode/twenty.code-workspace
vendored
|
|
@ -4,10 +4,6 @@
|
|||
"name": "ROOT",
|
||||
"path": "../"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-chrome-extension",
|
||||
"path": "../packages/twenty-chrome-extension"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-docker",
|
||||
"path": "../packages/twenty-docker"
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@ packages/
|
|||
├── twenty-ui/ # Shared UI components library
|
||||
├── twenty-shared/ # Common types and utilities
|
||||
├── twenty-emails/ # Email templates with React Email
|
||||
├── twenty-chrome-extension/ # Chrome extension
|
||||
├── twenty-website/ # Next.js documentation website
|
||||
├── twenty-zapier/ # Zapier integration
|
||||
└── twenty-e2e-testing/ # Playwright E2E tests
|
||||
|
|
|
|||
|
|
@ -349,7 +349,6 @@
|
|||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/twenty-chrome-extension",
|
||||
"packages/twenty-front",
|
||||
"packages/twenty-server",
|
||||
"packages/twenty-emails",
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
VITE_SERVER_BASE_URL=https://api.twenty.com
|
||||
VITE_FRONT_BASE_URL=https://app.twenty.com
|
||||
VITE_MODE=production
|
||||
|
||||
# Used to generate packages/twenty-chrome-extension/src/generated/graphql.tsx
|
||||
AUTH_TOKEN=<YOUR-TOKEN-HERE>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
extends: ['./.eslintrc.cjs'],
|
||||
rules: {
|
||||
'no-console': 'error',
|
||||
},
|
||||
};
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: ['../../.eslintrc.global.cjs', '../../.eslintrc.react.cjs'],
|
||||
ignorePatterns: [
|
||||
'node_modules',
|
||||
'dist',
|
||||
'**/generated/*',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
parserOptions: {
|
||||
project: ['packages/twenty-chrome-extension/tsconfig.*.json'],
|
||||
},
|
||||
rules: {
|
||||
'@nx/workspace-explicit-boolean-predicates-in-if': 'warn',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
24
packages/twenty-chrome-extension/.gitignore
vendored
|
|
@ -1,24 +0,0 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -1 +0,0 @@
|
|||
src/generated
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
# Twenty Chrome Extension.
|
||||
|
||||
This extension allows you to save `company` and `people` information to your twenty workspace directly from LinkedIn.
|
||||
|
||||
To install the extension in development mode with hmr (hot module reload), follow these steps.
|
||||
|
||||
- STEP 1: Clone the repository and run `yarn install` in the root directory.
|
||||
|
||||
- STEP 2: Once the dependencies installation succeeds, create a file with env variables by executing the following command in the root directory.
|
||||
|
||||
```
|
||||
cp ./packages/twenty-chrome-extension/.env.example ./packages/twenty-chrome-extension/.env
|
||||
```
|
||||
|
||||
- STEP 3 (optional): Update values of the environment variables to match those of your instance for `twenty-front` and `twenty-server`. If you want to work on your local machine with the default setup from `Twenty Docs`, replace everything in the .env file with the following.
|
||||
|
||||
```
|
||||
VITE_SERVER_BASE_URL=http://localhost:3000
|
||||
VITE_FRONT_BASE_URL=http://localhost:3001
|
||||
```
|
||||
|
||||
- STEP 4: Now, execute the following command in the root directory to start up the development server on Port 3002. This will create a `dist` folder in `twenty-chrome-extension`.
|
||||
|
||||
```
|
||||
npx nx start twenty-chrome-extension
|
||||
```
|
||||
|
||||
- STEP 5: Open Google Chrome and head to the extensions page by typing `chrome://extensions` in the address bar.
|
||||
|
||||
<p align="center">
|
||||
<img src="../twenty-chrome-extension/public/readme-images/01-img-one.png" width="600" />
|
||||
</p>
|
||||
|
||||
- STEP 6: Turn on the `Developer mode` from the top-right corner and click `Load unpacked`.
|
||||
|
||||
<p align="center">
|
||||
<img src="../twenty-chrome-extension/public/readme-images/02-img-two.png" width="600" />
|
||||
</p>
|
||||
|
||||
- STEP 7: Select the `dist` folder from `twenty-chrome-extension`.
|
||||
|
||||
<p align="center">
|
||||
<img src="../twenty-chrome-extension/public/readme-images/03-img-three.png" width="600" />
|
||||
</p>
|
||||
|
||||
- STEP 8: This opens up the `options` page, where you must enter your API key.
|
||||
|
||||
<p align="center">
|
||||
<img src="../twenty-chrome-extension/public/readme-images/04-img-four.png" width="600" />
|
||||
</p>
|
||||
|
||||
- STEP 9: Reload any LinkedIn page that you opened before installing the extension for seamless experience.
|
||||
|
||||
- STEP 10: Visit any individual or company profile on LinkedIn and click the `Add to Twenty` button to test.
|
||||
|
||||
<p align="center">
|
||||
<img src="../twenty-chrome-extension/public/readme-images/05-img-five.png" width="600" />
|
||||
</p>
|
||||
|
||||
To install the extension in production mode without hmr (hot module reload), replace the command in STEP FOUR with `npx nx build twenty-chrome-extension`. You may or may not want to execute STEP THREE based on your requirements.
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { CodegenConfig } from '@graphql-codegen/cli';
|
||||
|
||||
const config: CodegenConfig = {
|
||||
schema: [
|
||||
{
|
||||
[`${import.meta.env.VITE_SERVER_BASE_URL}/graphql`]: {
|
||||
// some of the mutations and queries require authorization (people or companies)
|
||||
// so to regenerate the schema with types we need to pass an auth token
|
||||
headers: {
|
||||
Authorization: `Bearer ${import.meta.env.AUTH_TOKEN}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
overwrite: true,
|
||||
documents: ['./src/**/*.ts', '!src/generated/**/*.*'],
|
||||
generates: {
|
||||
'./src/generated/graphql.tsx': {
|
||||
plugins: [
|
||||
'typescript',
|
||||
'typescript-operations',
|
||||
'typescript-react-apollo',
|
||||
],
|
||||
config: {
|
||||
skipTypename: true,
|
||||
withHooks: true,
|
||||
withHOC: false,
|
||||
withComponent: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="images/icons/android/android-launchericon-48-48.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Twenty</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/options/loading-index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"name": "twenty-chrome-extension",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "npx vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-shared": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="images/icons/android/android-launchericon-48-48.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Twenty</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/options/page-inaccessible-index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
{
|
||||
"name": "twenty-chrome-extension",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"tags": ["scope:frontend"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "{projectRoot}/dist"
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"start": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["build"],
|
||||
"options": {
|
||||
"cwd": "packages/twenty-chrome-extension",
|
||||
"command": "VITE_MODE=development vite"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"executor": "@nx/vite:preview-server",
|
||||
"options": {
|
||||
"buildTarget": "twenty-chrome-extension:build",
|
||||
"port": 3002,
|
||||
"open": true
|
||||
}
|
||||
},
|
||||
"reset:env": {
|
||||
"executor": "nx:run-commands",
|
||||
"inputs": ["{projectRoot}/.env.example"],
|
||||
"outputs": ["{projectRoot}/.env"],
|
||||
"cache": true,
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "cp .env.example .env"
|
||||
}
|
||||
},
|
||||
"typecheck": {},
|
||||
"lint": {
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"{projectRoot}/src/**/*.{ts,tsx,json}",
|
||||
"{projectRoot}/package.json"
|
||||
],
|
||||
"maxWarnings": 0,
|
||||
"reportUnusedDisableDirectives": "error"
|
||||
},
|
||||
"configurations": {
|
||||
"ci": { "eslintConfig": "{projectRoot}/.eslintrc-ci.cjs" },
|
||||
"fix": {}
|
||||
}
|
||||
},
|
||||
"fmt": {
|
||||
"options": {
|
||||
"files": "src"
|
||||
},
|
||||
"configurations": {
|
||||
"fix": {}
|
||||
}
|
||||
},
|
||||
"graphql:generate": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "graphql-codegen"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 790 B |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 350 KiB |
|
Before Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 650 KiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 830 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -1,22 +0,0 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="images/icons/android/android-launchericon-48-48.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Twenty</title>
|
||||
<style>
|
||||
/* Reset margin and padding */
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%; /* Ensure body takes full viewport height */
|
||||
overflow: hidden; /* Prevents scrollbars from appearing */
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/options/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
// Open options page programmatically in a new tab.
|
||||
// chrome.runtime.onInstalled.addListener((details) => {
|
||||
// if (details.reason === 'install') {
|
||||
// openOptionsPage();
|
||||
// }
|
||||
// });
|
||||
|
||||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
|
||||
|
||||
// This listens for an event from other parts of the extension, such as the content script, and performs the required tasks.
|
||||
// The cases themselves are labelled such that their operations are reflected by their names.
|
||||
chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
|
||||
switch (message.action) {
|
||||
case 'getActiveTab': {
|
||||
// e.g. "https://linkedin.com/company/twenty/"
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
|
||||
if (isDefined(tab) && isDefined(tab.id)) {
|
||||
sendResponse({ tab });
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'openSidepanel': {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
|
||||
if (isDefined(tab) && isDefined(tab.id)) {
|
||||
chrome.sidePanel.open({ tabId: tab.id });
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
chrome.tabs.onUpdated.addListener(async (tabId, _, tab) => {
|
||||
const isDesiredRoute =
|
||||
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) ||
|
||||
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/);
|
||||
|
||||
if (tab.active === true) {
|
||||
if (isDefined(isDesiredRoute)) {
|
||||
chrome.tabs.sendMessage(tabId, { action: 'executeContentScript' });
|
||||
}
|
||||
}
|
||||
|
||||
await chrome.sidePanel.setOptions({
|
||||
tabId,
|
||||
path: tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com/)
|
||||
? 'sidepanel.html'
|
||||
: 'page-inaccessible.html',
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
const setTokenStateFromCookie = (cookie: string) => {
|
||||
const decodedValue = decodeURIComponent(cookie);
|
||||
const tokenPair = JSON.parse(decodedValue);
|
||||
if (isDefined(tokenPair)) {
|
||||
chrome.storage.local.set({
|
||||
isAuthenticated: true,
|
||||
accessToken: tokenPair.accessOrWorkspaceAgnosticToken,
|
||||
refreshToken: tokenPair.refreshToken,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
chrome.cookies.onChanged.addListener(async ({ cookie }) => {
|
||||
if (cookie.name === 'tokenPair') {
|
||||
const store = await chrome.storage.local.get(['clientUrl']);
|
||||
const clientUrl = isDefined(store.clientUrl)
|
||||
? store.clientUrl
|
||||
: import.meta.env.VITE_FRONT_BASE_URL;
|
||||
chrome.cookies.get({ name: 'tokenPair', url: `${clientUrl}` }, (cookie) => {
|
||||
if (isDefined(cookie)) {
|
||||
setTokenStateFromCookie(cookie.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// This will only run the very first time the extension loads, after we have stored the
|
||||
// cookiesRead variable to true, this will not allow to change the token state everytime background script runs
|
||||
chrome.cookies.get(
|
||||
{ name: 'tokenPair', url: `${import.meta.env.VITE_FRONT_BASE_URL}` },
|
||||
async (cookie) => {
|
||||
const store = await chrome.storage.local.get(['cookiesRead']);
|
||||
if (isDefined(cookie) && !isDefined(store.cookiesRead)) {
|
||||
setTokenStateFromCookie(cookie.value);
|
||||
chrome.storage.local.set({ cookiesRead: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
interface CustomDiv extends HTMLDivElement {
|
||||
onClickHandler: (newHandler: () => void) => void;
|
||||
}
|
||||
|
||||
export const createDefaultButton = (
|
||||
buttonId: string,
|
||||
buttonText = '',
|
||||
): CustomDiv => {
|
||||
const btn = document.getElementById(buttonId) as CustomDiv;
|
||||
if (isDefined(btn)) return btn;
|
||||
const div = document.createElement('div') as CustomDiv;
|
||||
const img = document.createElement('img');
|
||||
const span = document.createElement('span');
|
||||
|
||||
span.textContent = buttonText;
|
||||
img.src =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=';
|
||||
img.height = 16;
|
||||
img.width = 16;
|
||||
img.alt = 'Twenty logo';
|
||||
|
||||
// Write universal styles for the button
|
||||
const divStyles = {
|
||||
border: '1px solid black',
|
||||
borderRadius: '20px',
|
||||
backgroundColor: 'black',
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
fontSize: '1.5rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
justifyContent: 'center',
|
||||
padding: '0 1rem',
|
||||
cursor: 'pointer',
|
||||
height: '32px',
|
||||
width: 'max-content',
|
||||
};
|
||||
|
||||
Object.assign(div.style, divStyles);
|
||||
|
||||
// Apply common styles to specifc states of a button.
|
||||
div.addEventListener('mouseenter', () => {
|
||||
const hoverStyles = {
|
||||
//eslint-disable-next-line @nx/workspace-no-hardcoded-colors
|
||||
backgroundColor: '#5e5e5e',
|
||||
//eslint-disable-next-line @nx/workspace-no-hardcoded-colors
|
||||
borderColor: '#5e5e5e',
|
||||
};
|
||||
Object.assign(div.style, hoverStyles);
|
||||
});
|
||||
|
||||
div.addEventListener('mouseleave', () => {
|
||||
Object.assign(div.style, divStyles);
|
||||
});
|
||||
|
||||
div.onClickHandler = (newHandler) => {
|
||||
div.onclick = async () => {
|
||||
const store = await chrome.storage.local.get();
|
||||
|
||||
// If an api key is not set, the options page opens up to allow the user to configure an api key.
|
||||
if (!store.accessToken) {
|
||||
chrome.runtime.sendMessage({ action: 'openSidepanel' });
|
||||
return;
|
||||
}
|
||||
newHandler();
|
||||
};
|
||||
};
|
||||
|
||||
div.id = buttonId;
|
||||
|
||||
div.appendChild(img);
|
||||
div.appendChild(span);
|
||||
|
||||
return div;
|
||||
};
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
import { createDefaultButton } from '~/contentScript/createButton';
|
||||
import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl';
|
||||
import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLinkedinLink';
|
||||
import extractDomain from '~/contentScript/utils/extractDomain';
|
||||
import { createCompany, fetchCompany } from '~/db/company.db';
|
||||
import { CompanyInput } from '~/db/types/company.types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const checkIfCompanyExists = async () => {
|
||||
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
||||
action: 'getActiveTab',
|
||||
});
|
||||
|
||||
const companyURL = extractCompanyLinkedinLink(activeTab.url);
|
||||
|
||||
return await fetchCompany({
|
||||
linkedinLink: {
|
||||
url: { eq: companyURL },
|
||||
label: { eq: companyURL },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const addCompany = async () => {
|
||||
// Extract company-specific data from the DOM
|
||||
const companyNameElement = document.querySelector(
|
||||
'.org-top-card-summary__title',
|
||||
);
|
||||
const domainNameElement = document.querySelector(
|
||||
'.org-top-card-primary-actions__inner a',
|
||||
);
|
||||
const addressElement = document.querySelectorAll(
|
||||
'.org-top-card-summary-info-list__info-item',
|
||||
)[1];
|
||||
const employeesNumberElement = document.querySelectorAll(
|
||||
'.org-top-card-summary-info-list__info-item',
|
||||
)[3];
|
||||
|
||||
// Get the text content or other necessary data from the DOM elements
|
||||
const companyName = companyNameElement
|
||||
? companyNameElement.getAttribute('title')
|
||||
: '';
|
||||
const domainName = extractDomain(
|
||||
domainNameElement && domainNameElement.getAttribute('href'),
|
||||
);
|
||||
const address = addressElement
|
||||
? addressElement.textContent?.trim().replace(/\s+/g, ' ')
|
||||
: '';
|
||||
const employees = employeesNumberElement
|
||||
? Number(
|
||||
employeesNumberElement.textContent
|
||||
?.trim()
|
||||
.replace(/\s+/g, ' ')
|
||||
.split('-')[0],
|
||||
)
|
||||
: 0;
|
||||
|
||||
// Prepare company data to send to the backend
|
||||
const companyInputData: CompanyInput = {
|
||||
name: companyName ?? '',
|
||||
domainName: domainName,
|
||||
address: address ?? '',
|
||||
employees: employees,
|
||||
};
|
||||
|
||||
// Extract active tab url using chrome API - an event is triggered here and is caught by background script.
|
||||
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
||||
action: 'getActiveTab',
|
||||
});
|
||||
|
||||
// Convert URLs like https://www.linkedin.com/company/twenty/about/ to https://www.linkedin.com/company/twenty
|
||||
const companyURL = extractCompanyLinkedinLink(activeTab.url);
|
||||
companyInputData.linkedinLink = { url: companyURL, label: companyURL };
|
||||
|
||||
const companyId = await createCompany(companyInputData);
|
||||
|
||||
if (isDefined(companyId)) {
|
||||
await changeSidePanelUrl(`/object/company/${companyId}`);
|
||||
}
|
||||
|
||||
return companyId;
|
||||
};
|
||||
|
||||
export const insertButtonForCompany = async () => {
|
||||
const companyButtonDiv = createDefaultButton('twenty-company-btn');
|
||||
|
||||
const companyDiv: HTMLDivElement | null = document.querySelector(
|
||||
'.org-top-card__primary-content',
|
||||
);
|
||||
|
||||
if (isDefined(companyDiv)) {
|
||||
Object.assign(companyButtonDiv.style, {
|
||||
marginTop: '.8rem',
|
||||
});
|
||||
companyDiv.parentElement?.append(companyButtonDiv);
|
||||
}
|
||||
|
||||
const companyButtonSpan = companyButtonDiv.getElementsByTagName('span')[0];
|
||||
const company = await checkIfCompanyExists();
|
||||
|
||||
const openCompanyOnSidePanel = (companyId: string) => {
|
||||
companyButtonSpan.textContent = 'View in Twenty';
|
||||
companyButtonDiv.onClickHandler(async () => {
|
||||
await changeSidePanelUrl(`/object/company/${companyId}`);
|
||||
chrome.runtime.sendMessage({ action: 'openSidepanel' });
|
||||
});
|
||||
};
|
||||
|
||||
if (isDefined(company)) {
|
||||
await changeSidePanelUrl(`/object/company/${company.id}`);
|
||||
if (isDefined(company.id)) openCompanyOnSidePanel(company.id);
|
||||
} else {
|
||||
await changeSidePanelUrl(`/objects/companies`);
|
||||
companyButtonSpan.textContent = 'Add to Twenty';
|
||||
|
||||
companyButtonDiv.onClickHandler(async () => {
|
||||
companyButtonSpan.textContent = 'Saving...';
|
||||
const companyId = await addCompany();
|
||||
if (isDefined(companyId)) {
|
||||
openCompanyOnSidePanel(companyId);
|
||||
} else {
|
||||
companyButtonSpan.textContent = 'Try again';
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
import { createDefaultButton } from '~/contentScript/createButton';
|
||||
import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl';
|
||||
import extractFirstAndLastName from '~/contentScript/utils/extractFirstAndLastName';
|
||||
import { createPerson, fetchPerson } from '~/db/person.db';
|
||||
import { PersonInput } from '~/db/types/person.types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const checkIfPersonExists = async () => {
|
||||
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
||||
action: 'getActiveTab',
|
||||
});
|
||||
|
||||
let activeTabUrl = '';
|
||||
if (isDefined(activeTab.url.endsWith('/'))) {
|
||||
activeTabUrl = activeTab.url.slice(0, -1);
|
||||
}
|
||||
|
||||
const personNameElement = document.querySelector('.text-heading-xlarge');
|
||||
const personName = personNameElement ? personNameElement.textContent : '';
|
||||
|
||||
const { firstName, lastName } = extractFirstAndLastName(String(personName));
|
||||
const person = await fetchPerson({
|
||||
name: {
|
||||
firstName: { eq: firstName },
|
||||
lastName: { eq: lastName },
|
||||
},
|
||||
linkedinLink: { url: { eq: activeTabUrl }, label: { eq: activeTabUrl } },
|
||||
});
|
||||
return person;
|
||||
};
|
||||
|
||||
export const addPerson = async () => {
|
||||
const personNameElement = document.querySelector('.text-heading-xlarge');
|
||||
|
||||
const separatorElement = document.querySelector(
|
||||
'.pv-text-details__separator',
|
||||
);
|
||||
const personCityElement = separatorElement?.previousElementSibling;
|
||||
|
||||
const profilePictureElement = document.querySelector(
|
||||
'.pv-top-card-profile-picture__image',
|
||||
);
|
||||
|
||||
const firstListItem = document.querySelector(
|
||||
'div[data-view-name="profile-component-entity"]',
|
||||
);
|
||||
const secondDivElement = firstListItem?.querySelector('div:nth-child(2)');
|
||||
const ariaHiddenSpan = secondDivElement?.querySelector(
|
||||
'span[aria-hidden="true"]',
|
||||
);
|
||||
|
||||
// Get the text content or other necessary data from the DOM elements.
|
||||
const personName = personNameElement ? personNameElement.textContent : '';
|
||||
const personCity = personCityElement
|
||||
? personCityElement.textContent?.trim().replace(/\s+/g, ' ').split(',')[0]
|
||||
: '';
|
||||
const profilePicture = profilePictureElement
|
||||
? profilePictureElement?.getAttribute('src')
|
||||
: '';
|
||||
const jobTitle = ariaHiddenSpan ? ariaHiddenSpan.textContent?.trim() : '';
|
||||
|
||||
const { firstName, lastName } = extractFirstAndLastName(String(personName));
|
||||
|
||||
// Prepare person data to send to the backend.
|
||||
const personData: PersonInput = {
|
||||
name: { firstName, lastName },
|
||||
city: personCity ?? '',
|
||||
avatarUrl: profilePicture ?? '',
|
||||
jobTitle: jobTitle ?? '',
|
||||
linkedinLink: { url: '', label: '' },
|
||||
};
|
||||
|
||||
// Extract active tab url using chrome API - an event is triggered here and is caught by background script.
|
||||
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
||||
action: 'getActiveTab',
|
||||
});
|
||||
|
||||
let activeTabUrl = '';
|
||||
|
||||
// Remove last slash from the URL for consistency when saving usernames.
|
||||
if (isDefined(activeTab.url.endsWith('/'))) {
|
||||
activeTabUrl = activeTab.url.slice(0, -1);
|
||||
}
|
||||
|
||||
personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl };
|
||||
const personId = await createPerson(personData);
|
||||
|
||||
if (isDefined(personId)) {
|
||||
await changeSidePanelUrl(`/object/person/${personId}`);
|
||||
}
|
||||
|
||||
return personId;
|
||||
};
|
||||
|
||||
export const insertButtonForPerson = async () => {
|
||||
const personButtonDiv = createDefaultButton('twenty-person-btn');
|
||||
|
||||
if (isDefined(personButtonDiv)) {
|
||||
const addedProfileDiv = document.querySelector('.artdeco-card > .ph5');
|
||||
|
||||
if (isDefined(addedProfileDiv)) {
|
||||
Object.assign(personButtonDiv.style, {
|
||||
marginTop: '.8rem',
|
||||
});
|
||||
addedProfileDiv.append(personButtonDiv);
|
||||
}
|
||||
|
||||
const personButtonSpan = personButtonDiv.getElementsByTagName('span')[0];
|
||||
const person = await checkIfPersonExists();
|
||||
|
||||
const openPersonOnSidePanel = (personId: string) => {
|
||||
personButtonSpan.textContent = 'View in Twenty';
|
||||
personButtonDiv.onClickHandler(async () => {
|
||||
await changeSidePanelUrl(`/object/person/${personId}`);
|
||||
chrome.runtime.sendMessage({ action: 'openSidepanel' });
|
||||
});
|
||||
};
|
||||
|
||||
if (isDefined(person)) {
|
||||
await changeSidePanelUrl(`/object/person/${person.id}`);
|
||||
if (isDefined(person.id)) openPersonOnSidePanel(person.id);
|
||||
} else {
|
||||
await changeSidePanelUrl(`/objects/people`);
|
||||
personButtonSpan.textContent = 'Add to Twenty';
|
||||
personButtonDiv.onClickHandler(async () => {
|
||||
personButtonSpan.textContent = 'Saving...';
|
||||
const personId = await addPerson();
|
||||
if (isDefined(personId)) openPersonOnSidePanel(personId);
|
||||
else personButtonSpan.textContent = 'Try again';
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile';
|
||||
import { insertButtonForPerson } from '~/contentScript/extractPersonProfile';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
// Inject buttons into the DOM when SPA is reloaded on the resource url.
|
||||
// e.g. reload the page when on https://www.linkedin.com/in/mabdullahabaid/
|
||||
// await insertButtonForCompany();
|
||||
|
||||
const companyRoute = /^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/;
|
||||
const personRoute = /^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/;
|
||||
|
||||
const executeScript = async () => {
|
||||
const loc = window.location.href;
|
||||
switch (true) {
|
||||
case companyRoute.test(loc):
|
||||
await insertButtonForCompany();
|
||||
break;
|
||||
case personRoute.test(loc):
|
||||
await insertButtonForPerson();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// The content script gets executed upon load, so the the content script is executed when a user visits https://www.linkedin.com/feed/.
|
||||
// However, there would never be another reload in a single page application unless triggered manually.
|
||||
// Therefore, if the user navigates to a person or a company page, we must manually re-execute the content script to create the "Add to Twenty" button.
|
||||
// e.g. create "Add to Twenty" button when a user navigates to https://www.linkedin.com/in/mabdullahabaid/ from https://www.linkedin.com/feed/
|
||||
chrome.runtime.onMessage.addListener(async (message, _, sendResponse) => {
|
||||
if (message.action === 'executeContentScript') {
|
||||
await executeScript();
|
||||
}
|
||||
|
||||
sendResponse('Executing!');
|
||||
});
|
||||
|
||||
chrome.storage.local.onChanged.addListener(async (store) => {
|
||||
if (isDefined(store.accessToken)) {
|
||||
if (isDefined(store.accessToken.newValue)) {
|
||||
await executeScript();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
const btn = document.getElementById('twenty-settings-btn');
|
||||
if (!isDefined(btn)) {
|
||||
const div = document.createElement('div');
|
||||
const img = document.createElement('img');
|
||||
img.src =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=';
|
||||
img.height = 20;
|
||||
img.width = 20;
|
||||
img.alt = 'Twenty logo';
|
||||
|
||||
// Write universal styles for the button
|
||||
const divStyles = {
|
||||
border: '1px solid black',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'black',
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
fontSize: '1.5rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
justifyContent: 'center',
|
||||
padding: '0 1rem',
|
||||
cursor: 'pointer',
|
||||
height: '50px',
|
||||
width: '50px',
|
||||
position: 'fixed',
|
||||
bottom: '80px',
|
||||
right: '20px',
|
||||
zIndex: '9999999999999999999999999',
|
||||
};
|
||||
|
||||
div.addEventListener('mouseenter', () => {
|
||||
const hoverStyles = {
|
||||
//eslint-disable-next-line @nx/workspace-no-hardcoded-colors
|
||||
backgroundColor: '#5e5e5e',
|
||||
//eslint-disable-next-line @nx/workspace-no-hardcoded-colors
|
||||
borderColor: '#5e5e5e',
|
||||
};
|
||||
Object.assign(div.style, hoverStyles);
|
||||
});
|
||||
|
||||
div.addEventListener('mouseleave', () => {
|
||||
Object.assign(div.style, divStyles);
|
||||
});
|
||||
|
||||
div.onclick = async () => {
|
||||
chrome.runtime.sendMessage({ action: 'openSidepanel' });
|
||||
chrome.storage.local.set({ navigateSidepanel: 'settings' });
|
||||
};
|
||||
|
||||
div.appendChild(img);
|
||||
|
||||
Object.assign(div.style, divStyles);
|
||||
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
const changeSidePanelUrl = async (url: string) => {
|
||||
if (isDefined(url)) {
|
||||
chrome.storage.local.set({ navigateSidepanel: 'sidepanel' });
|
||||
// we first clear the sidepanelUrl to trigger the onchange listener on sidepanel
|
||||
// which will pass the post meessage to handle internal navigation of iframe
|
||||
chrome.storage.local.set({ sidepanelUrl: '' });
|
||||
chrome.storage.local.set({ sidepanelUrl: url });
|
||||
}
|
||||
};
|
||||
|
||||
export default changeSidePanelUrl;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
// "https://www.linkedin.com/company/twenty/" "https://www.linkedin.com/company/twenty/about/" "https://www.linkedin.com/company/twenty/people/".
|
||||
const extractCompanyLinkedinLink = (activeTabUrl: string) => {
|
||||
// Regular expression to match the company ID
|
||||
const regex = /\/company\/([^/]*)/;
|
||||
|
||||
// Extract the company ID using the regex
|
||||
const match = activeTabUrl.match(regex);
|
||||
|
||||
if (isDefined(match) && isDefined(match[1])) {
|
||||
const companyID = match[1];
|
||||
const cleanCompanyURL = `https://www.linkedin.com/company/${companyID}`;
|
||||
return cleanCompanyURL;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export default extractCompanyLinkedinLink;
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
const extractDomain = (url: string | null) => {
|
||||
if (!url) return '';
|
||||
|
||||
const hostname = new URL(url).hostname;
|
||||
let domain = hostname.replace('www.', '');
|
||||
|
||||
const parts = domain.split('.');
|
||||
if (parts.length > 2) {
|
||||
domain = parts.slice(1).join('.');
|
||||
}
|
||||
|
||||
return domain;
|
||||
};
|
||||
|
||||
export default extractDomain;
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
// Separate first name and last name from a full name.
|
||||
const extractFirstAndLastName = (fullName: string) => {
|
||||
const spaceIndex = fullName.lastIndexOf(' ');
|
||||
const firstName = fullName.substring(0, spaceIndex);
|
||||
const lastName = fullName.substring(spaceIndex + 1);
|
||||
return { firstName, lastName };
|
||||
};
|
||||
|
||||
export default extractFirstAndLastName;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import {
|
||||
ExchangeAuthCodeInput,
|
||||
ExchangeAuthCodeResponse,
|
||||
Tokens,
|
||||
} from '~/db/types/auth.types';
|
||||
import { EXCHANGE_AUTHORIZATION_CODE } from '~/graphql/auth/mutations';
|
||||
import { callMutation } from '~/utils/requestDb';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const exchangeAuthorizationCode = async (
|
||||
exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
||||
): Promise<Tokens | null> => {
|
||||
const data = await callMutation<ExchangeAuthCodeResponse>(
|
||||
EXCHANGE_AUTHORIZATION_CODE,
|
||||
exchangeAuthCodeInput,
|
||||
);
|
||||
if (isDefined(data?.exchangeAuthorizationCode))
|
||||
return data.exchangeAuthorizationCode;
|
||||
else return null;
|
||||
};
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import {
|
||||
CompanyInput,
|
||||
CreateCompanyResponse,
|
||||
FindCompanyResponse,
|
||||
} from '~/db/types/company.types';
|
||||
import { Company, CompanyFilterInput } from '~/generated/graphql';
|
||||
import { CREATE_COMPANY } from '~/graphql/company/mutations';
|
||||
import { FIND_COMPANY } from '~/graphql/company/queries';
|
||||
|
||||
import { callMutation, callQuery } from '../utils/requestDb';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const fetchCompany = async (
|
||||
companyfilerInput: CompanyFilterInput,
|
||||
): Promise<Company | null> => {
|
||||
const data = await callQuery<FindCompanyResponse>(FIND_COMPANY, {
|
||||
filter: {
|
||||
...companyfilerInput,
|
||||
},
|
||||
});
|
||||
if (isDefined(data?.companies.edges)) {
|
||||
return data.companies.edges.length > 0
|
||||
? isDefined(data.companies.edges[0].node)
|
||||
? data.companies.edges[0].node
|
||||
: null
|
||||
: null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const createCompany = async (
|
||||
company: CompanyInput,
|
||||
): Promise<string | null> => {
|
||||
const data = await callMutation<CreateCompanyResponse>(CREATE_COMPANY, {
|
||||
input: company,
|
||||
});
|
||||
if (isDefined(data)) {
|
||||
return data.createCompany.id;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import {
|
||||
CreatePersonResponse,
|
||||
FindPersonResponse,
|
||||
PersonInput,
|
||||
} from '~/db/types/person.types';
|
||||
import { Person, PersonFilterInput } from '~/generated/graphql';
|
||||
import { CREATE_PERSON } from '~/graphql/person/mutations';
|
||||
import { FIND_PERSON } from '~/graphql/person/queries';
|
||||
|
||||
import { callMutation, callQuery } from '../utils/requestDb';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const fetchPerson = async (
|
||||
personFilterData: PersonFilterInput,
|
||||
): Promise<Person | null> => {
|
||||
const data = await callQuery<FindPersonResponse>(FIND_PERSON, {
|
||||
filter: {
|
||||
...personFilterData,
|
||||
},
|
||||
});
|
||||
if (isDefined(data?.people.edges)) {
|
||||
return data.people.edges.length > 0
|
||||
? isDefined(data.people.edges[0].node)
|
||||
? data.people.edges[0].node
|
||||
: null
|
||||
: null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const createPerson = async (
|
||||
person: PersonInput,
|
||||
): Promise<string | null> => {
|
||||
const data = await callMutation<CreatePersonResponse>(CREATE_PERSON, {
|
||||
input: person,
|
||||
});
|
||||
if (isDefined(data?.createPerson)) {
|
||||
return data.createPerson.id;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import { ApolloClient, InMemoryCache } from '@apollo/client';
|
||||
import { Tokens } from '~/db/types/auth.types';
|
||||
import { RENEW_TOKEN } from '~/graphql/auth/mutations';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const renewToken = async (
|
||||
appToken: string,
|
||||
): Promise<{ renewToken: { tokens: Tokens } } | null> => {
|
||||
const store = await chrome.storage.local.get();
|
||||
const serverUrl = `${
|
||||
isDefined(store.serverBaseUrl)
|
||||
? store.serverBaseUrl
|
||||
: import.meta.env.VITE_SERVER_BASE_URL
|
||||
}/graphql`;
|
||||
|
||||
// Create new client to call refresh token graphql mutation
|
||||
const client = new ApolloClient({
|
||||
uri: serverUrl,
|
||||
cache: new InMemoryCache({}),
|
||||
});
|
||||
|
||||
const { data } = await client.mutate({
|
||||
mutation: RENEW_TOKEN,
|
||||
variables: {
|
||||
appToken,
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
if (isDefined(data)) {
|
||||
return data;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
export type AuthToken = {
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
};
|
||||
|
||||
export type ExchangeAuthCodeInput = {
|
||||
authorizationCode: string;
|
||||
codeVerifier?: string;
|
||||
clientSecret?: string;
|
||||
};
|
||||
|
||||
export type Tokens = {
|
||||
loginToken: AuthToken;
|
||||
accessOrWorkspaceAgnosticToken: AuthToken;
|
||||
refreshToken: AuthToken;
|
||||
};
|
||||
|
||||
export type ExchangeAuthCodeResponse = {
|
||||
exchangeAuthorizationCode: Tokens;
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { Company, CompanyConnection } from '~/generated/graphql';
|
||||
|
||||
export type CompanyInput = Pick<
|
||||
Company,
|
||||
'name' | 'domainName' | 'address' | 'employees' | 'linkedinLink'
|
||||
>;
|
||||
export type FindCompanyResponse = {
|
||||
companies: Pick<CompanyConnection, 'edges'>;
|
||||
};
|
||||
export type CreateCompanyResponse = { createCompany: { id: string } };
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { Person, PersonConnection } from '~/generated/graphql';
|
||||
|
||||
export type PersonInput = Pick<
|
||||
Person,
|
||||
'name' | 'city' | 'avatarUrl' | 'jobTitle' | 'linkedinLink'
|
||||
>;
|
||||
export type FindPersonResponse = { people: Pick<PersonConnection, 'edges'> };
|
||||
export type CreatePersonResponse = { createPerson: { id: string } };
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
declare const __APP_VERSION__: string;
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const EXCHANGE_AUTHORIZATION_CODE = gql`
|
||||
mutation ExchangeAuthorizationCode(
|
||||
$authorizationCode: String!
|
||||
$codeVerifier: String
|
||||
$clientSecret: String
|
||||
) {
|
||||
exchangeAuthorizationCode(
|
||||
authorizationCode: $authorizationCode
|
||||
codeVerifier: $codeVerifier
|
||||
clientSecret: $clientSecret
|
||||
) {
|
||||
loginToken {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
accessOrWorkspaceAgnosticToken {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
refreshToken {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const RENEW_TOKEN = gql`
|
||||
mutation RenewToken($appToken: String!) {
|
||||
renewToken(appToken: $appToken) {
|
||||
tokens {
|
||||
accessOrWorkspaceAgnosticToken {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
refreshToken {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_COMPANY = gql`
|
||||
mutation CreateOneCompany($input: CompanyCreateInput!) {
|
||||
createCompany(data: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const FIND_COMPANY = gql`
|
||||
query FindCompany($filter: CompanyFilterInput!) {
|
||||
companies(filter: $filter) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
linkedinLink {
|
||||
url
|
||||
label
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_PERSON = gql`
|
||||
mutation CreateOnePerson($input: PersonCreateInput!) {
|
||||
createPerson(data: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const FIND_PERSON = gql`
|
||||
query FindPerson($filter: PersonFilterInput!) {
|
||||
people(filter: $filter) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
linkedinLink {
|
||||
url
|
||||
label
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
* {
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Inter', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import { defineManifest } from '@crxjs/vite-plugin';
|
||||
|
||||
import packageData from '../package.json';
|
||||
|
||||
const external_sites =
|
||||
process.env.VITE_MODE === 'development'
|
||||
? [`https://app.twenty.com/*`, `http://localhost:3001/*`]
|
||||
: [`https://app.twenty.com/*`];
|
||||
|
||||
export default defineManifest({
|
||||
manifest_version: 3,
|
||||
name: 'Twenty',
|
||||
description: packageData.description,
|
||||
version: packageData.version,
|
||||
|
||||
icons: {
|
||||
16: 'logo/32-32.png',
|
||||
32: 'logo/32-32.png',
|
||||
48: 'logo/32-32.png',
|
||||
},
|
||||
|
||||
action: {},
|
||||
|
||||
//TODO: change this to a documenation page
|
||||
options_page: 'sidepanel.html',
|
||||
|
||||
background: {
|
||||
service_worker: 'src/background/index.ts',
|
||||
type: 'module',
|
||||
},
|
||||
|
||||
content_scripts: [
|
||||
{
|
||||
matches: ['https://www.linkedin.com/*'],
|
||||
js: [
|
||||
'src/contentScript/index.ts',
|
||||
'src/contentScript/insertSettingsButton.ts',
|
||||
],
|
||||
run_at: 'document_end',
|
||||
},
|
||||
],
|
||||
|
||||
web_accessible_resources: [
|
||||
{
|
||||
resources: ['sidepanel.html', 'page-inaccessible.html'],
|
||||
matches: ['https://www.linkedin.com/*'],
|
||||
},
|
||||
],
|
||||
|
||||
permissions: ['activeTab', 'storage', 'identity', 'sidePanel', 'cookies'],
|
||||
|
||||
// setting host permissions to all http connections will allow
|
||||
// for people who host on their custom domain to get access to
|
||||
// extension instead of white listing individual urls
|
||||
host_permissions: ['https://*/*', 'http://*/*'],
|
||||
|
||||
externally_connectable: {
|
||||
matches: external_sites,
|
||||
},
|
||||
});
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import Settings from '~/options/Settings';
|
||||
import Sidepanel from '~/options/Sidepanel';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const App = () => {
|
||||
const [currentScreen, setCurrentScreen] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const setCurrentScreenState = async () => {
|
||||
const store = await chrome.storage.local.get(['navigateSidepanel']);
|
||||
if (isDefined(store.navigateSidepanel)) {
|
||||
setCurrentScreen(store.navigateSidepanel);
|
||||
}
|
||||
};
|
||||
|
||||
setCurrentScreenState();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
chrome.storage.local.onChanged.addListener((updatedStore) => {
|
||||
if (
|
||||
isDefined(updatedStore.navigateSidepanel) &&
|
||||
isDefined(updatedStore.navigateSidepanel.newValue)
|
||||
) {
|
||||
setCurrentScreen(updatedStore.navigateSidepanel.newValue);
|
||||
}
|
||||
});
|
||||
}, [setCurrentScreen]);
|
||||
|
||||
switch (currentScreen) {
|
||||
case 'sidepanel':
|
||||
return <Sidepanel />;
|
||||
case 'settings':
|
||||
return <Settings />;
|
||||
default:
|
||||
return <Settings />;
|
||||
}
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { Loader } from '@/ui/display/loader/components/Loader';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.noisy};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<img src="/logo/32-32.svg" alt="twenty-logo" height={64} width={64} />
|
||||
<Loader />
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
import { MainButton } from '@/ui/input/button/MainButton';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 400px;
|
||||
height: 350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledTextContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledLargeText = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.lg};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
const StyledMediumText = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
`;
|
||||
|
||||
const PageInaccessible = () => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<StyledContainer>
|
||||
<img src="/logo/32-32.svg" alt="twenty-logo" height={40} width={40} />
|
||||
<StyledTextContainer>
|
||||
<StyledLargeText>
|
||||
Extension not available on the website
|
||||
</StyledLargeText>
|
||||
<StyledMediumText>
|
||||
Open LinkedIn to use the extension
|
||||
</StyledMediumText>
|
||||
</StyledTextContainer>
|
||||
<MainButton
|
||||
title="Go to LinkedIn"
|
||||
onClick={() => window.open('https://www.linkedin.com/')}
|
||||
/>
|
||||
</StyledContainer>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageInaccessible;
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { MainButton } from '@/ui/input/button/MainButton';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { clearStore } from '~/utils/apolloClient';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 400px;
|
||||
height: 350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledActionContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
width: 300px;
|
||||
`;
|
||||
|
||||
const Settings = () => {
|
||||
const [serverBaseUrl, setServerBaseUrl] = useState('');
|
||||
const [clientUrl, setClientUrl] = useState('');
|
||||
const [currentClientUrl, setCurrentClientUrl] = useState('');
|
||||
const [currentServerUrl, setCurrentServerUrl] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const getState = async () => {
|
||||
const store = await chrome.storage.local.get([
|
||||
'serverBaseUrl',
|
||||
'clientUrl',
|
||||
]);
|
||||
if (isDefined(store.serverBaseUrl)) {
|
||||
setServerBaseUrl(store.serverBaseUrl);
|
||||
setCurrentServerUrl(store.serverBaseUrl);
|
||||
} else {
|
||||
setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL);
|
||||
setCurrentServerUrl(import.meta.env.VITE_SERVER_BASE_URL);
|
||||
}
|
||||
|
||||
if (isDefined(store.clientUrl)) {
|
||||
setClientUrl(store.clientUrl);
|
||||
setCurrentClientUrl(store.clientUrl);
|
||||
} else {
|
||||
setClientUrl(import.meta.env.VITE_FRONT_BASE_URL);
|
||||
setCurrentClientUrl(import.meta.env.VITE_FRONT_BASE_URL);
|
||||
}
|
||||
};
|
||||
void getState();
|
||||
}, []);
|
||||
|
||||
const handleSettingsChange = () => {
|
||||
chrome.storage.local.set({
|
||||
serverBaseUrl,
|
||||
clientUrl,
|
||||
navigateSidepanel: 'sidepanel',
|
||||
});
|
||||
clearStore();
|
||||
};
|
||||
|
||||
const handleCloseSettings = () => {
|
||||
chrome.storage.local.set({
|
||||
navigateSidepanel: 'sidepanel',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<StyledContainer>
|
||||
<img src="/logo/32-32.svg" alt="twenty-logo" height={40} width={40} />
|
||||
<StyledActionContainer>
|
||||
<TextInput
|
||||
label="Client URL"
|
||||
value={clientUrl}
|
||||
onChange={setClientUrl}
|
||||
placeholder="My client URL"
|
||||
fullWidth
|
||||
/>
|
||||
<TextInput
|
||||
label="Server URL"
|
||||
value={serverBaseUrl}
|
||||
onChange={setServerBaseUrl}
|
||||
placeholder="My server URL"
|
||||
fullWidth
|
||||
/>
|
||||
<MainButton
|
||||
title="Done"
|
||||
disabled={
|
||||
currentClientUrl === clientUrl &&
|
||||
currentServerUrl === serverBaseUrl
|
||||
}
|
||||
variant="primary"
|
||||
onClick={handleSettingsChange}
|
||||
fullWidth
|
||||
/>
|
||||
<MainButton
|
||||
title="Close"
|
||||
variant="secondary"
|
||||
onClick={handleCloseSettings}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledActionContainer>
|
||||
</StyledContainer>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { MainButton } from '@/ui/input/button/MainButton';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledIframe = styled.iframe`
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
border: none;
|
||||
`;
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 400px;
|
||||
height: 350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledActionContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
width: 300px;
|
||||
`;
|
||||
|
||||
const Sidepanel = () => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [clientUrl, setClientUrl] = useState(
|
||||
import.meta.env.VITE_FRONT_BASE_URL,
|
||||
);
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
const setIframeState = useCallback(async () => {
|
||||
const store = await chrome.storage.local.get([
|
||||
'isAuthenticated',
|
||||
'sidepanelUrl',
|
||||
'clientUrl',
|
||||
'accessToken',
|
||||
'refreshToken',
|
||||
]);
|
||||
|
||||
if (
|
||||
store.isAuthenticated === true &&
|
||||
isDefined(store.accessToken) &&
|
||||
isDefined(store.refreshToken) &&
|
||||
new Date(store.accessToken.expiresAt).getTime() >= Date.now()
|
||||
) {
|
||||
setIsAuthenticated(true);
|
||||
if (isDefined(store.sidepanelUrl)) {
|
||||
if (isDefined(store.clientUrl)) {
|
||||
setClientUrl(`${store.clientUrl}${store.sidepanelUrl}`);
|
||||
} else {
|
||||
setClientUrl(
|
||||
`${import.meta.env.VITE_FRONT_BASE_URL}${store.sidepanelUrl}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
chrome.storage.local.set({ isAuthenticated: false });
|
||||
if (isDefined(store.clientUrl)) {
|
||||
setClientUrl(store.clientUrl);
|
||||
}
|
||||
}
|
||||
}, [setClientUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
void setIframeState();
|
||||
}, [setIframeState]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', async (event) => {
|
||||
const store = await chrome.storage.local.get([
|
||||
'clientUrl',
|
||||
'accessToken',
|
||||
'refreshToken',
|
||||
]);
|
||||
const clientUrl = isDefined(store.clientUrl)
|
||||
? store.clientUrl
|
||||
: import.meta.env.VITE_FRONT_BASE_URL;
|
||||
|
||||
if (
|
||||
isDefined(store.accessToken) &&
|
||||
isDefined(store.refreshToken) &&
|
||||
event.origin === clientUrl &&
|
||||
event.data === 'loaded'
|
||||
) {
|
||||
event.source?.postMessage(
|
||||
{
|
||||
type: 'tokens',
|
||||
value: {
|
||||
accessToken: {
|
||||
token: store.accessToken.token,
|
||||
expiresAt: store.accessToken.expiresAt,
|
||||
},
|
||||
refreshToken: {
|
||||
token: store.refreshToken.token,
|
||||
expiresAt: store.refreshToken.expiresAt,
|
||||
},
|
||||
},
|
||||
},
|
||||
clientUrl,
|
||||
);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
chrome.storage.local.onChanged.addListener(async (updatedStore) => {
|
||||
if (isDefined(updatedStore.isAuthenticated)) {
|
||||
if (updatedStore.isAuthenticated.newValue === true) {
|
||||
setIframeState();
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(updatedStore.sidepanelUrl)) {
|
||||
if (isDefined(updatedStore.sidepanelUrl.newValue)) {
|
||||
const store = await chrome.storage.local.get(['clientUrl']);
|
||||
const clientUrl = isDefined(store.clientUrl)
|
||||
? store.clientUrl
|
||||
: import.meta.env.VITE_FRONT_BASE_URL;
|
||||
|
||||
iframeRef.current?.contentWindow?.postMessage(
|
||||
{
|
||||
type: 'navigate',
|
||||
value: updatedStore.sidepanelUrl.newValue,
|
||||
},
|
||||
clientUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [setIframeState]);
|
||||
|
||||
return isAuthenticated ? (
|
||||
<StyledIframe
|
||||
ref={iframeRef}
|
||||
title="twenty-website"
|
||||
src={clientUrl}
|
||||
></StyledIframe>
|
||||
) : (
|
||||
<StyledWrapper>
|
||||
<StyledContainer>
|
||||
<img src="/logo/32-32.svg" alt="twenty-logo" height={40} width={40} />
|
||||
<StyledActionContainer>
|
||||
<MainButton
|
||||
title="Connect your account"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
window.open(clientUrl, '_blank');
|
||||
}}
|
||||
/>
|
||||
</StyledActionContainer>
|
||||
</StyledContainer>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidepanel;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
|
||||
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
|
||||
import App from '~/options/App';
|
||||
|
||||
import '~/index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
|
||||
<AppThemeProvider>
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
</AppThemeProvider>,
|
||||
);
|
||||
|
||||
declare module '@emotion/react' {
|
||||
export interface Theme extends ThemeType {}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
|
||||
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
|
||||
import Loading from '~/options/Loading';
|
||||
|
||||
import '~/index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
|
||||
<AppThemeProvider>
|
||||
<React.StrictMode>
|
||||
<Loading />
|
||||
</React.StrictMode>
|
||||
</AppThemeProvider>,
|
||||
);
|
||||
|
||||
declare module '@emotion/react' {
|
||||
export interface Theme extends ThemeType {}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const StyledLoaderContainer = styled.div`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
width: ${({ theme }) => theme.spacing(6)};
|
||||
height: ${({ theme }) => theme.spacing(3)};
|
||||
border-radius: ${({ theme }) => theme.border.radius.pill};
|
||||
border: 1px solid ${({ theme }) => theme.font.color.tertiary};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledLoader = styled(motion.div)`
|
||||
background-color: ${({ theme }) => theme.font.color.tertiary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.pill};
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
`;
|
||||
|
||||
export const Loader = () => (
|
||||
<StyledLoaderContainer>
|
||||
<StyledLoader
|
||||
animate={{
|
||||
x: [-16, 0, 16],
|
||||
width: [8, 12, 8],
|
||||
height: [8, 2, 8],
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
times: [0, 0.15, 0.3],
|
||||
repeat: Infinity,
|
||||
}}
|
||||
/>
|
||||
</StyledLoaderContainer>
|
||||
);
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
type H2TitleProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
adornment?: React.ReactNode;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.h2`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const StyledDescription = styled.h3`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
margin: 0;
|
||||
margin-top: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
export const H2Title = ({ title, description, adornment }: H2TitleProps) => (
|
||||
<StyledContainer>
|
||||
<StyledTitleContainer>
|
||||
<StyledTitle>{title}</StyledTitle>
|
||||
{adornment}
|
||||
</StyledTitleContainer>
|
||||
{description && <StyledDescription>{description}</StyledDescription>}
|
||||
</StyledContainer>
|
||||
);
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export type ButtonSize = 'medium' | 'small';
|
||||
export type ButtonPosition = 'standalone' | 'left' | 'middle' | 'right';
|
||||
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
|
||||
export type ButtonAccent = 'default' | 'blue' | 'danger';
|
||||
|
||||
export type ButtonProps = {
|
||||
className?: string;
|
||||
Icon?: React.ReactNode;
|
||||
title?: string;
|
||||
fullWidth?: boolean;
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
position?: ButtonPosition;
|
||||
accent?: ButtonAccent;
|
||||
soon?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
};
|
||||
|
||||
const StyledButton = styled.button<ButtonProps>`
|
||||
border: 1px solid transparent;
|
||||
border-radius: ${({ position, theme }) => {
|
||||
switch (position) {
|
||||
case 'left':
|
||||
return `${theme.border.radius.sm} 0px 0px ${theme.border.radius.sm}`;
|
||||
case 'right':
|
||||
return `0px ${theme.border.radius.sm} ${theme.border.radius.sm} 0px`;
|
||||
case 'middle':
|
||||
return '0px';
|
||||
case 'standalone':
|
||||
return theme.border.radius.sm;
|
||||
default:
|
||||
return theme.border.radius.sm;
|
||||
}
|
||||
}};
|
||||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-family: ${({ theme }) => theme.font.family};
|
||||
font-weight: 500;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
white-space: nowrap;
|
||||
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
|
||||
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Button = ({
|
||||
className,
|
||||
Icon,
|
||||
title,
|
||||
fullWidth = false,
|
||||
variant = 'primary',
|
||||
size = 'medium',
|
||||
position = 'standalone',
|
||||
soon = false,
|
||||
disabled = false,
|
||||
onClick,
|
||||
}: ButtonProps) => (
|
||||
<StyledButton
|
||||
fullWidth={fullWidth}
|
||||
variant={variant}
|
||||
size={size}
|
||||
position={position}
|
||||
disabled={soon || disabled}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
{Icon && Icon}
|
||||
{title}
|
||||
{soon && 'Soon'}
|
||||
</StyledButton>
|
||||
);
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
type Variant = 'primary' | 'secondary';
|
||||
|
||||
type MainButtonProps = {
|
||||
title: string;
|
||||
fullWidth?: boolean;
|
||||
width?: number;
|
||||
variant?: Variant;
|
||||
soon?: boolean;
|
||||
} & React.ComponentProps<'button'>;
|
||||
|
||||
const StyledButton = styled.button<
|
||||
Pick<MainButtonProps, 'fullWidth' | 'width' | 'variant'>
|
||||
>`
|
||||
align-items: center;
|
||||
background: ${({ theme, variant, disabled }) => {
|
||||
if (disabled === true) {
|
||||
return theme.background.secondary;
|
||||
}
|
||||
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return theme.background.primaryInverted;
|
||||
case 'secondary':
|
||||
return theme.background.primary;
|
||||
default:
|
||||
return theme.background.primary;
|
||||
}
|
||||
}};
|
||||
border: 1px solid;
|
||||
border-color: ${({ theme, disabled, variant }) => {
|
||||
if (disabled === true) {
|
||||
return theme.background.transparent.lighter;
|
||||
}
|
||||
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return theme.background.transparent.strong;
|
||||
case 'secondary':
|
||||
return theme.border.color.medium;
|
||||
default:
|
||||
return theme.background.primary;
|
||||
}
|
||||
}};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
${({ theme, disabled }) => {
|
||||
if (disabled === true) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `box-shadow: ${theme.boxShadow.light};`;
|
||||
}}
|
||||
color: ${({ theme, variant, disabled }) => {
|
||||
if (disabled === true) {
|
||||
return theme.font.color.light;
|
||||
}
|
||||
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return theme.font.color.inverted;
|
||||
case 'secondary':
|
||||
return theme.font.color.primary;
|
||||
default:
|
||||
return theme.font.color.primary;
|
||||
}
|
||||
}};
|
||||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-family: ${({ theme }) => theme.font.family};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
|
||||
width: ${({ fullWidth, width }) =>
|
||||
fullWidth ? '100%' : width ? `${width}px` : 'auto'};
|
||||
${({ theme, variant }) => {
|
||||
switch (variant) {
|
||||
case 'secondary':
|
||||
return `
|
||||
&:hover {
|
||||
background: ${theme.background.tertiary};
|
||||
}
|
||||
`;
|
||||
default:
|
||||
return `
|
||||
&:hover {
|
||||
background: ${theme.background.primaryInvertedHover}};
|
||||
}
|
||||
`;
|
||||
}
|
||||
}};
|
||||
`;
|
||||
|
||||
export const MainButton = ({
|
||||
title,
|
||||
width,
|
||||
fullWidth = false,
|
||||
variant = 'primary',
|
||||
type,
|
||||
onClick,
|
||||
disabled,
|
||||
className,
|
||||
}: MainButtonProps) => {
|
||||
return (
|
||||
<StyledButton
|
||||
className={className}
|
||||
{...{ disabled, fullWidth, width, onClick, type, variant }}
|
||||
>
|
||||
{title}
|
||||
</StyledButton>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
import styled from '@emotion/styled';
|
||||
import React, { useId } from 'react';
|
||||
|
||||
interface TextInputProps {
|
||||
label?: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
fullWidth?: boolean;
|
||||
error?: string;
|
||||
placeholder?: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const StyledContainer = styled.div<{ fullWidth?: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.label`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid ${({ theme }) => theme.color.gray30};
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.span`
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
|
||||
&::placeholder {
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledErrorHelper = styled.div`
|
||||
color: ${({ theme }) => theme.color.red};
|
||||
font-size: 12px;
|
||||
padding: 5px 0;
|
||||
`;
|
||||
|
||||
const TextInput: React.FC<TextInputProps> = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
fullWidth,
|
||||
error,
|
||||
placeholder,
|
||||
icon,
|
||||
}) => {
|
||||
const instanceId = useId();
|
||||
|
||||
return (
|
||||
<StyledContainer fullWidth={fullWidth}>
|
||||
{label && <StyledLabel htmlFor={instanceId}>{label}</StyledLabel>}
|
||||
<StyledInputContainer>
|
||||
{icon && <StyledIcon>{icon}</StyledIcon>}
|
||||
<StyledInput
|
||||
id={instanceId}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
{error && <StyledErrorHelper>{error}</StyledErrorHelper>}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export { TextInput };
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type ToggleSize = 'small' | 'medium';
|
||||
|
||||
type ContainerProps = {
|
||||
isOn: boolean;
|
||||
color?: string;
|
||||
toggleSize: ToggleSize;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div<ContainerProps>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme, isOn, color }) =>
|
||||
isOn ? (color ?? theme.color.blue) : theme.background.quaternary};
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: ${({ toggleSize }) => (toggleSize === 'small' ? 16 : 20)}px;
|
||||
transition: background-color 0.3s ease;
|
||||
width: ${({ toggleSize }) => (toggleSize === 'small' ? 24 : 32)}px;
|
||||
`;
|
||||
|
||||
const StyledCircle = styled(motion.div)<{
|
||||
toggleSize: ToggleSize;
|
||||
}>`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border-radius: 50%;
|
||||
height: ${({ toggleSize }) => (toggleSize === 'small' ? 12 : 16)}px;
|
||||
width: ${({ toggleSize }) => (toggleSize === 'small' ? 12 : 16)}px;
|
||||
`;
|
||||
|
||||
export type ToggleProps = {
|
||||
value?: boolean;
|
||||
onChange?: (value: boolean) => void;
|
||||
color?: string;
|
||||
toggleSize?: ToggleSize;
|
||||
};
|
||||
|
||||
export const Toggle = ({
|
||||
value,
|
||||
onChange,
|
||||
color,
|
||||
toggleSize = 'medium',
|
||||
}: ToggleProps) => {
|
||||
const [isOn, setIsOn] = useState(value ?? false);
|
||||
|
||||
const circleVariants = {
|
||||
on: { x: toggleSize === 'small' ? 10 : 14 },
|
||||
off: { x: 2 },
|
||||
};
|
||||
|
||||
const handleChange = () => {
|
||||
setIsOn(!isOn);
|
||||
|
||||
if (isDefined(onChange)) {
|
||||
onChange(!isOn);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== isOn) {
|
||||
setIsOn(value ?? false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
onClick={handleChange}
|
||||
isOn={isOn}
|
||||
color={color}
|
||||
toggleSize={toggleSize}
|
||||
>
|
||||
<StyledCircle
|
||||
animate={isOn ? 'on' : 'off'}
|
||||
variants={circleVariants}
|
||||
toggleSize={toggleSize}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
|
@ -1,15 +0,0 @@
|
|||
import { ThemeProvider } from '@emotion/react';
|
||||
|
||||
import { THEME_LIGHT } from '@/ui/theme/constants/ThemeLight';
|
||||
|
||||
type AppThemeProviderProps = {
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
const AppThemeProvider: React.FC<AppThemeProviderProps> = ({ children }) => {
|
||||
const theme = THEME_LIGHT;
|
||||
|
||||
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
|
||||
};
|
||||
|
||||
export { AppThemeProvider };
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { COLOR } from '@/ui/theme/constants/Colors';
|
||||
|
||||
export const ACCENT_DARK = {
|
||||
primary: COLOR.blueAccent75,
|
||||
secondary: COLOR.blueAccent80,
|
||||
tertiary: COLOR.blueAccent85,
|
||||
quaternary: COLOR.blueAccent90,
|
||||
accent3570: COLOR.blueAccent70,
|
||||
accent4060: COLOR.blueAccent60,
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { COLOR } from '@/ui/theme/constants/Colors';
|
||||
|
||||
export const ACCENT_LIGHT = {
|
||||
primary: COLOR.blueAccent25,
|
||||
secondary: COLOR.blueAccent20,
|
||||
tertiary: COLOR.blueAccent15,
|
||||
quaternary: COLOR.blueAccent10,
|
||||
accent3570: COLOR.blueAccent35,
|
||||
accent4060: COLOR.blueAccent40,
|
||||
};
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
export const ANIMATION = {
|
||||
duration: {
|
||||
instant: 0.075,
|
||||
fast: 0.15,
|
||||
normal: 0.3,
|
||||
},
|
||||
};
|
||||
|
||||
export type AnimationDuration = 'instant' | 'fast' | 'normal';
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
/* eslint-disable @nx/workspace-no-hardcoded-colors */
|
||||
import DarkNoise from '@/ui/theme/assets/dark-noise.jpg';
|
||||
import { COLOR } from '@/ui/theme/constants/Colors';
|
||||
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
|
||||
import { RGBA } from '@/ui/theme/constants/Rgba';
|
||||
|
||||
export const BACKGROUND_DARK = {
|
||||
noisy: `url(${DarkNoise.toString()});`,
|
||||
primary: GRAY_SCALE.gray85,
|
||||
secondary: GRAY_SCALE.gray80,
|
||||
tertiary: GRAY_SCALE.gray75,
|
||||
quaternary: GRAY_SCALE.gray70,
|
||||
danger: COLOR.red80,
|
||||
transparent: {
|
||||
primary: RGBA(GRAY_SCALE.gray85, 0.5),
|
||||
secondary: RGBA(GRAY_SCALE.gray80, 0.5),
|
||||
strong: RGBA(GRAY_SCALE.gray0, 0.14),
|
||||
medium: RGBA(GRAY_SCALE.gray0, 0.1),
|
||||
light: RGBA(GRAY_SCALE.gray0, 0.06),
|
||||
lighter: RGBA(GRAY_SCALE.gray0, 0.03),
|
||||
danger: RGBA(COLOR.red, 0.08),
|
||||
},
|
||||
overlay: RGBA(GRAY_SCALE.gray80, 0.8),
|
||||
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
|
||||
radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
|
||||
primaryInverted: GRAY_SCALE.gray20,
|
||||
primaryInvertedHover: GRAY_SCALE.gray15,
|
||||
};
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
/* eslint-disable @nx/workspace-no-hardcoded-colors */
|
||||
import LightNoise from '@/ui/theme/assets/light-noise.png';
|
||||
import { COLOR } from '@/ui/theme/constants/Colors';
|
||||
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
|
||||
import { RGBA } from '@/ui/theme/constants/Rgba';
|
||||
|
||||
export const BACKGROUND_LIGHT = {
|
||||
noisy: `url(${LightNoise.toString()});`,
|
||||
primary: GRAY_SCALE.gray0,
|
||||
secondary: GRAY_SCALE.gray10,
|
||||
tertiary: GRAY_SCALE.gray15,
|
||||
quaternary: GRAY_SCALE.gray20,
|
||||
danger: COLOR.red10,
|
||||
transparent: {
|
||||
primary: RGBA(GRAY_SCALE.gray0, 0.5),
|
||||
secondary: RGBA(GRAY_SCALE.gray10, 0.5),
|
||||
strong: RGBA(GRAY_SCALE.gray100, 0.16),
|
||||
medium: RGBA(GRAY_SCALE.gray100, 0.08),
|
||||
light: RGBA(GRAY_SCALE.gray100, 0.04),
|
||||
lighter: RGBA(GRAY_SCALE.gray100, 0.02),
|
||||
danger: RGBA(COLOR.red, 0.08),
|
||||
},
|
||||
overlay: RGBA(GRAY_SCALE.gray80, 0.8),
|
||||
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
|
||||
radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
|
||||
primaryInverted: GRAY_SCALE.gray60,
|
||||
primaryInvertedHover: GRAY_SCALE.gray55,
|
||||
};
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
export const BLUR = {
|
||||
light: 'blur(6px)',
|
||||
strong: 'blur(20px)',
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
export const BORDER_COMMON = {
|
||||
radius: {
|
||||
xs: '2px',
|
||||
sm: '4px',
|
||||
md: '8px',
|
||||
xl: '20px',
|
||||
pill: '999px',
|
||||
rounded: '100%',
|
||||
},
|
||||
};
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { BORDER_COMMON } from '@/ui/theme/constants/BorderCommon';
|
||||
import { COLOR } from '@/ui/theme/constants/Colors';
|
||||
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
|
||||
|
||||
export const BORDER_DARK = {
|
||||
color: {
|
||||
strong: GRAY_SCALE.gray55,
|
||||
medium: GRAY_SCALE.gray65,
|
||||
light: GRAY_SCALE.gray70,
|
||||
secondaryInverted: GRAY_SCALE.gray35,
|
||||
inverted: GRAY_SCALE.gray20,
|
||||
danger: COLOR.red70,
|
||||
},
|
||||
...BORDER_COMMON,
|
||||
};
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { BORDER_COMMON } from '@/ui/theme/constants/BorderCommon';
|
||||
import { COLOR } from '@/ui/theme/constants/Colors';
|
||||
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
|
||||
|
||||
export const BORDER_LIGHT = {
|
||||
color: {
|
||||
strong: GRAY_SCALE.gray25,
|
||||
medium: GRAY_SCALE.gray20,
|
||||
light: GRAY_SCALE.gray15,
|
||||
secondaryInverted: GRAY_SCALE.gray50,
|
||||
inverted: GRAY_SCALE.gray60,
|
||||
danger: COLOR.red20,
|
||||
},
|
||||
...BORDER_COMMON,
|
||||
};
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
|
||||
import { RGBA } from '@/ui/theme/constants/Rgba';
|
||||
|
||||
export const BOX_SHADOW_DARK = {
|
||||
light: `0px 2px 4px 0px ${RGBA(
|
||||
GRAY_SCALE.gray100,
|
||||
0.04,
|
||||
)}, 0px 0px 4px 0px ${RGBA(GRAY_SCALE.gray100, 0.08)}`,
|
||||
strong: `2px 4px 16px 0px ${RGBA(
|
||||
GRAY_SCALE.gray100,
|
||||
0.16,
|
||||
)}, 0px 2px 4px 0px ${RGBA(GRAY_SCALE.gray100, 0.08)}`,
|
||||
underline: `0px 1px 0px 0px ${RGBA(GRAY_SCALE.gray100, 0.32)}`,
|
||||
superHeavy: `2px 4px 16px 0px ${RGBA(
|
||||
GRAY_SCALE.gray100,
|
||||
0.12,
|
||||
)}, 0px 2px 4px 0px ${RGBA(GRAY_SCALE.gray100, 0.04)}`,
|
||||
};
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
|
||||
import { RGBA } from '@/ui/theme/constants/Rgba';
|
||||
|
||||
export const BOX_SHADOW_LIGHT = {
|
||||
light: `0px 2px 4px 0px ${RGBA(
|
||||
GRAY_SCALE.gray100,
|
||||
0.04,
|
||||
)}, 0px 0px 4px 0px ${RGBA(GRAY_SCALE.gray100, 0.08)}`,
|
||||
strong: `2px 4px 16px 0px ${RGBA(
|
||||
GRAY_SCALE.gray100,
|
||||
0.12,
|
||||
)}, 0px 2px 4px 0px ${RGBA(GRAY_SCALE.gray100, 0.04)}`,
|
||||
underline: `0px 1px 0px 0px ${RGBA(GRAY_SCALE.gray100, 0.32)}`,
|
||||
superHeavy: `0px 0px 8px 0px ${RGBA(
|
||||
GRAY_SCALE.gray100,
|
||||
0.16,
|
||||
)}, 0px 8px 64px -16px ${RGBA(
|
||||
GRAY_SCALE.gray100,
|
||||
0.48,
|
||||
)}, 0px 24px 56px -16px ${RGBA(GRAY_SCALE.gray100, 0.08)}`,
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { MAIN_COLORS } from '@/ui/theme/constants/MainColors';
|
||||
import { SECONDARY_COLORS } from '@/ui/theme/constants/SecondaryColors';
|
||||
|
||||
export const COLOR = {
|
||||
...MAIN_COLORS,
|
||||
...SECONDARY_COLORS,
|
||||
};
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
export const FONT_COMMON = {
|
||||
size: {
|
||||
xxs: '0.625rem',
|
||||
xs: '0.85rem',
|
||||
sm: '0.92rem',
|
||||
md: '1rem',
|
||||
lg: '1.23rem',
|
||||
xl: '1.54rem',
|
||||
xxl: '1.85rem',
|
||||
},
|
||||
weight: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
semiBold: 600,
|
||||
},
|
||||
family: 'Inter, sans-serif',
|
||||
};
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import { COLOR } from '@/ui/theme/constants/Colors';
|
||||
import { FONT_COMMON } from '@/ui/theme/constants/FontCommon';
|
||||
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
|
||||
|
||||
export const FONT_DARK = {
|
||||
color: {
|
||||
primary: GRAY_SCALE.gray20,
|
||||
secondary: GRAY_SCALE.gray35,
|
||||
tertiary: GRAY_SCALE.gray45,
|
||||
light: GRAY_SCALE.gray50,
|
||||
extraLight: GRAY_SCALE.gray55,
|
||||
inverted: GRAY_SCALE.gray100,
|
||||
danger: COLOR.red,
|
||||
},
|
||||
...FONT_COMMON,
|
||||
};
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import { COLOR } from '@/ui/theme/constants/Colors';
|
||||
import { FONT_COMMON } from '@/ui/theme/constants/FontCommon';
|
||||
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
|
||||
|
||||
export const FONT_LIGHT = {
|
||||
color: {
|
||||
primary: GRAY_SCALE.gray60,
|
||||
secondary: GRAY_SCALE.gray50,
|
||||
tertiary: GRAY_SCALE.gray40,
|
||||
light: GRAY_SCALE.gray35,
|
||||
extraLight: GRAY_SCALE.gray30,
|
||||
inverted: GRAY_SCALE.gray0,
|
||||
danger: COLOR.red,
|
||||
},
|
||||
...FONT_COMMON,
|
||||
};
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
/* eslint-disable @nx/workspace-no-hardcoded-colors */
|
||||
export const GRAY_SCALE = {
|
||||
gray100: '#000000',
|
||||
gray90: '#141414',
|
||||
gray85: '#171717',
|
||||
gray80: '#1b1b1b',
|
||||
gray75: '#1d1d1d',
|
||||
gray70: '#222222',
|
||||
gray65: '#292929',
|
||||
gray60: '#333333',
|
||||
gray55: '#4c4c4c',
|
||||
gray50: '#666666',
|
||||
gray45: '#818181',
|
||||
gray40: '#999999',
|
||||
gray35: '#b3b3b3',
|
||||
gray30: '#cccccc',
|
||||
gray25: '#d6d6d6',
|
||||
gray20: '#ebebeb',
|
||||
gray15: '#f1f1f1',
|
||||
gray10: '#fcfcfc',
|
||||
gray0: '#ffffff',
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { css } from '@emotion/react';
|
||||
|
||||
export const HOVER_BACKGROUND = (props: any) => css`
|
||||
transition: background 0.1s ease;
|
||||
&:hover {
|
||||
background: ${props.theme.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
export const ICON = {
|
||||
size: {
|
||||
sm: 14,
|
||||
md: 16,
|
||||
lg: 20,
|
||||
xl: 40,
|
||||
},
|
||||
stroke: {
|
||||
sm: 1.6,
|
||||
md: 2,
|
||||
lg: 2.5,
|
||||
},
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { MAIN_COLORS } from '@/ui/theme/constants/MainColors';
|
||||
|
||||
export const MAIN_COLOR_NAMES = Object.keys(MAIN_COLORS) as ThemeColor[];
|
||||
|
||||
export type ThemeColor = keyof typeof MAIN_COLORS;
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
/* eslint-disable @nx/workspace-no-hardcoded-colors */
|
||||
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
|
||||
|
||||
export const MAIN_COLORS = {
|
||||
green: '#55ef3c',
|
||||
turquoise: '#15de8f',
|
||||
sky: '#00e0ff',
|
||||
blue: '#1961ed',
|
||||
purple: '#915ffd',
|
||||
pink: '#f54bd0',
|
||||
red: '#f83e3e',
|
||||
orange: '#ff7222',
|
||||
yellow: '#ffd338',
|
||||
gray: GRAY_SCALE.gray30,
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
export const MOBILE_VIEWPORT = 768;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export const MODAL = {
|
||||
size: {
|
||||
sm: '300px',
|
||||
md: '400px',
|
||||
lg: '53%',
|
||||
},
|
||||
};
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { css } from '@emotion/react';
|
||||
|
||||
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
|
||||
|
||||
export const OVERLAY_BACKGROUND = (props: { theme: ThemeType }) => css`
|
||||
backdrop-filter: blur(12px) saturate(200%) contrast(50%) brightness(130%);
|
||||
background: ${props.theme.background.transparent.secondary};
|
||||
box-shadow: ${props.theme.boxShadow.strong};
|
||||
`;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/* eslint-disable @nx/workspace-no-hardcoded-colors */
|
||||
import hexRgb from 'hex-rgb';
|
||||
|
||||
export const RGBA = (hex: string, alpha: number) => {
|
||||
return `rgba(${hexRgb(hex, { format: 'array' })
|
||||
.slice(0, -1)
|
||||
.join(',')},${alpha})`;
|
||||
};
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
/* eslint-disable @nx/workspace-no-hardcoded-colors */
|
||||
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
|
||||
|
||||
export const SECONDARY_COLORS = {
|
||||
yellow80: '#2e2a1a',
|
||||
yellow70: '#453d1e',
|
||||
yellow60: '#746224',
|
||||
yellow50: '#b99b2e',
|
||||
yellow40: '#ffe074',
|
||||
yellow30: '#ffedaf',
|
||||
yellow20: '#fff6d7',
|
||||
yellow10: '#fffbeb',
|
||||
|
||||
green80: '#1d2d1b',
|
||||
green70: '#23421e',
|
||||
green60: '#2a5822',
|
||||
green50: '#42ae31',
|
||||
green40: '#88f477',
|
||||
green30: '#ccfac5',
|
||||
green20: '#ddfcd8',
|
||||
green10: '#eefdec',
|
||||
|
||||
turquoise80: '#172b23',
|
||||
turquoise70: '#173f2f',
|
||||
turquoise60: '#166747',
|
||||
turquoise50: '#16a26b',
|
||||
turquoise40: '#5be8b1',
|
||||
turquoise30: '#a1f2d2',
|
||||
turquoise20: '#d0f8e9',
|
||||
turquoise10: '#e8fcf4',
|
||||
|
||||
sky80: '#152b2e',
|
||||
sky70: '#123f45',
|
||||
sky60: '#0e6874',
|
||||
sky50: '#07a4b9',
|
||||
sky40: '#4de9ff',
|
||||
sky30: '#99f3ff',
|
||||
sky20: '#ccf9ff',
|
||||
sky10: '#e5fcff',
|
||||
|
||||
blue80: '#171e2c',
|
||||
blue70: '#172642',
|
||||
blue60: '#18356d',
|
||||
blue50: '#184bad',
|
||||
blue40: '#5e90f2',
|
||||
blue30: '#a3c0f8',
|
||||
blue20: '#d1dffb',
|
||||
blue10: '#e8effd',
|
||||
|
||||
purple80: '#231e2e',
|
||||
purple70: '#2f2545',
|
||||
purple60: '#483473',
|
||||
purple50: '#6c49b8',
|
||||
purple40: '#b28ffe',
|
||||
purple30: '#d3bffe',
|
||||
purple20: '#e9dfff',
|
||||
purple10: '#f4efff',
|
||||
|
||||
pink80: '#2d1c29',
|
||||
pink70: '#43213c',
|
||||
pink60: '#702c61',
|
||||
pink50: '#b23b98',
|
||||
pink40: '#f881de',
|
||||
pink30: '#fbb7ec',
|
||||
pink20: '#fddbf6',
|
||||
pink10: '#feedfa',
|
||||
|
||||
red80: '#2d1b1b',
|
||||
red70: '#441f1f',
|
||||
red60: '#712727',
|
||||
red50: '#b43232',
|
||||
red40: '#fa7878',
|
||||
red30: '#fcb2b2',
|
||||
red20: '#fed8d8',
|
||||
red10: '#feecec',
|
||||
|
||||
orange80: '#2e2018',
|
||||
orange70: '#452919',
|
||||
orange60: '#743b1b',
|
||||
orange50: '#b9571f',
|
||||
orange40: '#ff9c64',
|
||||
orange30: '#ffc7a7',
|
||||
orange20: '#ffe3d3',
|
||||
orange10: '#fff1e9',
|
||||
|
||||
gray80: GRAY_SCALE.gray70,
|
||||
gray70: GRAY_SCALE.gray65,
|
||||
gray60: GRAY_SCALE.gray55,
|
||||
gray50: GRAY_SCALE.gray40,
|
||||
gray40: GRAY_SCALE.gray25,
|
||||
gray30: GRAY_SCALE.gray20,
|
||||
gray20: GRAY_SCALE.gray15,
|
||||
gray10: GRAY_SCALE.gray10,
|
||||
blueAccent90: '#141a25',
|
||||
blueAccent85: '#151d2e',
|
||||
blueAccent80: '#152037',
|
||||
blueAccent75: '#16233f',
|
||||
blueAccent70: '#17294a',
|
||||
blueAccent60: '#18356d',
|
||||
blueAccent40: '#a3c0f8',
|
||||
blueAccent35: '#c8d9fb',
|
||||
blueAccent25: '#dae6fc',
|
||||
blueAccent20: '#e2ecfd',
|
||||
blueAccent15: '#edf2fe',
|
||||
blueAccent10: '#f5f9fd',
|
||||
};
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { COLOR } from '@/ui/theme/constants/Colors';
|
||||
|
||||
export const TAG_DARK = {
|
||||
text: {
|
||||
green: COLOR.green10,
|
||||
turquoise: COLOR.turquoise10,
|
||||
sky: COLOR.sky10,
|
||||
blue: COLOR.blue10,
|
||||
purple: COLOR.purple10,
|
||||
pink: COLOR.pink10,
|
||||
red: COLOR.red10,
|
||||
orange: COLOR.orange10,
|
||||
yellow: COLOR.yellow10,
|
||||
gray: COLOR.gray10,
|
||||
},
|
||||
background: {
|
||||
green: COLOR.green60,
|
||||
turquoise: COLOR.turquoise60,
|
||||
sky: COLOR.sky60,
|
||||
blue: COLOR.blue60,
|
||||
purple: COLOR.purple60,
|
||||
pink: COLOR.pink60,
|
||||
red: COLOR.red60,
|
||||
orange: COLOR.orange60,
|
||||
yellow: COLOR.yellow60,
|
||||
gray: COLOR.gray60,
|
||||
},
|
||||
};
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { COLOR } from '@/ui/theme/constants/Colors';
|
||||
|
||||
export const TAG_LIGHT = {
|
||||
text: {
|
||||
green: COLOR.green60,
|
||||
turquoise: COLOR.turquoise60,
|
||||
sky: COLOR.sky60,
|
||||
blue: COLOR.blue60,
|
||||
purple: COLOR.purple60,
|
||||
pink: COLOR.pink60,
|
||||
red: COLOR.red60,
|
||||
orange: COLOR.orange60,
|
||||
yellow: COLOR.yellow60,
|
||||
gray: COLOR.gray60,
|
||||
},
|
||||
background: {
|
||||
green: COLOR.green20,
|
||||
turquoise: COLOR.turquoise20,
|
||||
sky: COLOR.sky20,
|
||||
blue: COLOR.blue20,
|
||||
purple: COLOR.purple20,
|
||||
pink: COLOR.pink20,
|
||||
red: COLOR.red20,
|
||||
orange: COLOR.orange20,
|
||||
yellow: COLOR.yellow20,
|
||||
gray: COLOR.gray20,
|
||||
},
|
||||
};
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
export const TEXT = {
|
||||
lineHeight: {
|
||||
lg: 1.5,
|
||||
md: 1.2,
|
||||
},
|
||||
|
||||
iconSizeMedium: 16,
|
||||
iconSizeSmall: 14,
|
||||
|
||||
iconStrikeLight: 1.6,
|
||||
iconStrikeMedium: 2,
|
||||
iconStrikeBold: 2.5,
|
||||
};
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { css } from '@emotion/react';
|
||||
|
||||
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
|
||||
|
||||
export const TEXT_INPUT_STYLE = (props: { theme: ThemeType }) => css`
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: ${props.theme.font.color.primary};
|
||||
font-family: ${props.theme.font.family};
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
outline: none;
|
||||
padding: ${props.theme.spacing(0)} ${props.theme.spacing(2)};
|
||||
|
||||
&::placeholder,
|
||||
&::-webkit-input-placeholder {
|
||||
color: ${props.theme.font.color.light};
|
||||
font-family: ${props.theme.font.family};
|
||||
font-weight: ${props.theme.font.weight.medium};
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
/* eslint-disable @nx/workspace-no-hardcoded-colors */
|
||||
import { ANIMATION } from '@/ui/theme/constants/Animation';
|
||||
import { BLUR } from '@/ui/theme/constants/Blur';
|
||||
import { COLOR } from '@/ui/theme/constants/Colors';
|
||||
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
|
||||
import { ICON } from '@/ui/theme/constants/Icon';
|
||||
import { MODAL } from '@/ui/theme/constants/Modal';
|
||||
import { TEXT } from '@/ui/theme/constants/Text';
|
||||
|
||||
export const THEME_COMMON = {
|
||||
color: COLOR,
|
||||
grayScale: GRAY_SCALE,
|
||||
icon: ICON,
|
||||
modal: MODAL,
|
||||
text: TEXT,
|
||||
blur: BLUR,
|
||||
animation: ANIMATION,
|
||||
snackBar: {
|
||||
success: {
|
||||
background: '#16A26B',
|
||||
color: '#D0F8E9',
|
||||
},
|
||||
error: {
|
||||
background: '#B43232',
|
||||
color: '#FED8D8',
|
||||
},
|
||||
info: {
|
||||
background: COLOR.gray80,
|
||||
color: GRAY_SCALE.gray0,
|
||||
},
|
||||
},
|
||||
spacingMultiplicator: 4,
|
||||
spacing: (...args: number[]) =>
|
||||
args.map((multiplicator) => `${multiplicator * 4}px`).join(' '),
|
||||
betweenSiblingsGap: `2px`,
|
||||
table: {
|
||||
horizontalCellMargin: '8px',
|
||||
checkboxColumnWidth: '32px',
|
||||
horizontalCellPadding: '8px',
|
||||
},
|
||||
rightDrawerWidth: '500px',
|
||||
clickableElementBackgroundTransition: 'background 0.1s ease',
|
||||
lastLayerZIndex: 2147483647,
|
||||
};
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { ACCENT_DARK } from '@/ui/theme/constants/AccentDark';
|
||||
import { BACKGROUND_DARK } from '@/ui/theme/constants/BackgroundDark';
|
||||
import { BORDER_DARK } from '@/ui/theme/constants/BorderDark';
|
||||
import { BOX_SHADOW_DARK } from '@/ui/theme/constants/BoxShadowDark';
|
||||
import { FONT_DARK } from '@/ui/theme/constants/FontDark';
|
||||
import { TAG_DARK } from '@/ui/theme/constants/TagDark';
|
||||
import { THEME_COMMON } from '@/ui/theme/constants/ThemeCommon';
|
||||
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
|
||||
|
||||
export const THEME_DARK: ThemeType = {
|
||||
...THEME_COMMON,
|
||||
...{
|
||||
accent: ACCENT_DARK,
|
||||
background: BACKGROUND_DARK,
|
||||
border: BORDER_DARK,
|
||||
tag: TAG_DARK,
|
||||
boxShadow: BOX_SHADOW_DARK,
|
||||
font: FONT_DARK,
|
||||
name: 'dark',
|
||||
},
|
||||
};
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { ACCENT_LIGHT } from '@/ui/theme/constants/AccentLight';
|
||||
import { BACKGROUND_LIGHT } from '@/ui/theme/constants/BackgroundLight';
|
||||
import { BORDER_LIGHT } from '@/ui/theme/constants/BorderLight';
|
||||
import { BOX_SHADOW_LIGHT } from '@/ui/theme/constants/BoxShadowLight';
|
||||
import { FONT_LIGHT } from '@/ui/theme/constants/FontLight';
|
||||
import { TAG_LIGHT } from '@/ui/theme/constants/TagLight';
|
||||
import { THEME_COMMON } from '@/ui/theme/constants/ThemeCommon';
|
||||
|
||||
export const THEME_LIGHT = {
|
||||
...THEME_COMMON,
|
||||
...{
|
||||
accent: ACCENT_LIGHT,
|
||||
background: BACKGROUND_LIGHT,
|
||||
border: BORDER_LIGHT,
|
||||
tag: TAG_LIGHT,
|
||||
boxShadow: BOX_SHADOW_LIGHT,
|
||||
font: FONT_LIGHT,
|
||||
name: 'light',
|
||||
},
|
||||
};
|
||||
|
||||
export type ThemeType = typeof THEME_LIGHT;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
|
||||
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
|
||||
import PageInaccessible from '~/options/PageInaccessible';
|
||||
|
||||
import '~/index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
|
||||
<AppThemeProvider>
|
||||
<React.StrictMode>
|
||||
<PageInaccessible />
|
||||
</React.StrictMode>
|
||||
</AppThemeProvider>,
|
||||
);
|
||||
|
||||
declare module '@emotion/react' {
|
||||
export interface Theme extends ThemeType {}
|
||||
}
|
||||