Closes #2413 - Building a chrome extension for twenty to store person/company data into a workspace. (#3430)
* build: create a new vite project for chrome extension * feat: configure theme per the frontend codebase for chrome extension * feat: inject the add to twenty button into linkedin profile page * feat: create the api key form ui and render it on the options page * feat: inject the add to twenty button into linkedin company page * feat: scrape required data from both the user profile and the company profile * refactor: move modules into options because it is the only page using react for now * fix: show add to twenty button without having to reload the single page application * fix: extract domain of the business website instead of scrapping the industry type * feat: store api key to local storage and open options page when trying to store data without setting a key * feat: send data to the backend upon click and store it to the database * fix: open options page upon clicking the extension icon * fix: update terminology from user to person to match the codebase convention * fix: adopt chrome extension to monorepo approach using nx and get the development server working * fix: update vite config for build command to work per the requirement * feat: add instructions in the readme file to install the extension for local testing * fix: move server base url to a dotenv file and replace the hard-coded url * feat: permit user to configure a custom route for the server from the options page * fix: fetch api key and route from local storage and display on options page to inform users of their choices * fix: move front base url to dotenv and replace the hard-coded url * fix: remove the trailing slash from person and company linkedin username * fix: improve code commenting to explain implementation somewhat better * ci: introduce a workflow to build chrome extension to ensure it can be published * fix: format files to display code in a consistent manner per the prettier configuration in codebase * fix: improve the commenting significantly to explain important and hard-to-understand parts of the code * fix: remove unused permissions from the manifest file for publishing to the chrome web store * Add nx * Fix vale --------- Co-authored-by: Charles Bochet <charles@twenty.com>
							
								
								
									
										66
									
								
								.github/workflows/ci-chrome-extension.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,66 @@
 | 
				
			|||||||
 | 
					name: CI Chrome Extension
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - main
 | 
				
			||||||
 | 
					  pull_request:
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  chrome-extension-yarn-install:
 | 
				
			||||||
 | 
					    runs-on: ci-8-cores
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					      - name: Setup Node.js
 | 
				
			||||||
 | 
					        uses: actions/setup-node@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          node-version: "18"
 | 
				
			||||||
 | 
					      - name: Cache chrome extension node modules
 | 
				
			||||||
 | 
					        uses: actions/cache@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          path: packages/twenty-chrome-extension/node_modules
 | 
				
			||||||
 | 
					          key: chrome-extension-node_modules-${{hashFiles('yarn.lock')}}
 | 
				
			||||||
 | 
					          restore-keys: chrome-extension-node_modules-
 | 
				
			||||||
 | 
					      - name: Cache root node modules
 | 
				
			||||||
 | 
					        uses: actions/cache@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          path: node_modules
 | 
				
			||||||
 | 
					          key: root-node_modules-${{hashFiles('yarn.lock')}}
 | 
				
			||||||
 | 
					          restore-keys: root-node_modules-
 | 
				
			||||||
 | 
					      - name: Chrome Extension / Install Dependencies
 | 
				
			||||||
 | 
					        run: yarn
 | 
				
			||||||
 | 
					  chrome-extension-build:
 | 
				
			||||||
 | 
					    needs: chrome-extension-yarn-install
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					      - name: Setup Node.js
 | 
				
			||||||
 | 
					        uses: actions/setup-node@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          node-version: "18"
 | 
				
			||||||
 | 
					      - name: Cache chrome extension node modules
 | 
				
			||||||
 | 
					        uses: actions/cache@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          path: packages/twenty-chrome-extension/node_modules
 | 
				
			||||||
 | 
					          key: chrome-extension-node_modules-${{hashFiles('yarn.lock')}}
 | 
				
			||||||
 | 
					          restore-keys: chrome-extension-node_modules-
 | 
				
			||||||
 | 
					      - name: Cache root node modules
 | 
				
			||||||
 | 
					        uses: actions/cache@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          path: node_modules
 | 
				
			||||||
 | 
					          key: root-node_modules-${{hashFiles('yarn.lock')}}
 | 
				
			||||||
 | 
					          restore-keys: root-node_modules-
 | 
				
			||||||
 | 
					      - name: Chrome Extension / Run build
 | 
				
			||||||
 | 
					        run: yarn nx build twenty-chrome-extension
 | 
				
			||||||
							
								
								
									
										4
									
								
								.vscode/twenty.code-workspace
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -4,6 +4,10 @@
 | 
				
			|||||||
      "name": "ROOT",
 | 
					      "name": "ROOT",
 | 
				
			||||||
      "path": "../"
 | 
					      "path": "../"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "name": "packages/twenty-chrome-extension",
 | 
				
			||||||
 | 
					      "path": "../packages/twenty-chrome-extension"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      "name": "packages/twenty-docker",
 | 
					      "name": "packages/twenty-docker",
 | 
				
			||||||
      "path": "../packages/twenty-docker"
 | 
					      "path": "../packages/twenty-docker"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -283,6 +283,7 @@
 | 
				
			|||||||
  "version": "0.2.1",
 | 
					  "version": "0.2.1",
 | 
				
			||||||
  "workspaces": {
 | 
					  "workspaces": {
 | 
				
			||||||
    "packages": [
 | 
					    "packages": [
 | 
				
			||||||
 | 
					      "packages/twenty-chrome-extension",
 | 
				
			||||||
      "packages/twenty-front",
 | 
					      "packages/twenty-front",
 | 
				
			||||||
      "packages/twenty-docs",
 | 
					      "packages/twenty-docs",
 | 
				
			||||||
      "packages/twenty-server",
 | 
					      "packages/twenty-server",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								packages/twenty-chrome-extension/.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					VITE_SERVER_BASE_URL=http://localhost:3000
 | 
				
			||||||
 | 
					VITE_FRONT_BASE_URL=http://localhost:3001
 | 
				
			||||||
							
								
								
									
										18
									
								
								packages/twenty-chrome-extension/.eslintrc.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					  root: true,
 | 
				
			||||||
 | 
					  env: { browser: true, es2020: true },
 | 
				
			||||||
 | 
					  extends: [
 | 
				
			||||||
 | 
					    'eslint:recommended',
 | 
				
			||||||
 | 
					    'plugin:@typescript-eslint/recommended',
 | 
				
			||||||
 | 
					    'plugin:react-hooks/recommended',
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  ignorePatterns: ['dist', '.eslintrc.cjs'],
 | 
				
			||||||
 | 
					  parser: '@typescript-eslint/parser',
 | 
				
			||||||
 | 
					  plugins: ['react-refresh'],
 | 
				
			||||||
 | 
					  rules: {
 | 
				
			||||||
 | 
					    'react-refresh/only-export-components': [
 | 
				
			||||||
 | 
					      'warn',
 | 
				
			||||||
 | 
					      { allowConstantExport: true },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										24
									
								
								packages/twenty-chrome-extension/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					# 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?
 | 
				
			||||||
							
								
								
									
										51
									
								
								packages/twenty-chrome-extension/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					# 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: 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`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					yarn nx start twenty-chrome-extension
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- STEP 4: 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 5: 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 6: 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 7: 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 8: Reload any LinkedIn page that you opened before installing the extension for seamless experience.
 | 
				
			||||||
 | 
					- STEP 9: 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 THREE with `yarn nx build twenty-chrome-extension`.
 | 
				
			||||||
							
								
								
									
										12
									
								
								packages/twenty-chrome-extension/options.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8" />
 | 
				
			||||||
 | 
					    <link rel="icon" href="/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/index.tsx"></script>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										18
									
								
								packages/twenty-chrome-extension/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "twenty-chrome-extension",
 | 
				
			||||||
 | 
					  "description": "",
 | 
				
			||||||
 | 
					  "private": true,
 | 
				
			||||||
 | 
					  "version": "0.0.1",
 | 
				
			||||||
 | 
					  "type": "module",
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "nx": "NX_DEFAULT_PROJECT=twenty-chrome-extension node ../../node_modules/nx/bin/nx.js",
 | 
				
			||||||
 | 
					    "start": "vite",
 | 
				
			||||||
 | 
					    "build": "tsc && vite build"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@types/chrome": "^0.0.256"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@crxjs/vite-plugin": "^1.0.14"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								packages/twenty-chrome-extension/public/logo/32-32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 790 B  | 
| 
		 After Width: | Height: | Size: 350 KiB  | 
| 
		 After Width: | Height: | Size: 233 KiB  | 
| 
		 After Width: | Height: | Size: 650 KiB  | 
| 
		 After Width: | Height: | Size: 2.3 MiB  | 
| 
		 After Width: | Height: | Size: 830 KiB  | 
							
								
								
									
										1
									
								
								packages/twenty-chrome-extension/public/vite.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										62
									
								
								packages/twenty-chrome-extension/src/background/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					import { openOptionsPage } from './utils/openOptionsPage';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					console.log('Background Script Works');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Open options page programmatically in a new tab.
 | 
				
			||||||
 | 
					chrome.runtime.onInstalled.addListener(function (details) {
 | 
				
			||||||
 | 
					  if (details.reason === 'install') {
 | 
				
			||||||
 | 
					    openOptionsPage();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Open options page when extension icon is clicked.
 | 
				
			||||||
 | 
					chrome.action.onClicked.addListener(function () {
 | 
				
			||||||
 | 
					  openOptionsPage();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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 'getActiveTabUrl': // e.g. "https://linkedin.com/company/twenty/"
 | 
				
			||||||
 | 
					      chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
 | 
				
			||||||
 | 
					        if (tabs && tabs[0]) {
 | 
				
			||||||
 | 
					          const activeTabUrl: string | undefined = tabs[0].url;
 | 
				
			||||||
 | 
					          sendResponse({ url: activeTabUrl });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 'openOptionsPage':
 | 
				
			||||||
 | 
					      openOptionsPage();
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Keep track of the tabs in which the "Add to Twenty" button has already been injected.
 | 
				
			||||||
 | 
					// Could be that the content script is executed at "https://linkedin.com/feed/", but is needed at "https://linkedin.com/in/mabdullahabaid/".
 | 
				
			||||||
 | 
					// However, since Linkedin is a SPA, the script would not be re-executed when you navigate to "https://linkedin.com/in/mabdullahabaid/" from a user action.
 | 
				
			||||||
 | 
					// Therefore, this tracks if the user is on desired route and then re-executes the content script to create the "Add to Twenty" button.
 | 
				
			||||||
 | 
					// We use a "Set" to keep track of tab ids because it could be that the "Add to Twenty" button was created at "https://linkedin/com/company/twenty".
 | 
				
			||||||
 | 
					// However, when we change to about on the company page, the url becomes "https://www.linkedin.com/company/twenty/about/" and the button is created again.
 | 
				
			||||||
 | 
					// This creates a duplicate button, which we want to avoid. So, we instruct the extension to only create the button once for any of the following urls.
 | 
				
			||||||
 | 
					// "https://www.linkedin.com/company/twenty/" "https://www.linkedin.com/company/twenty/about/" "https://www.linkedin.com/company/twenty/people/".
 | 
				
			||||||
 | 
					const injectedTabs: Set<number> = new Set();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
 | 
				
			||||||
 | 
					  const isDesiredRoute =
 | 
				
			||||||
 | 
					    tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) ||
 | 
				
			||||||
 | 
					    tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (changeInfo.status === 'complete' && tab.active) {
 | 
				
			||||||
 | 
					    if (isDesiredRoute && !injectedTabs.has(tabId)) {
 | 
				
			||||||
 | 
					      chrome.tabs.sendMessage(tabId, { action: 'executeContentScript' });
 | 
				
			||||||
 | 
					      injectedTabs.add(tabId);
 | 
				
			||||||
 | 
					    } else if (!isDesiredRoute) {
 | 
				
			||||||
 | 
					      injectedTabs.delete(tabId); // Clear entry if navigated away from LinkedIn company page.
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					const openOptionsPage = () => {
 | 
				
			||||||
 | 
					  chrome.runtime.openOptionsPage();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { openOptionsPage };
 | 
				
			||||||
@@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					function createNewButton(
 | 
				
			||||||
 | 
					  text: string,
 | 
				
			||||||
 | 
					  onClickHandler: () => void,
 | 
				
			||||||
 | 
					): HTMLButtonElement {
 | 
				
			||||||
 | 
					  const newButton: HTMLButtonElement = document.createElement('button');
 | 
				
			||||||
 | 
					  newButton.textContent = text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Write universal styles for the button
 | 
				
			||||||
 | 
					  const buttonStyles = {
 | 
				
			||||||
 | 
					    border: '1px solid black',
 | 
				
			||||||
 | 
					    borderRadius: '20px',
 | 
				
			||||||
 | 
					    backgroundColor: 'black',
 | 
				
			||||||
 | 
					    color: 'white',
 | 
				
			||||||
 | 
					    fontSize: '1.5rem',
 | 
				
			||||||
 | 
					    fontWeight: '600',
 | 
				
			||||||
 | 
					    padding: '0.45em 1em',
 | 
				
			||||||
 | 
					    width: '15rem',
 | 
				
			||||||
 | 
					    height: '32px',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Apply common styles to the button.
 | 
				
			||||||
 | 
					  Object.assign(newButton.style, buttonStyles);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Apply common styles to specifc states of a button.
 | 
				
			||||||
 | 
					  newButton.addEventListener('mouseenter', () => {
 | 
				
			||||||
 | 
					    const hoverStyles = {
 | 
				
			||||||
 | 
					      backgroundColor: '#5e5e5e',
 | 
				
			||||||
 | 
					      borderColor: '#5e5e5e',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    Object.assign(newButton.style, hoverStyles);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  newButton.addEventListener('mouseleave', () => {
 | 
				
			||||||
 | 
					    Object.assign(newButton.style, buttonStyles);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Handle the click event.
 | 
				
			||||||
 | 
					  newButton.addEventListener('click', async () => {
 | 
				
			||||||
 | 
					    const { apiKey } = await chrome.storage.local.get('apiKey');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // If an api key is not set, the options page opens up to allow the user to configure an api key.
 | 
				
			||||||
 | 
					    if (!apiKey) {
 | 
				
			||||||
 | 
					      chrome.runtime.sendMessage({ action: 'openOptionsPage' });
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Update content during the resolution of the request.
 | 
				
			||||||
 | 
					    newButton.textContent = 'Saving...';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Call the provided onClickHandler function to handle button click logic
 | 
				
			||||||
 | 
					    onClickHandler();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return newButton;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default createNewButton;
 | 
				
			||||||
@@ -0,0 +1,106 @@
 | 
				
			|||||||
 | 
					import handleQueryParams from '../utils/handleQueryParams';
 | 
				
			||||||
 | 
					import requestDb from '../utils/requestDb';
 | 
				
			||||||
 | 
					import createNewButton from './createButton';
 | 
				
			||||||
 | 
					import extractCompanyLinkedinLink from './utils/extractCompanyLinkedinLink';
 | 
				
			||||||
 | 
					import extractDomain from './utils/extractDomain';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertButtonForCompany(): void {
 | 
				
			||||||
 | 
					  // Select the element in which to create the button.
 | 
				
			||||||
 | 
					  const parentDiv: HTMLDivElement | null = document.querySelector(
 | 
				
			||||||
 | 
					    '.org-top-card-primary-actions__inner',
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Create the button with desired callback funciton to execute upon click.
 | 
				
			||||||
 | 
					  if (parentDiv) {
 | 
				
			||||||
 | 
					    const newButtonCompany: HTMLButtonElement = createNewButton(
 | 
				
			||||||
 | 
					      'Add to Twenty',
 | 
				
			||||||
 | 
					      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 companyData = {
 | 
				
			||||||
 | 
					          name: companyName,
 | 
				
			||||||
 | 
					          domainName: domainName,
 | 
				
			||||||
 | 
					          address: address,
 | 
				
			||||||
 | 
					          employees: employees,
 | 
				
			||||||
 | 
					          linkedinLink: { url: '', label: '' },
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Extract active tab url using chrome API - an event is triggered here and is caught by background script.
 | 
				
			||||||
 | 
					        const { url: activeTabUrl } = await chrome.runtime.sendMessage({
 | 
				
			||||||
 | 
					          action: 'getActiveTabUrl',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Convert URLs like https://www.linkedin.com/company/twenty/about/ to https://www.linkedin.com/company/twenty
 | 
				
			||||||
 | 
					        const companyURL = extractCompanyLinkedinLink(activeTabUrl);
 | 
				
			||||||
 | 
					        companyData.linkedinLink = { url: companyURL, label: companyURL };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const query = `mutation CreateOneCompany { createCompany(data:{${handleQueryParams(
 | 
				
			||||||
 | 
					          companyData,
 | 
				
			||||||
 | 
					        )}}) {id} }`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const response = await requestDb(query);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (response.data) {
 | 
				
			||||||
 | 
					          newButtonCompany.textContent = 'Saved';
 | 
				
			||||||
 | 
					          newButtonCompany.setAttribute('disabled', 'true');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // Button specific styles once the button is unclickable after successfully sending data to server.
 | 
				
			||||||
 | 
					          newButtonCompany.addEventListener('mouseenter', () => {
 | 
				
			||||||
 | 
					            const hoverStyles = {
 | 
				
			||||||
 | 
					              backgroundColor: 'black',
 | 
				
			||||||
 | 
					              borderColor: 'black',
 | 
				
			||||||
 | 
					              cursor: 'default',
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            Object.assign(newButtonCompany.style, hoverStyles);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          newButtonCompany.textContent = 'Try Again';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Include the button in the DOM.
 | 
				
			||||||
 | 
					    parentDiv.prepend(newButtonCompany);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Write button specific styles here - common ones can be found in createButton.ts.
 | 
				
			||||||
 | 
					    const buttonSpecificStyles = {
 | 
				
			||||||
 | 
					      alignSelf: 'end',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Object.assign(newButtonCompany.style, buttonSpecificStyles);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default insertButtonForCompany;
 | 
				
			||||||
@@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					import handleQueryParams from '../utils/handleQueryParams';
 | 
				
			||||||
 | 
					import requestDb from '../utils/requestDb';
 | 
				
			||||||
 | 
					import createNewButton from './createButton';
 | 
				
			||||||
 | 
					import extractFirstAndLastName from './utils/extractFirstAndLastName';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function insertButtonForPerson(): void {
 | 
				
			||||||
 | 
					  // Select the element in which to create the button.
 | 
				
			||||||
 | 
					  const parentDiv: HTMLDivElement | null = document.querySelector(
 | 
				
			||||||
 | 
					    '.pv-top-card-v2-ctas',
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Create the button with desired callback funciton to execute upon click.
 | 
				
			||||||
 | 
					  if (parentDiv) {
 | 
				
			||||||
 | 
					    const newButtonPerson: HTMLButtonElement = createNewButton(
 | 
				
			||||||
 | 
					      'Add to Twenty',
 | 
				
			||||||
 | 
					      async () => {
 | 
				
			||||||
 | 
					        // Extract person-specific data from the DOM.
 | 
				
			||||||
 | 
					        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 = {
 | 
				
			||||||
 | 
					          name: { firstName, lastName },
 | 
				
			||||||
 | 
					          city: personCity,
 | 
				
			||||||
 | 
					          avatarUrl: profilePicture,
 | 
				
			||||||
 | 
					          jobTitle,
 | 
				
			||||||
 | 
					          linkedinLink: { url: '', label: '' },
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Extract active tab url using chrome API - an event is triggered here and is caught by background script.
 | 
				
			||||||
 | 
					        let { url: activeTabUrl } = await chrome.runtime.sendMessage({
 | 
				
			||||||
 | 
					          action: 'getActiveTabUrl',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Remove last slash from the URL for consistency when saving usernames.
 | 
				
			||||||
 | 
					        if (activeTabUrl.endsWith('/')) {
 | 
				
			||||||
 | 
					          activeTabUrl = activeTabUrl.slice(0, -1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const query = `mutation CreateOnePerson { createPerson(data:{${handleQueryParams(
 | 
				
			||||||
 | 
					          personData,
 | 
				
			||||||
 | 
					        )}}) {id} }`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const response = await requestDb(query);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (response.data) {
 | 
				
			||||||
 | 
					          newButtonPerson.textContent = 'Saved';
 | 
				
			||||||
 | 
					          newButtonPerson.setAttribute('disabled', 'true');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // Button specific styles once the button is unclickable after successfully sending data to server.
 | 
				
			||||||
 | 
					          newButtonPerson.addEventListener('mouseenter', () => {
 | 
				
			||||||
 | 
					            const hoverStyles = {
 | 
				
			||||||
 | 
					              backgroundColor: 'black',
 | 
				
			||||||
 | 
					              borderColor: 'black',
 | 
				
			||||||
 | 
					              cursor: 'default',
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            Object.assign(newButtonPerson.style, hoverStyles);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          newButtonPerson.textContent = 'Try Again';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Include the button in the DOM.
 | 
				
			||||||
 | 
					    parentDiv.prepend(newButtonPerson);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Write button specific styles here - common ones can be found in createButton.ts.
 | 
				
			||||||
 | 
					    const buttonSpecificStyles = {
 | 
				
			||||||
 | 
					      marginRight: '0.5em',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Object.assign(newButtonPerson.style, buttonSpecificStyles);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default insertButtonForPerson;
 | 
				
			||||||
							
								
								
									
										20
									
								
								packages/twenty-chrome-extension/src/contentScript/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					import insertButtonForPerson from './extractPersonProfile';
 | 
				
			||||||
 | 
					import insertButtonForCompany from './extractCompanyProfile';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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/
 | 
				
			||||||
 | 
					insertButtonForCompany();
 | 
				
			||||||
 | 
					insertButtonForPerson();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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((message, _, sendResponse) => {
 | 
				
			||||||
 | 
					  if (message.action === 'executeContentScript') {
 | 
				
			||||||
 | 
					    insertButtonForCompany();
 | 
				
			||||||
 | 
					    insertButtonForPerson();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  sendResponse('Executing!');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					// Extract "https://www.linkedin.com/company/twenty/" from any of the following urls, which the user can visit while on the company page.
 | 
				
			||||||
 | 
					// "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 (match && match[1]) {
 | 
				
			||||||
 | 
					    const companyID = match[1];
 | 
				
			||||||
 | 
					    const cleanCompanyURL = `https://www.linkedin.com/company/${companyID}`;
 | 
				
			||||||
 | 
					    return cleanCompanyURL;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return '';
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default extractCompanyLinkedinLink;
 | 
				
			||||||
@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					function 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;
 | 
				
			||||||
@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					// 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;
 | 
				
			||||||
							
								
								
									
										3
									
								
								packages/twenty-chrome-extension/src/global.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					/// <reference types="vite/client" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare const __APP_VERSION__: string;
 | 
				
			||||||
							
								
								
									
										11
									
								
								packages/twenty-chrome-extension/src/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					* {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  font-family: 'Inter', sans-serif;
 | 
				
			||||||
 | 
					  -webkit-font-smoothing: antialiased;
 | 
				
			||||||
 | 
					  -moz-osx-font-smoothing: grayscale;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html {
 | 
				
			||||||
 | 
					  font-size: 13px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										36
									
								
								packages/twenty-chrome-extension/src/manifest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					import { defineManifest } from '@crxjs/vite-plugin';
 | 
				
			||||||
 | 
					import packageData from '../package.json';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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: {},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  options_page: 'options.html',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  background: {
 | 
				
			||||||
 | 
					    service_worker: 'src/background/index.ts',
 | 
				
			||||||
 | 
					    type: 'module',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  content_scripts: [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      matches: ['https://www.linkedin.com/*'],
 | 
				
			||||||
 | 
					      js: ['src/contentScript/index.ts'],
 | 
				
			||||||
 | 
					      run_at: 'document_end',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  permissions: ['activeTab', 'storage'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  host_permissions: ['https://www.linkedin.com/*'],
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										21
									
								
								packages/twenty-chrome-extension/src/options/Options.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					import styled from '@emotion/styled';
 | 
				
			||||||
 | 
					import { ApiKeyForm } from './modules/api-key/components/ApiKeyForm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledContainer = styled.div`
 | 
				
			||||||
 | 
					  background: ${({ theme }) => theme.background.noisy};
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  height: 100vh;
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Options = () => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <StyledContainer>
 | 
				
			||||||
 | 
					      <ApiKeyForm />
 | 
				
			||||||
 | 
					    </StyledContainer>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Options;
 | 
				
			||||||
							
								
								
									
										19
									
								
								packages/twenty-chrome-extension/src/options/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import ReactDOM from 'react-dom/client';
 | 
				
			||||||
 | 
					import App from './Options';
 | 
				
			||||||
 | 
					import '../index.css';
 | 
				
			||||||
 | 
					import { AppThemeProvider } from './modules/ui/theme/components/AppThemeProvider';
 | 
				
			||||||
 | 
					import { ThemeType } from './modules/ui/theme/constants/theme';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
 | 
				
			||||||
 | 
					  <AppThemeProvider>
 | 
				
			||||||
 | 
					    <React.StrictMode>
 | 
				
			||||||
 | 
					      <App />
 | 
				
			||||||
 | 
					    </React.StrictMode>
 | 
				
			||||||
 | 
					  </AppThemeProvider>,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare module '@emotion/react' {
 | 
				
			||||||
 | 
					  // eslint-disable-next-line @typescript-eslint/no-empty-interface
 | 
				
			||||||
 | 
					  export interface Theme extends ThemeType {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,146 @@
 | 
				
			|||||||
 | 
					import styled from '@emotion/styled';
 | 
				
			||||||
 | 
					import { H2Title } from '../../ui/display/typography/components/H2Title';
 | 
				
			||||||
 | 
					import { useEffect, useState } from 'react';
 | 
				
			||||||
 | 
					import { TextInput } from '../../ui/input/components/TextInput';
 | 
				
			||||||
 | 
					import { Button } from '../../ui/input/button/Button';
 | 
				
			||||||
 | 
					import { Toggle } from '../../ui/input/components/Toggle';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledContainer = styled.div<{ isToggleOn: boolean }>`
 | 
				
			||||||
 | 
					  width: 400px;
 | 
				
			||||||
 | 
					  margin: 0 auto;
 | 
				
			||||||
 | 
					  background-color: ${({ theme }) => theme.background.primary};
 | 
				
			||||||
 | 
					  padding: ${({ theme }) => theme.spacing(10)};
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  transition: height 0.3s ease;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  height: ${({ isToggleOn }) => (isToggleOn ? '450px' : '390px')};
 | 
				
			||||||
 | 
					  max-height: ${({ isToggleOn }) => (isToggleOn ? '450px' : '390px')};
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledHeader = styled.header`
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  margin-bottom: ${({ theme }) => theme.spacing(8)};
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledImg = styled.img``;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledMain = styled.main`
 | 
				
			||||||
 | 
					  margin-bottom: ${({ theme }) => theme.spacing(8)};
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledFooter = styled.footer`
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledTitleContainer = styled.div`
 | 
				
			||||||
 | 
					  flex: 0 0 80%;
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledToggleContainer = styled.div`
 | 
				
			||||||
 | 
					  flex: 0 0 20%;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: flex-end;
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledSection = styled.div<{ showSection: boolean }>`
 | 
				
			||||||
 | 
					  transition:
 | 
				
			||||||
 | 
					    max-height 0.3s ease,
 | 
				
			||||||
 | 
					    opacity 0.3s ease;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  max-height: ${({ showSection }) => (showSection ? '200px' : '0')};
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ApiKeyForm = () => {
 | 
				
			||||||
 | 
					  const [apiKey, setApiKey] = useState('');
 | 
				
			||||||
 | 
					  const [route, setRoute] = useState('');
 | 
				
			||||||
 | 
					  const [showSection, setShowSection] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const getState = async () => {
 | 
				
			||||||
 | 
					      const localStorage = await chrome.storage.local.get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (localStorage.apiKey) {
 | 
				
			||||||
 | 
					        setApiKey(localStorage.apiKey);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (localStorage.serverBaseUrl) {
 | 
				
			||||||
 | 
					        setRoute(localStorage.serverBaseUrl);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void getState();
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    chrome.storage.local.set({ apiKey });
 | 
				
			||||||
 | 
					  }, [apiKey]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    chrome.storage.local.set({ serverBaseUrl: route });
 | 
				
			||||||
 | 
					  }, [route]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleGenerateClick = () => {
 | 
				
			||||||
 | 
					    window.open(
 | 
				
			||||||
 | 
					      `${import.meta.env.VITE_FRONT_BASE_URL}/settings/developers/api-keys`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleToggle = () => {
 | 
				
			||||||
 | 
					    setShowSection(!showSection);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <StyledContainer isToggleOn={showSection}>
 | 
				
			||||||
 | 
					      <StyledHeader>
 | 
				
			||||||
 | 
					        <StyledImg src="/logo/32-32.png" alt="Twenty Logo" />
 | 
				
			||||||
 | 
					      </StyledHeader>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <StyledMain>
 | 
				
			||||||
 | 
					        <H2Title
 | 
				
			||||||
 | 
					          title="Connect your account"
 | 
				
			||||||
 | 
					          description="Input your key to link the extension to your workspace."
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <TextInput
 | 
				
			||||||
 | 
					          label="Api key"
 | 
				
			||||||
 | 
					          value={apiKey}
 | 
				
			||||||
 | 
					          onChange={setApiKey}
 | 
				
			||||||
 | 
					          placeholder="My API key"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          title="Generate a key"
 | 
				
			||||||
 | 
					          fullWidth={false}
 | 
				
			||||||
 | 
					          variant="primary"
 | 
				
			||||||
 | 
					          accent="default"
 | 
				
			||||||
 | 
					          size="small"
 | 
				
			||||||
 | 
					          position="standalone"
 | 
				
			||||||
 | 
					          soon={false}
 | 
				
			||||||
 | 
					          disabled={false}
 | 
				
			||||||
 | 
					          onClick={handleGenerateClick}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </StyledMain>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <StyledFooter>
 | 
				
			||||||
 | 
					        <StyledTitleContainer>
 | 
				
			||||||
 | 
					          <H2Title
 | 
				
			||||||
 | 
					            title="Custom route"
 | 
				
			||||||
 | 
					            description="For developers interested in self-hosting or local testing of the extension."
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </StyledTitleContainer>
 | 
				
			||||||
 | 
					        <StyledToggleContainer>
 | 
				
			||||||
 | 
					          <Toggle value={showSection} onChange={handleToggle} />
 | 
				
			||||||
 | 
					        </StyledToggleContainer>
 | 
				
			||||||
 | 
					      </StyledFooter>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <StyledSection showSection={showSection}>
 | 
				
			||||||
 | 
					        {showSection && (
 | 
				
			||||||
 | 
					          <TextInput
 | 
				
			||||||
 | 
					            label="Route"
 | 
				
			||||||
 | 
					            value={route}
 | 
				
			||||||
 | 
					            onChange={setRoute}
 | 
				
			||||||
 | 
					            placeholder="My Route"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </StyledSection>
 | 
				
			||||||
 | 
					    </StyledContainer>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					import styled from '@emotion/styled';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type H2TitleProps = {
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  description?: string;
 | 
				
			||||||
 | 
					  addornment?: 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, addornment }: H2TitleProps) => (
 | 
				
			||||||
 | 
					  <StyledContainer>
 | 
				
			||||||
 | 
					    <StyledTitleContainer>
 | 
				
			||||||
 | 
					      <StyledTitle>{title}</StyledTitle>
 | 
				
			||||||
 | 
					      {addornment}
 | 
				
			||||||
 | 
					    </StyledTitleContainer>
 | 
				
			||||||
 | 
					    {description && <StyledDescription>{description}</StyledDescription>}
 | 
				
			||||||
 | 
					  </StyledContainer>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					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>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import styled from '@emotion/styled';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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.span`
 | 
				
			||||||
 | 
					  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 #ccc;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  padding: 8px;
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledIcon = styled.span`
 | 
				
			||||||
 | 
					  margin-right: 8px;
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledInput = styled.input`
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					  font-family: Arial, sans-serif;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &::placeholder {
 | 
				
			||||||
 | 
					    color: #aaa;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledErrorHelper = styled.div`
 | 
				
			||||||
 | 
					  color: #ff0000;
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  padding: 5px 0;
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TextInput: React.FC<TextInputProps> = ({
 | 
				
			||||||
 | 
					  label,
 | 
				
			||||||
 | 
					  value,
 | 
				
			||||||
 | 
					  onChange,
 | 
				
			||||||
 | 
					  fullWidth,
 | 
				
			||||||
 | 
					  error,
 | 
				
			||||||
 | 
					  placeholder,
 | 
				
			||||||
 | 
					  icon,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <StyledContainer fullWidth={fullWidth}>
 | 
				
			||||||
 | 
					      {label && <StyledLabel>{label}</StyledLabel>}
 | 
				
			||||||
 | 
					      <StyledInputContainer>
 | 
				
			||||||
 | 
					        {icon && <StyledIcon>{icon}</StyledIcon>}
 | 
				
			||||||
 | 
					        <StyledInput
 | 
				
			||||||
 | 
					          type="text"
 | 
				
			||||||
 | 
					          value={value}
 | 
				
			||||||
 | 
					          onChange={(e) => onChange(e.target.value)}
 | 
				
			||||||
 | 
					          placeholder={placeholder}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </StyledInputContainer>
 | 
				
			||||||
 | 
					      {error && <StyledErrorHelper>{error}</StyledErrorHelper>}
 | 
				
			||||||
 | 
					    </StyledContainer>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { TextInput };
 | 
				
			||||||
@@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					import { useEffect, useState } from 'react';
 | 
				
			||||||
 | 
					import styled from '@emotion/styled';
 | 
				
			||||||
 | 
					import { motion } from 'framer-motion';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 (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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 25 KiB  | 
| 
		 After Width: | Height: | Size: 9.4 KiB  | 
@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					import { ThemeProvider } from '@emotion/react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { lightTheme } from '../constants/theme';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AppThemeProviderProps = {
 | 
				
			||||||
 | 
					  children: JSX.Element;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AppThemeProvider: React.FC<AppThemeProviderProps> = ({ children }) => {
 | 
				
			||||||
 | 
					  const theme = lightTheme;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { AppThemeProvider };
 | 
				
			||||||
@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					import { color } from './colors';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const accentLight = {
 | 
				
			||||||
 | 
					  primary: color.blueAccent25,
 | 
				
			||||||
 | 
					  secondary: color.blueAccent20,
 | 
				
			||||||
 | 
					  tertiary: color.blueAccent15,
 | 
				
			||||||
 | 
					  quaternary: color.blueAccent10,
 | 
				
			||||||
 | 
					  accent3570: color.blueAccent35,
 | 
				
			||||||
 | 
					  accent4060: color.blueAccent40,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const accentDark = {
 | 
				
			||||||
 | 
					  primary: color.blueAccent75,
 | 
				
			||||||
 | 
					  secondary: color.blueAccent80,
 | 
				
			||||||
 | 
					  tertiary: color.blueAccent85,
 | 
				
			||||||
 | 
					  quaternary: color.blueAccent90,
 | 
				
			||||||
 | 
					  accent3570: color.blueAccent70,
 | 
				
			||||||
 | 
					  accent4060: color.blueAccent60,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					export const animation = {
 | 
				
			||||||
 | 
					  duration: {
 | 
				
			||||||
 | 
					    instant: 0.075,
 | 
				
			||||||
 | 
					    fast: 0.15,
 | 
				
			||||||
 | 
					    normal: 0.3,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AnimationDuration = 'instant' | 'fast' | 'normal';
 | 
				
			||||||
@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					/* eslint-disable twenty/no-hardcoded-colors */
 | 
				
			||||||
 | 
					import DarkNoise from '../assets/dark-noise.jpg';
 | 
				
			||||||
 | 
					import LightNoise from '../assets/light-noise.png';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { color, grayScale, rgba } from './colors';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const backgroundLight = {
 | 
				
			||||||
 | 
					  noisy: `url(${LightNoise.toString()});`,
 | 
				
			||||||
 | 
					  primary: grayScale.gray0,
 | 
				
			||||||
 | 
					  secondary: grayScale.gray10,
 | 
				
			||||||
 | 
					  tertiary: grayScale.gray15,
 | 
				
			||||||
 | 
					  quaternary: grayScale.gray20,
 | 
				
			||||||
 | 
					  danger: color.red10,
 | 
				
			||||||
 | 
					  transparent: {
 | 
				
			||||||
 | 
					    primary: rgba(grayScale.gray0, 0.8),
 | 
				
			||||||
 | 
					    secondary: rgba(grayScale.gray10, 0.8),
 | 
				
			||||||
 | 
					    strong: rgba(grayScale.gray100, 0.16),
 | 
				
			||||||
 | 
					    medium: rgba(grayScale.gray100, 0.08),
 | 
				
			||||||
 | 
					    light: rgba(grayScale.gray100, 0.04),
 | 
				
			||||||
 | 
					    lighter: rgba(grayScale.gray100, 0.02),
 | 
				
			||||||
 | 
					    danger: rgba(color.red, 0.08),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  overlay: rgba(grayScale.gray80, 0.8),
 | 
				
			||||||
 | 
					  radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${grayScale.gray60} 100%)`,
 | 
				
			||||||
 | 
					  radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${grayScale.gray60} 100%)`,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const backgroundDark = {
 | 
				
			||||||
 | 
					  noisy: `url(${DarkNoise.toString()});`,
 | 
				
			||||||
 | 
					  primary: grayScale.gray85,
 | 
				
			||||||
 | 
					  secondary: grayScale.gray80,
 | 
				
			||||||
 | 
					  tertiary: grayScale.gray75,
 | 
				
			||||||
 | 
					  quaternary: grayScale.gray70,
 | 
				
			||||||
 | 
					  danger: color.red80,
 | 
				
			||||||
 | 
					  transparent: {
 | 
				
			||||||
 | 
					    primary: rgba(grayScale.gray85, 0.8),
 | 
				
			||||||
 | 
					    secondary: rgba(grayScale.gray80, 0.8),
 | 
				
			||||||
 | 
					    strong: rgba(grayScale.gray0, 0.14),
 | 
				
			||||||
 | 
					    medium: rgba(grayScale.gray0, 0.1),
 | 
				
			||||||
 | 
					    light: rgba(grayScale.gray0, 0.06),
 | 
				
			||||||
 | 
					    lighter: rgba(grayScale.gray0, 0.03),
 | 
				
			||||||
 | 
					    danger: rgba(color.red, 0.08),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  overlay: rgba(grayScale.gray80, 0.8),
 | 
				
			||||||
 | 
					  radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${grayScale.gray60} 100%)`,
 | 
				
			||||||
 | 
					  radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${grayScale.gray60} 100%)`,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					export const blur = {
 | 
				
			||||||
 | 
					  light: 'blur(6px)',
 | 
				
			||||||
 | 
					  strong: 'blur(20px)',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					import { color, grayScale } from './colors';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const common = {
 | 
				
			||||||
 | 
					  radius: {
 | 
				
			||||||
 | 
					    xs: '2px',
 | 
				
			||||||
 | 
					    sm: '4px',
 | 
				
			||||||
 | 
					    md: '8px',
 | 
				
			||||||
 | 
					    rounded: '100%',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const borderLight = {
 | 
				
			||||||
 | 
					  color: {
 | 
				
			||||||
 | 
					    strong: grayScale.gray25,
 | 
				
			||||||
 | 
					    medium: grayScale.gray20,
 | 
				
			||||||
 | 
					    light: grayScale.gray15,
 | 
				
			||||||
 | 
					    secondaryInverted: grayScale.gray50,
 | 
				
			||||||
 | 
					    inverted: grayScale.gray60,
 | 
				
			||||||
 | 
					    danger: color.red20,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  ...common,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const borderDark = {
 | 
				
			||||||
 | 
					  color: {
 | 
				
			||||||
 | 
					    strong: grayScale.gray55,
 | 
				
			||||||
 | 
					    medium: grayScale.gray65,
 | 
				
			||||||
 | 
					    light: grayScale.gray70,
 | 
				
			||||||
 | 
					    secondaryInverted: grayScale.gray35,
 | 
				
			||||||
 | 
					    inverted: grayScale.gray20,
 | 
				
			||||||
 | 
					    danger: color.red70,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  ...common,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					import { grayScale, rgba } from './colors';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const boxShadowLight = {
 | 
				
			||||||
 | 
					  extraLight: `0px 1px 0px 0px ${rgba(grayScale.gray100, 0.04)}`,
 | 
				
			||||||
 | 
					  light: `0px 2px 4px 0px ${rgba(
 | 
				
			||||||
 | 
					    grayScale.gray100,
 | 
				
			||||||
 | 
					    0.04,
 | 
				
			||||||
 | 
					  )}, 0px 0px 4px 0px ${rgba(grayScale.gray100, 0.08)}`,
 | 
				
			||||||
 | 
					  strong: `2px 4px 16px 0px ${rgba(
 | 
				
			||||||
 | 
					    grayScale.gray100,
 | 
				
			||||||
 | 
					    0.12,
 | 
				
			||||||
 | 
					  )}, 0px 2px 4px 0px ${rgba(grayScale.gray100, 0.04)}`,
 | 
				
			||||||
 | 
					  underline: `0px 1px 0px 0px ${rgba(grayScale.gray100, 0.32)}`,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const boxShadowDark = {
 | 
				
			||||||
 | 
					  extraLight: `0px 1px 0px 0px ${rgba(grayScale.gray100, 0.04)}`,
 | 
				
			||||||
 | 
					  light: `0px 2px 4px 0px ${rgba(
 | 
				
			||||||
 | 
					    grayScale.gray100,
 | 
				
			||||||
 | 
					    0.04,
 | 
				
			||||||
 | 
					  )}, 0px 0px 4px 0px ${rgba(grayScale.gray100, 0.08)}`,
 | 
				
			||||||
 | 
					  strong: `2px 4px 16px 0px ${rgba(
 | 
				
			||||||
 | 
					    grayScale.gray100,
 | 
				
			||||||
 | 
					    0.16,
 | 
				
			||||||
 | 
					  )}, 0px 2px 4px 0px ${rgba(grayScale.gray100, 0.08)}`,
 | 
				
			||||||
 | 
					  underline: `0px 1px 0px 0px ${rgba(grayScale.gray100, 0.32)}`,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,153 @@
 | 
				
			|||||||
 | 
					/* eslint-disable twenty/no-hardcoded-colors */
 | 
				
			||||||
 | 
					import hexRgb from 'hex-rgb';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const grayScale = {
 | 
				
			||||||
 | 
					  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',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const mainColors = {
 | 
				
			||||||
 | 
					  yellow: '#ffd338',
 | 
				
			||||||
 | 
					  green: '#55ef3c',
 | 
				
			||||||
 | 
					  turquoise: '#15de8f',
 | 
				
			||||||
 | 
					  sky: '#00e0ff',
 | 
				
			||||||
 | 
					  blue: '#1961ed',
 | 
				
			||||||
 | 
					  purple: '#915ffd',
 | 
				
			||||||
 | 
					  pink: '#f54bd0',
 | 
				
			||||||
 | 
					  red: '#f83e3e',
 | 
				
			||||||
 | 
					  orange: '#ff7222',
 | 
				
			||||||
 | 
					  gray: grayScale.gray30,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ThemeColor = keyof typeof mainColors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const secondaryColors = {
 | 
				
			||||||
 | 
					  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: grayScale.gray70,
 | 
				
			||||||
 | 
					  gray70: grayScale.gray65,
 | 
				
			||||||
 | 
					  gray60: grayScale.gray55,
 | 
				
			||||||
 | 
					  gray50: grayScale.gray40,
 | 
				
			||||||
 | 
					  gray40: grayScale.gray25,
 | 
				
			||||||
 | 
					  gray30: grayScale.gray20,
 | 
				
			||||||
 | 
					  gray20: grayScale.gray15,
 | 
				
			||||||
 | 
					  gray10: grayScale.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',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const color = {
 | 
				
			||||||
 | 
					  ...mainColors,
 | 
				
			||||||
 | 
					  ...secondaryColors,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const rgba = (hex: string, alpha: number) => {
 | 
				
			||||||
 | 
					  const rgb = hexRgb(hex, { format: 'array' }).slice(0, -1).join(',');
 | 
				
			||||||
 | 
					  return `rgba(${rgb},${alpha})`;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					import { css } from '@emotion/react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ThemeType } from './theme';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const overlayBackground = (props: { theme: ThemeType }) =>
 | 
				
			||||||
 | 
					  css`
 | 
				
			||||||
 | 
					    backdrop-filter: blur(8px);
 | 
				
			||||||
 | 
					    background: ${props.theme.background.transparent.secondary};
 | 
				
			||||||
 | 
					    box-shadow: ${props.theme.boxShadow.strong};
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const textInputStyle = (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};
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const hoverBackground = (props: any) =>
 | 
				
			||||||
 | 
					  css`
 | 
				
			||||||
 | 
					    transition: background 0.1s ease;
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      background: ${props.theme.background.transparent.light};
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					import { color, grayScale } from './colors';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const 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',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fontLight = {
 | 
				
			||||||
 | 
					  color: {
 | 
				
			||||||
 | 
					    primary: grayScale.gray60,
 | 
				
			||||||
 | 
					    secondary: grayScale.gray50,
 | 
				
			||||||
 | 
					    tertiary: grayScale.gray40,
 | 
				
			||||||
 | 
					    light: grayScale.gray35,
 | 
				
			||||||
 | 
					    extraLight: grayScale.gray30,
 | 
				
			||||||
 | 
					    inverted: grayScale.gray0,
 | 
				
			||||||
 | 
					    danger: color.red,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  ...common,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fontDark = {
 | 
				
			||||||
 | 
					  color: {
 | 
				
			||||||
 | 
					    primary: grayScale.gray20,
 | 
				
			||||||
 | 
					    secondary: grayScale.gray35,
 | 
				
			||||||
 | 
					    tertiary: grayScale.gray45,
 | 
				
			||||||
 | 
					    light: grayScale.gray50,
 | 
				
			||||||
 | 
					    extraLight: grayScale.gray55,
 | 
				
			||||||
 | 
					    inverted: grayScale.gray100,
 | 
				
			||||||
 | 
					    danger: color.red,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  ...common,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					export const icon = {
 | 
				
			||||||
 | 
					  size: {
 | 
				
			||||||
 | 
					    sm: 14,
 | 
				
			||||||
 | 
					    md: 16,
 | 
				
			||||||
 | 
					    lg: 20,
 | 
				
			||||||
 | 
					    xl: 40,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  stroke: {
 | 
				
			||||||
 | 
					    sm: 1.6,
 | 
				
			||||||
 | 
					    md: 2,
 | 
				
			||||||
 | 
					    lg: 2.5,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					export const modal = {
 | 
				
			||||||
 | 
					  size: {
 | 
				
			||||||
 | 
					    sm: '300px',
 | 
				
			||||||
 | 
					    md: '400px',
 | 
				
			||||||
 | 
					    lg: '53%',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					import { color } from './colors';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const tagLight: { [key: string]: { [key: string]: string } } = {
 | 
				
			||||||
 | 
					  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,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const tagDark = {
 | 
				
			||||||
 | 
					  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,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					export const text = {
 | 
				
			||||||
 | 
					  lineHeight: {
 | 
				
			||||||
 | 
					    lg: 1.5,
 | 
				
			||||||
 | 
					    md: 1.2,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  iconSizeMedium: 16,
 | 
				
			||||||
 | 
					  iconSizeSmall: 14,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  iconStrikeLight: 1.6,
 | 
				
			||||||
 | 
					  iconStrikeMedium: 2,
 | 
				
			||||||
 | 
					  iconStrikeBold: 2.5,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					/* eslint-disable twenty/no-hardcoded-colors */
 | 
				
			||||||
 | 
					import { accentDark, accentLight } from './accent';
 | 
				
			||||||
 | 
					import { animation } from './animation';
 | 
				
			||||||
 | 
					import { backgroundDark, backgroundLight } from './background';
 | 
				
			||||||
 | 
					import { blur } from './blur';
 | 
				
			||||||
 | 
					import { borderDark, borderLight } from './border';
 | 
				
			||||||
 | 
					import { boxShadowDark, boxShadowLight } from './boxShadow';
 | 
				
			||||||
 | 
					import { color, grayScale } from './colors';
 | 
				
			||||||
 | 
					import { fontDark, fontLight } from './font';
 | 
				
			||||||
 | 
					import { icon } from './icon';
 | 
				
			||||||
 | 
					import { modal } from './modal';
 | 
				
			||||||
 | 
					import { tagDark, tagLight } from './tag';
 | 
				
			||||||
 | 
					import { text } from './text';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const common = {
 | 
				
			||||||
 | 
					  color: color,
 | 
				
			||||||
 | 
					  grayScale: grayScale,
 | 
				
			||||||
 | 
					  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: grayScale.gray0,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  spacingMultiplicator: 4,
 | 
				
			||||||
 | 
					  spacing: (multiplicator: number) => `${multiplicator * 4}px`,
 | 
				
			||||||
 | 
					  betweenSiblingsGap: `2px`,
 | 
				
			||||||
 | 
					  table: {
 | 
				
			||||||
 | 
					    horizontalCellMargin: '8px',
 | 
				
			||||||
 | 
					    checkboxColumnWidth: '32px',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  rightDrawerWidth: '500px',
 | 
				
			||||||
 | 
					  clickableElementBackgroundTransition: 'background 0.1s ease',
 | 
				
			||||||
 | 
					  lastLayerZIndex: 2147483647,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const lightTheme = {
 | 
				
			||||||
 | 
					  ...common,
 | 
				
			||||||
 | 
					  ...{
 | 
				
			||||||
 | 
					    accent: accentLight,
 | 
				
			||||||
 | 
					    background: backgroundLight,
 | 
				
			||||||
 | 
					    border: borderLight,
 | 
				
			||||||
 | 
					    tag: tagLight,
 | 
				
			||||||
 | 
					    boxShadow: boxShadowLight,
 | 
				
			||||||
 | 
					    font: fontLight,
 | 
				
			||||||
 | 
					    name: 'light',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export type ThemeType = typeof lightTheme;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const darkTheme: ThemeType = {
 | 
				
			||||||
 | 
					  ...common,
 | 
				
			||||||
 | 
					  ...{
 | 
				
			||||||
 | 
					    accent: accentDark,
 | 
				
			||||||
 | 
					    background: backgroundDark,
 | 
				
			||||||
 | 
					    border: borderDark,
 | 
				
			||||||
 | 
					    tag: tagDark,
 | 
				
			||||||
 | 
					    boxShadow: boxShadowDark,
 | 
				
			||||||
 | 
					    font: fontDark,
 | 
				
			||||||
 | 
					    name: 'dark',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MOBILE_VIEWPORT = 768;
 | 
				
			||||||
@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					// Convert extracted data into a structure that can be sent to the server for storage.
 | 
				
			||||||
 | 
					const handleQueryParams = (inputData: { [x: string]: unknown }): string => {
 | 
				
			||||||
 | 
					  let result = '';
 | 
				
			||||||
 | 
					  Object.keys(inputData).forEach((key) => {
 | 
				
			||||||
 | 
					    let quote = '';
 | 
				
			||||||
 | 
					    if (typeof inputData[key] === 'string') quote = '"';
 | 
				
			||||||
 | 
					    if (typeof inputData[key] === 'object') {
 | 
				
			||||||
 | 
					      result = result.concat(
 | 
				
			||||||
 | 
					        `${key}: {${handleQueryParams(
 | 
				
			||||||
 | 
					          inputData[key] as { [x: string]: unknown },
 | 
				
			||||||
 | 
					        )}}, `,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      result = result.concat(`${key}: ${quote}${inputData[key]}${quote}, `);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  if (result.length) result = result.slice(0, -2); // Remove the last ', '
 | 
				
			||||||
 | 
					  return result;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default handleQueryParams;
 | 
				
			||||||
							
								
								
									
										29
									
								
								packages/twenty-chrome-extension/src/utils/requestDb.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					const requestDb = async (query: string) => {
 | 
				
			||||||
 | 
					  const { apiKey } = await chrome.storage.local.get('apiKey');
 | 
				
			||||||
 | 
					  const { serverBaseUrl } = await chrome.storage.local.get('serverBaseUrl');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const options = {
 | 
				
			||||||
 | 
					    method: 'POST',
 | 
				
			||||||
 | 
					    body: JSON.stringify({ query }),
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					      Accept: 'application/json',
 | 
				
			||||||
 | 
					      Authorization: `Bearer ${apiKey}`,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const response = await fetch(
 | 
				
			||||||
 | 
					    `${
 | 
				
			||||||
 | 
					      serverBaseUrl ? serverBaseUrl : import.meta.env.VITE_SERVER_BASE_URL
 | 
				
			||||||
 | 
					    }/graphql`,
 | 
				
			||||||
 | 
					    options,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!response.ok) {
 | 
				
			||||||
 | 
					    console.error(response);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return await response.json();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default requestDb;
 | 
				
			||||||
							
								
								
									
										10
									
								
								packages/twenty-chrome-extension/src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					/// <reference types="vite/client" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ImportMetaEnv {
 | 
				
			||||||
 | 
					  readonly VITE_SERVER_BASE_URL: string;
 | 
				
			||||||
 | 
					  readonly VITE_FRONT_BASE_URL: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ImportMeta {
 | 
				
			||||||
 | 
					  readonly env: ImportMetaEnv;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								packages/twenty-chrome-extension/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "target": "ES2020",
 | 
				
			||||||
 | 
					    "useDefineForClassFields": true,
 | 
				
			||||||
 | 
					    "lib": ["ES2020", "DOM", "DOM.Iterable"],
 | 
				
			||||||
 | 
					    "module": "ESNext",
 | 
				
			||||||
 | 
					    "skipLibCheck": true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* Bundler mode */
 | 
				
			||||||
 | 
					    "moduleResolution": "bundler",
 | 
				
			||||||
 | 
					    "allowImportingTsExtensions": true,
 | 
				
			||||||
 | 
					    "resolveJsonModule": true,
 | 
				
			||||||
 | 
					    "isolatedModules": true,
 | 
				
			||||||
 | 
					    "noEmit": true,
 | 
				
			||||||
 | 
					    "jsx": "react-jsx",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /* Linting */
 | 
				
			||||||
 | 
					    "strict": true,
 | 
				
			||||||
 | 
					    "noUnusedLocals": true,
 | 
				
			||||||
 | 
					    "noUnusedParameters": true,
 | 
				
			||||||
 | 
					    "noFallthroughCasesInSwitch": true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "include": ["src/**/*"],
 | 
				
			||||||
 | 
					  "references": [{ "path": "./tsconfig.node.json" }]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										10
									
								
								packages/twenty-chrome-extension/tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "composite": true,
 | 
				
			||||||
 | 
					    "skipLibCheck": true,
 | 
				
			||||||
 | 
					    "module": "ESNext",
 | 
				
			||||||
 | 
					    "moduleResolution": "bundler",
 | 
				
			||||||
 | 
					    "allowSyntheticDefaultImports": true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "include": ["vite.config.ts", "package.json", "src/*"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										38
									
								
								packages/twenty-chrome-extension/vite.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					import { defineConfig, Plugin } from 'vite';
 | 
				
			||||||
 | 
					import { crx } from '@crxjs/vite-plugin';
 | 
				
			||||||
 | 
					import react from '@vitejs/plugin-react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import manifest from './src/manifest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const viteManifestHack: Plugin & {
 | 
				
			||||||
 | 
					  renderCrxManifest: (manifest: unknown, bundle: unknown) => void;
 | 
				
			||||||
 | 
					} = {
 | 
				
			||||||
 | 
					  // Workaround from https://github.com/crxjs/chrome-extension-tools/issues/846#issuecomment-1861880919.
 | 
				
			||||||
 | 
					  name: 'manifestHack',
 | 
				
			||||||
 | 
					  renderCrxManifest(_manifest, bundle) {
 | 
				
			||||||
 | 
					    bundle['manifest.json'] = bundle['.vite/manifest.json'];
 | 
				
			||||||
 | 
					    bundle['manifest.json'].fileName = 'manifest.json';
 | 
				
			||||||
 | 
					    delete bundle['.vite/manifest.json'];
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default defineConfig(() => {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    build: {
 | 
				
			||||||
 | 
					      emptyOutDir: true,
 | 
				
			||||||
 | 
					      outDir: 'dist',
 | 
				
			||||||
 | 
					      rollupOptions: {
 | 
				
			||||||
 | 
					        output: { chunkFileNames: 'assets/chunk-[hash].js' },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Adding this to fix websocket connection error.
 | 
				
			||||||
 | 
					    server: {
 | 
				
			||||||
 | 
					      port: 3002,
 | 
				
			||||||
 | 
					      strictPort: true,
 | 
				
			||||||
 | 
					      hmr: { port: 3002 },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    plugins: [viteManifestHack, crx({ manifest }), react()],
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										284
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						@@ -1544,6 +1544,29 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/core@npm:^7.23.5":
 | 
				
			||||||
 | 
					  version: 7.23.9
 | 
				
			||||||
 | 
					  resolution: "@babel/core@npm:7.23.9"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@ampproject/remapping": "npm:^2.2.0"
 | 
				
			||||||
 | 
					    "@babel/code-frame": "npm:^7.23.5"
 | 
				
			||||||
 | 
					    "@babel/generator": "npm:^7.23.6"
 | 
				
			||||||
 | 
					    "@babel/helper-compilation-targets": "npm:^7.23.6"
 | 
				
			||||||
 | 
					    "@babel/helper-module-transforms": "npm:^7.23.3"
 | 
				
			||||||
 | 
					    "@babel/helpers": "npm:^7.23.9"
 | 
				
			||||||
 | 
					    "@babel/parser": "npm:^7.23.9"
 | 
				
			||||||
 | 
					    "@babel/template": "npm:^7.23.9"
 | 
				
			||||||
 | 
					    "@babel/traverse": "npm:^7.23.9"
 | 
				
			||||||
 | 
					    "@babel/types": "npm:^7.23.9"
 | 
				
			||||||
 | 
					    convert-source-map: "npm:^2.0.0"
 | 
				
			||||||
 | 
					    debug: "npm:^4.1.0"
 | 
				
			||||||
 | 
					    gensync: "npm:^1.0.0-beta.2"
 | 
				
			||||||
 | 
					    json5: "npm:^2.2.3"
 | 
				
			||||||
 | 
					    semver: "npm:^6.3.1"
 | 
				
			||||||
 | 
					  checksum: 03883300bf1252ab4c9ba5b52f161232dd52873dbe5cde9289bb2bb26e935c42682493acbac9194a59a3b6cbd17f4c4c84030db8d6d482588afe64531532ff9b
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@babel/generator@npm:^7.14.0, @babel/generator@npm:^7.18.13, @babel/generator@npm:^7.22.5, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.3, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.7.2":
 | 
					"@babel/generator@npm:^7.14.0, @babel/generator@npm:^7.18.13, @babel/generator@npm:^7.22.5, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.3, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.7.2":
 | 
				
			||||||
  version: 7.23.6
 | 
					  version: 7.23.6
 | 
				
			||||||
  resolution: "@babel/generator@npm:7.23.6"
 | 
					  resolution: "@babel/generator@npm:7.23.6"
 | 
				
			||||||
@@ -1858,6 +1881,17 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/helpers@npm:^7.23.9":
 | 
				
			||||||
 | 
					  version: 7.23.9
 | 
				
			||||||
 | 
					  resolution: "@babel/helpers@npm:7.23.9"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/template": "npm:^7.23.9"
 | 
				
			||||||
 | 
					    "@babel/traverse": "npm:^7.23.9"
 | 
				
			||||||
 | 
					    "@babel/types": "npm:^7.23.9"
 | 
				
			||||||
 | 
					  checksum: f69fd0aca96a6fb8bd6dd044cd8a5c0f1851072d4ce23355345b9493c4032e76d1217f86b70df795e127553cf7f3fcd1587ede9d1b03b95e8b62681ca2165b87
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@babel/highlight@npm:^7.23.4":
 | 
					"@babel/highlight@npm:^7.23.4":
 | 
				
			||||||
  version: 7.23.4
 | 
					  version: 7.23.4
 | 
				
			||||||
  resolution: "@babel/highlight@npm:7.23.4"
 | 
					  resolution: "@babel/highlight@npm:7.23.4"
 | 
				
			||||||
@@ -1887,6 +1921,15 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/parser@npm:^7.23.9":
 | 
				
			||||||
 | 
					  version: 7.23.9
 | 
				
			||||||
 | 
					  resolution: "@babel/parser@npm:7.23.9"
 | 
				
			||||||
 | 
					  bin:
 | 
				
			||||||
 | 
					    parser: ./bin/babel-parser.js
 | 
				
			||||||
 | 
					  checksum: 7df97386431366d4810538db4b9ec538f4377096f720c0591c7587a16f6810e62747e9fbbfa1ff99257fd4330035e4fb1b5b77c7bd3b97ce0d2e3780a6618975
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3":
 | 
					"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3":
 | 
				
			||||||
  version: 7.23.3
 | 
					  version: 7.23.3
 | 
				
			||||||
  resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3"
 | 
					  resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3"
 | 
				
			||||||
@@ -2769,7 +2812,7 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@babel/plugin-transform-react-jsx-self@npm:^7.18.6":
 | 
					"@babel/plugin-transform-react-jsx-self@npm:^7.18.6, @babel/plugin-transform-react-jsx-self@npm:^7.23.3":
 | 
				
			||||||
  version: 7.23.3
 | 
					  version: 7.23.3
 | 
				
			||||||
  resolution: "@babel/plugin-transform-react-jsx-self@npm:7.23.3"
 | 
					  resolution: "@babel/plugin-transform-react-jsx-self@npm:7.23.3"
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
@@ -2780,7 +2823,7 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@babel/plugin-transform-react-jsx-source@npm:^7.19.6":
 | 
					"@babel/plugin-transform-react-jsx-source@npm:^7.19.6, @babel/plugin-transform-react-jsx-source@npm:^7.23.3":
 | 
				
			||||||
  version: 7.23.3
 | 
					  version: 7.23.3
 | 
				
			||||||
  resolution: "@babel/plugin-transform-react-jsx-source@npm:7.23.3"
 | 
					  resolution: "@babel/plugin-transform-react-jsx-source@npm:7.23.3"
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
@@ -3281,6 +3324,17 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/template@npm:^7.23.9":
 | 
				
			||||||
 | 
					  version: 7.23.9
 | 
				
			||||||
 | 
					  resolution: "@babel/template@npm:7.23.9"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/code-frame": "npm:^7.23.5"
 | 
				
			||||||
 | 
					    "@babel/parser": "npm:^7.23.9"
 | 
				
			||||||
 | 
					    "@babel/types": "npm:^7.23.9"
 | 
				
			||||||
 | 
					  checksum: 0e8b60119433787742bc08ae762bbd8d6755611c4cabbcb7627b292ec901a55af65d93d1c88572326069efb64136ef151ec91ffb74b2df7689bbab237030833a
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@babel/traverse@npm:^7.14.0, @babel/traverse@npm:^7.16.8, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.23.5":
 | 
					"@babel/traverse@npm:^7.14.0, @babel/traverse@npm:^7.16.8, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.23.5":
 | 
				
			||||||
  version: 7.23.5
 | 
					  version: 7.23.5
 | 
				
			||||||
  resolution: "@babel/traverse@npm:7.23.5"
 | 
					  resolution: "@babel/traverse@npm:7.23.5"
 | 
				
			||||||
@@ -3335,6 +3389,24 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/traverse@npm:^7.23.9":
 | 
				
			||||||
 | 
					  version: 7.23.9
 | 
				
			||||||
 | 
					  resolution: "@babel/traverse@npm:7.23.9"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/code-frame": "npm:^7.23.5"
 | 
				
			||||||
 | 
					    "@babel/generator": "npm:^7.23.6"
 | 
				
			||||||
 | 
					    "@babel/helper-environment-visitor": "npm:^7.22.20"
 | 
				
			||||||
 | 
					    "@babel/helper-function-name": "npm:^7.23.0"
 | 
				
			||||||
 | 
					    "@babel/helper-hoist-variables": "npm:^7.22.5"
 | 
				
			||||||
 | 
					    "@babel/helper-split-export-declaration": "npm:^7.22.6"
 | 
				
			||||||
 | 
					    "@babel/parser": "npm:^7.23.9"
 | 
				
			||||||
 | 
					    "@babel/types": "npm:^7.23.9"
 | 
				
			||||||
 | 
					    debug: "npm:^4.3.1"
 | 
				
			||||||
 | 
					    globals: "npm:^11.1.0"
 | 
				
			||||||
 | 
					  checksum: d1615d1d02f04d47111a7ea4446a1a6275668ca39082f31d51f08380de9502e19862be434eaa34b022ce9a17dbb8f9e2b73a746c654d9575f3a680a7ffdf5630
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.8, @babel/types@npm:^7.18.13, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.4, @babel/types@npm:^7.23.6, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
 | 
					"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.8, @babel/types@npm:^7.18.13, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.4, @babel/types@npm:^7.23.6, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
 | 
				
			||||||
  version: 7.23.6
 | 
					  version: 7.23.6
 | 
				
			||||||
  resolution: "@babel/types@npm:7.23.6"
 | 
					  resolution: "@babel/types@npm:7.23.6"
 | 
				
			||||||
@@ -3357,6 +3429,17 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@babel/types@npm:^7.23.9":
 | 
				
			||||||
 | 
					  version: 7.23.9
 | 
				
			||||||
 | 
					  resolution: "@babel/types@npm:7.23.9"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/helper-string-parser": "npm:^7.23.4"
 | 
				
			||||||
 | 
					    "@babel/helper-validator-identifier": "npm:^7.22.20"
 | 
				
			||||||
 | 
					    to-fast-properties: "npm:^2.0.0"
 | 
				
			||||||
 | 
					  checksum: edc7bb180ce7e4d2aea10c6972fb10474341ac39ba8fdc4a27ffb328368dfdfbf40fca18e441bbe7c483774500d5c05e222cec276c242e952853dcaf4eb884f7
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@base2/pretty-print-object@npm:1.0.1":
 | 
					"@base2/pretty-print-object@npm:1.0.1":
 | 
				
			||||||
  version: 1.0.1
 | 
					  version: 1.0.1
 | 
				
			||||||
  resolution: "@base2/pretty-print-object@npm:1.0.1"
 | 
					  resolution: "@base2/pretty-print-object@npm:1.0.1"
 | 
				
			||||||
@@ -3873,6 +3956,34 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@crxjs/vite-plugin@npm:^1.0.14":
 | 
				
			||||||
 | 
					  version: 1.0.14
 | 
				
			||||||
 | 
					  resolution: "@crxjs/vite-plugin@npm:1.0.14"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@rollup/pluginutils": "npm:^4.1.2"
 | 
				
			||||||
 | 
					    "@vitejs/plugin-react": "npm:>=1.2.0"
 | 
				
			||||||
 | 
					    "@webcomponents/custom-elements": "npm:^1.5.0"
 | 
				
			||||||
 | 
					    acorn-walk: "npm:^8.2.0"
 | 
				
			||||||
 | 
					    cheerio: "npm:^1.0.0-rc.10"
 | 
				
			||||||
 | 
					    connect-injector: "npm:^0.4.4"
 | 
				
			||||||
 | 
					    debug: "npm:^4.3.3"
 | 
				
			||||||
 | 
					    es-module-lexer: "npm:^0.10.0"
 | 
				
			||||||
 | 
					    fast-glob: "npm:^3.2.11"
 | 
				
			||||||
 | 
					    fs-extra: "npm:^10.0.1"
 | 
				
			||||||
 | 
					    jsesc: "npm:^3.0.2"
 | 
				
			||||||
 | 
					    magic-string: "npm:^0.26.0"
 | 
				
			||||||
 | 
					    picocolors: "npm:^1.0.0"
 | 
				
			||||||
 | 
					    react-refresh: "npm:^0.13.0"
 | 
				
			||||||
 | 
					    rollup: "npm:^2.70.2"
 | 
				
			||||||
 | 
					  peerDependencies:
 | 
				
			||||||
 | 
					    vite: ^2.9.0
 | 
				
			||||||
 | 
					  dependenciesMeta:
 | 
				
			||||||
 | 
					    "@vitejs/plugin-react":
 | 
				
			||||||
 | 
					      optional: true
 | 
				
			||||||
 | 
					  checksum: 19e203ddcfc3110973999bcc5224a0e8846b985a720d37ed55a945ed7ef726115bbb2ae06e5d30328c5c6338877acd1c77f27b35870fd92d2493b9bee65421f5
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@cspotcode/source-map-support@npm:^0.8.0":
 | 
					"@cspotcode/source-map-support@npm:^0.8.0":
 | 
				
			||||||
  version: 0.8.1
 | 
					  version: 0.8.1
 | 
				
			||||||
  resolution: "@cspotcode/source-map-support@npm:0.8.1"
 | 
					  resolution: "@cspotcode/source-map-support@npm:0.8.1"
 | 
				
			||||||
@@ -10786,6 +10897,16 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@rollup/pluginutils@npm:^4.1.2":
 | 
				
			||||||
 | 
					  version: 4.2.1
 | 
				
			||||||
 | 
					  resolution: "@rollup/pluginutils@npm:4.2.1"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    estree-walker: "npm:^2.0.1"
 | 
				
			||||||
 | 
					    picomatch: "npm:^2.2.2"
 | 
				
			||||||
 | 
					  checksum: 3ee56b2c8f1ed8dfd0a92631da1af3a2dfdd0321948f089b3752b4de1b54dc5076701eadd0e5fc18bd191b77af594ac1db6279e83951238ba16bf8a414c64c48
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@rollup/pluginutils@npm:^5.0.2, @rollup/pluginutils@npm:^5.0.5":
 | 
					"@rollup/pluginutils@npm:^5.0.2, @rollup/pluginutils@npm:^5.0.5":
 | 
				
			||||||
  version: 5.1.0
 | 
					  version: 5.1.0
 | 
				
			||||||
  resolution: "@rollup/pluginutils@npm:5.1.0"
 | 
					  resolution: "@rollup/pluginutils@npm:5.1.0"
 | 
				
			||||||
@@ -14530,7 +14651,7 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.18.0":
 | 
					"@types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.18.0, @types/babel__core@npm:^7.20.5":
 | 
				
			||||||
  version: 7.20.5
 | 
					  version: 7.20.5
 | 
				
			||||||
  resolution: "@types/babel__core@npm:7.20.5"
 | 
					  resolution: "@types/babel__core@npm:7.20.5"
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
@@ -14644,6 +14765,16 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@types/chrome@npm:^0.0.256":
 | 
				
			||||||
 | 
					  version: 0.0.256
 | 
				
			||||||
 | 
					  resolution: "@types/chrome@npm:0.0.256"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@types/filesystem": "npm:*"
 | 
				
			||||||
 | 
					    "@types/har-format": "npm:*"
 | 
				
			||||||
 | 
					  checksum: 35b3d2c92a3888cc14e5961421233003407a95078bf9b2f30c52a90470dae02588560bff1733ed3e7a8e9f12a1d0c5a6bae0ca30b6acdb3d723e1c2f29c8e861
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@types/codemirror@npm:^0.0.90":
 | 
					"@types/codemirror@npm:^0.0.90":
 | 
				
			||||||
  version: 0.0.90
 | 
					  version: 0.0.90
 | 
				
			||||||
  resolution: "@types/codemirror@npm:0.0.90"
 | 
					  resolution: "@types/codemirror@npm:0.0.90"
 | 
				
			||||||
@@ -14959,6 +15090,22 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@types/filesystem@npm:*":
 | 
				
			||||||
 | 
					  version: 0.0.35
 | 
				
			||||||
 | 
					  resolution: "@types/filesystem@npm:0.0.35"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@types/filewriter": "npm:*"
 | 
				
			||||||
 | 
					  checksum: 16a380e9774c5a9e1358f3ee28a3d85a93488443f235d160da3969aae7858dc6c6148cb3ff6b7e814f1c43f17940da0941f004373566d4fe7f75d9fda5efe246
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@types/filewriter@npm:*":
 | 
				
			||||||
 | 
					  version: 0.0.32
 | 
				
			||||||
 | 
					  resolution: "@types/filewriter@npm:0.0.32"
 | 
				
			||||||
 | 
					  checksum: 4feea7890d7945059f8eec0f89b1a4fe4f0522156c9345d9123c3498c6dba4584a17bd886daa4392a2e19bd9d16ee82aff9a0e1b837af507b612bcc6bd4c4305
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@types/find-cache-dir@npm:^3.2.1":
 | 
					"@types/find-cache-dir@npm:^3.2.1":
 | 
				
			||||||
  version: 3.2.1
 | 
					  version: 3.2.1
 | 
				
			||||||
  resolution: "@types/find-cache-dir@npm:3.2.1"
 | 
					  resolution: "@types/find-cache-dir@npm:3.2.1"
 | 
				
			||||||
@@ -15013,7 +15160,7 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@types/har-format@npm:^1.2.10":
 | 
					"@types/har-format@npm:*, @types/har-format@npm:^1.2.10":
 | 
				
			||||||
  version: 1.2.15
 | 
					  version: 1.2.15
 | 
				
			||||||
  resolution: "@types/har-format@npm:1.2.15"
 | 
					  resolution: "@types/har-format@npm:1.2.15"
 | 
				
			||||||
  checksum: 43ede55c3947e5cf59561f165930dc2eb3ae983fd162980cdc7274be1e7f528a6f77e65fee9a02a20d91b09bde10bed832b0470724f5c744ef6669015d00564e
 | 
					  checksum: 43ede55c3947e5cf59561f165930dc2eb3ae983fd162980cdc7274be1e7f528a6f77e65fee9a02a20d91b09bde10bed832b0470724f5c744ef6669015d00564e
 | 
				
			||||||
@@ -16361,6 +16508,21 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@vitejs/plugin-react@npm:>=1.2.0":
 | 
				
			||||||
 | 
					  version: 4.2.1
 | 
				
			||||||
 | 
					  resolution: "@vitejs/plugin-react@npm:4.2.1"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@babel/core": "npm:^7.23.5"
 | 
				
			||||||
 | 
					    "@babel/plugin-transform-react-jsx-self": "npm:^7.23.3"
 | 
				
			||||||
 | 
					    "@babel/plugin-transform-react-jsx-source": "npm:^7.23.3"
 | 
				
			||||||
 | 
					    "@types/babel__core": "npm:^7.20.5"
 | 
				
			||||||
 | 
					    react-refresh: "npm:^0.14.0"
 | 
				
			||||||
 | 
					  peerDependencies:
 | 
				
			||||||
 | 
					    vite: ^4.2.0 || ^5.0.0
 | 
				
			||||||
 | 
					  checksum: de1eec44d703f32e5b58e776328ca20793657fe991835d15b290230b19a2a08be5d31501d424279ae13ecfed28044c117b69d746891c8d9b92c69e8a8907e989
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@vitejs/plugin-react@npm:^3.0.1":
 | 
					"@vitejs/plugin-react@npm:^3.0.1":
 | 
				
			||||||
  version: 3.1.0
 | 
					  version: 3.1.0
 | 
				
			||||||
  resolution: "@vitejs/plugin-react@npm:3.1.0"
 | 
					  resolution: "@vitejs/plugin-react@npm:3.1.0"
 | 
				
			||||||
@@ -16558,6 +16720,13 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@webcomponents/custom-elements@npm:^1.5.0":
 | 
				
			||||||
 | 
					  version: 1.6.0
 | 
				
			||||||
 | 
					  resolution: "@webcomponents/custom-elements@npm:1.6.0"
 | 
				
			||||||
 | 
					  checksum: 8c3c3b0250ad7b063fe92b550fb725cc6074c8c5caea4a80901f9d9a93cdacf6dc0c73f715fa7b16f86e2ca1630e43cd80499bbf80e3a9b5c6ec042e074d22b4
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@whatwg-node/events@npm:^0.0.3":
 | 
					"@whatwg-node/events@npm:^0.0.3":
 | 
				
			||||||
  version: 0.0.3
 | 
					  version: 0.0.3
 | 
				
			||||||
  resolution: "@whatwg-node/events@npm:0.0.3"
 | 
					  resolution: "@whatwg-node/events@npm:0.0.3"
 | 
				
			||||||
@@ -16854,7 +17023,7 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1":
 | 
					"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.2.0":
 | 
				
			||||||
  version: 8.3.1
 | 
					  version: 8.3.1
 | 
				
			||||||
  resolution: "acorn-walk@npm:8.3.1"
 | 
					  resolution: "acorn-walk@npm:8.3.1"
 | 
				
			||||||
  checksum: a23d2f7c6b6cad617f4c77f14dfeb062a239208d61753e9ba808d916c550add92b39535467d2e6028280761ac4f5a904cc9df21530b84d3f834e3edef74ddde5
 | 
					  checksum: a23d2f7c6b6cad617f4c77f14dfeb062a239208d61753e9ba808d916c550add92b39535467d2e6028280761ac4f5a904cc9df21530b84d3f834e3edef74ddde5
 | 
				
			||||||
@@ -20273,7 +20442,7 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"cheerio@npm:^1.0.0-rc.12":
 | 
					"cheerio@npm:^1.0.0-rc.10, cheerio@npm:^1.0.0-rc.12":
 | 
				
			||||||
  version: 1.0.0-rc.12
 | 
					  version: 1.0.0-rc.12
 | 
				
			||||||
  resolution: "cheerio@npm:1.0.0-rc.12"
 | 
					  resolution: "cheerio@npm:1.0.0-rc.12"
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
@@ -21230,6 +21399,18 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"connect-injector@npm:^0.4.4":
 | 
				
			||||||
 | 
					  version: 0.4.4
 | 
				
			||||||
 | 
					  resolution: "connect-injector@npm:0.4.4"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    debug: "npm:^2.0.0"
 | 
				
			||||||
 | 
					    q: "npm:^1.0.1"
 | 
				
			||||||
 | 
					    stream-buffers: "npm:^0.2.3"
 | 
				
			||||||
 | 
					    uberproto: "npm:^1.1.0"
 | 
				
			||||||
 | 
					  checksum: 6186a21285db6e989d610e7e2223305f59f1f11d4977bbf62db21680eb6b2f9b6080d5f03171d98d346496388c0cdb3f38bcec9bfebd3a58bc258ce12186ea1c
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"consola@npm:^2.15.0, consola@npm:^2.15.3":
 | 
					"consola@npm:^2.15.0, consola@npm:^2.15.3":
 | 
				
			||||||
  version: 2.15.3
 | 
					  version: 2.15.3
 | 
				
			||||||
  resolution: "consola@npm:2.15.3"
 | 
					  resolution: "consola@npm:2.15.3"
 | 
				
			||||||
@@ -22314,7 +22495,7 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"debug@npm:2.6.9, debug@npm:^2.6.0, debug@npm:^2.6.8, debug@npm:^2.6.9":
 | 
					"debug@npm:2.6.9, debug@npm:^2.0.0, debug@npm:^2.6.0, debug@npm:^2.6.8, debug@npm:^2.6.9":
 | 
				
			||||||
  version: 2.6.9
 | 
					  version: 2.6.9
 | 
				
			||||||
  resolution: "debug@npm:2.6.9"
 | 
					  resolution: "debug@npm:2.6.9"
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
@@ -23497,6 +23678,13 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"es-module-lexer@npm:^0.10.0":
 | 
				
			||||||
 | 
					  version: 0.10.5
 | 
				
			||||||
 | 
					  resolution: "es-module-lexer@npm:0.10.5"
 | 
				
			||||||
 | 
					  checksum: 5a199242971341fefe12ce5984602698d8f9c477e207f847aaed0f70519cf2c68ddbd22dd92b2cc4669a9d421a3b89a67d371994b64604ea24da21d35c42089e
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"es-module-lexer@npm:^0.9.3":
 | 
					"es-module-lexer@npm:^0.9.3":
 | 
				
			||||||
  version: 0.9.3
 | 
					  version: 0.9.3
 | 
				
			||||||
  resolution: "es-module-lexer@npm:0.9.3"
 | 
					  resolution: "es-module-lexer@npm:0.9.3"
 | 
				
			||||||
@@ -24339,7 +24527,7 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"estree-walker@npm:^2.0.2":
 | 
					"estree-walker@npm:^2.0.1, estree-walker@npm:^2.0.2":
 | 
				
			||||||
  version: 2.0.2
 | 
					  version: 2.0.2
 | 
				
			||||||
  resolution: "estree-walker@npm:2.0.2"
 | 
					  resolution: "estree-walker@npm:2.0.2"
 | 
				
			||||||
  checksum: 53a6c54e2019b8c914dc395890153ffdc2322781acf4bd7d1a32d7aedc1710807bdcd866ac133903d5629ec601fbb50abe8c2e5553c7f5a0afdd9b6af6c945af
 | 
					  checksum: 53a6c54e2019b8c914dc395890153ffdc2322781acf4bd7d1a32d7aedc1710807bdcd866ac133903d5629ec601fbb50abe8c2e5553c7f5a0afdd9b6af6c945af
 | 
				
			||||||
@@ -25549,7 +25737,7 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"fs-extra@npm:^10.0.0, fs-extra@npm:^10.1.0":
 | 
					"fs-extra@npm:^10.0.0, fs-extra@npm:^10.0.1, fs-extra@npm:^10.1.0":
 | 
				
			||||||
  version: 10.1.0
 | 
					  version: 10.1.0
 | 
				
			||||||
  resolution: "fs-extra@npm:10.1.0"
 | 
					  resolution: "fs-extra@npm:10.1.0"
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
@@ -30290,6 +30478,15 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"jsesc@npm:^3.0.2":
 | 
				
			||||||
 | 
					  version: 3.0.2
 | 
				
			||||||
 | 
					  resolution: "jsesc@npm:3.0.2"
 | 
				
			||||||
 | 
					  bin:
 | 
				
			||||||
 | 
					    jsesc: bin/jsesc
 | 
				
			||||||
 | 
					  checksum: ef22148f9e793180b14d8a145ee6f9f60f301abf443288117b4b6c53d0ecd58354898dc506ccbb553a5f7827965cd38bc5fb726575aae93c5e8915e2de8290e1
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"jsesc@npm:~0.5.0":
 | 
					"jsesc@npm:~0.5.0":
 | 
				
			||||||
  version: 0.5.0
 | 
					  version: 0.5.0
 | 
				
			||||||
  resolution: "jsesc@npm:0.5.0"
 | 
					  resolution: "jsesc@npm:0.5.0"
 | 
				
			||||||
@@ -31538,6 +31735,15 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"magic-string@npm:^0.26.0":
 | 
				
			||||||
 | 
					  version: 0.26.7
 | 
				
			||||||
 | 
					  resolution: "magic-string@npm:0.26.7"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    sourcemap-codec: "npm:^1.4.8"
 | 
				
			||||||
 | 
					  checksum: 950035b344fe2a8163668980bc4a215a0b225086e6e22100fd947e7647053c6ba6b4f11a04de83a97a276526ccb602ef53b173725dbb1971fb146cff5a5e14f6
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"magic-string@npm:^0.27.0":
 | 
					"magic-string@npm:^0.27.0":
 | 
				
			||||||
  version: 0.27.0
 | 
					  version: 0.27.0
 | 
				
			||||||
  resolution: "magic-string@npm:0.27.0"
 | 
					  resolution: "magic-string@npm:0.27.0"
 | 
				
			||||||
@@ -36647,7 +36853,7 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.0, picomatch@npm:^2.3.1":
 | 
					"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.2, picomatch@npm:^2.2.3, picomatch@npm:^2.3.0, picomatch@npm:^2.3.1":
 | 
				
			||||||
  version: 2.3.1
 | 
					  version: 2.3.1
 | 
				
			||||||
  resolution: "picomatch@npm:2.3.1"
 | 
					  resolution: "picomatch@npm:2.3.1"
 | 
				
			||||||
  checksum: 26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be
 | 
					  checksum: 26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be
 | 
				
			||||||
@@ -38017,6 +38223,13 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"q@npm:^1.0.1":
 | 
				
			||||||
 | 
					  version: 1.5.1
 | 
				
			||||||
 | 
					  resolution: "q@npm:1.5.1"
 | 
				
			||||||
 | 
					  checksum: 7855fbdba126cb7e92ef3a16b47ba998c0786ec7fface236e3eb0135b65df36429d91a86b1fff3ab0927b4ac4ee88a2c44527c7c3b8e2a37efbec9fe34803df4
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"qs@npm:6.11.0":
 | 
					"qs@npm:6.11.0":
 | 
				
			||||||
  version: 6.11.0
 | 
					  version: 6.11.0
 | 
				
			||||||
  resolution: "qs@npm:6.11.0"
 | 
					  resolution: "qs@npm:6.11.0"
 | 
				
			||||||
@@ -38622,6 +38835,13 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"react-refresh@npm:^0.13.0":
 | 
				
			||||||
 | 
					  version: 0.13.0
 | 
				
			||||||
 | 
					  resolution: "react-refresh@npm:0.13.0"
 | 
				
			||||||
 | 
					  checksum: cb9f324d471485e569628854dc08d1550c0798cde57f1bfb8d954e006659de1da0bdccaf7d5d2ac0d3d53df1aae7b740b2a36128789afb8aff0f7ec01b128587
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"react-refresh@npm:^0.14.0":
 | 
					"react-refresh@npm:^0.14.0":
 | 
				
			||||||
  version: 0.14.0
 | 
					  version: 0.14.0
 | 
				
			||||||
  resolution: "react-refresh@npm:0.14.0"
 | 
					  resolution: "react-refresh@npm:0.14.0"
 | 
				
			||||||
@@ -40100,6 +40320,20 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"rollup@npm:^2.70.2":
 | 
				
			||||||
 | 
					  version: 2.79.1
 | 
				
			||||||
 | 
					  resolution: "rollup@npm:2.79.1"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    fsevents: "npm:~2.3.2"
 | 
				
			||||||
 | 
					  dependenciesMeta:
 | 
				
			||||||
 | 
					    fsevents:
 | 
				
			||||||
 | 
					      optional: true
 | 
				
			||||||
 | 
					  bin:
 | 
				
			||||||
 | 
					    rollup: dist/bin/rollup
 | 
				
			||||||
 | 
					  checksum: 421418687f5dcd7324f4387f203c6bfc7118b7ace789e30f5da022471c43e037a76f5fd93837052754eeeae798a4fb266ac05ccee1e594406d912a59af98dde9
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"rollup@npm:^4.0.2":
 | 
					"rollup@npm:^4.0.2":
 | 
				
			||||||
  version: 4.9.2
 | 
					  version: 4.9.2
 | 
				
			||||||
  resolution: "rollup@npm:4.9.2"
 | 
					  resolution: "rollup@npm:4.9.2"
 | 
				
			||||||
@@ -41177,6 +41411,13 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"sourcemap-codec@npm:^1.4.8":
 | 
				
			||||||
 | 
					  version: 1.4.8
 | 
				
			||||||
 | 
					  resolution: "sourcemap-codec@npm:1.4.8"
 | 
				
			||||||
 | 
					  checksum: f099279fdaae070ff156df7414bbe39aad69cdd615454947ed3e19136bfdfcb4544952685ee73f56e17038f4578091e12b17b283ed8ac013882916594d95b9e6
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"space-separated-tokens@npm:^1.0.0":
 | 
					"space-separated-tokens@npm:^1.0.0":
 | 
				
			||||||
  version: 1.1.5
 | 
					  version: 1.1.5
 | 
				
			||||||
  resolution: "space-separated-tokens@npm:1.1.5"
 | 
					  resolution: "space-separated-tokens@npm:1.1.5"
 | 
				
			||||||
@@ -41541,6 +41782,13 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"stream-buffers@npm:^0.2.3":
 | 
				
			||||||
 | 
					  version: 0.2.6
 | 
				
			||||||
 | 
					  resolution: "stream-buffers@npm:0.2.6"
 | 
				
			||||||
 | 
					  checksum: 8d685a5f98e0b392802fc07617f31e6ae63652ed2fff7fe7df309222ffb06502f47b31ab35c2cf9b4de0320f657ed3aa6d697641f0f72f5c6f3a703ba8d7b594
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"stream-combiner2@npm:^1.1.1":
 | 
					"stream-combiner2@npm:^1.1.1":
 | 
				
			||||||
  version: 1.1.1
 | 
					  version: 1.1.1
 | 
				
			||||||
  resolution: "stream-combiner2@npm:1.1.1"
 | 
					  resolution: "stream-combiner2@npm:1.1.1"
 | 
				
			||||||
@@ -43179,6 +43427,15 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"twenty-chrome-extension@workspace:packages/twenty-chrome-extension":
 | 
				
			||||||
 | 
					  version: 0.0.0-use.local
 | 
				
			||||||
 | 
					  resolution: "twenty-chrome-extension@workspace:packages/twenty-chrome-extension"
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    "@crxjs/vite-plugin": "npm:^1.0.14"
 | 
				
			||||||
 | 
					    "@types/chrome": "npm:^0.0.256"
 | 
				
			||||||
 | 
					  languageName: unknown
 | 
				
			||||||
 | 
					  linkType: soft
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"twenty-docs@workspace:packages/twenty-docs":
 | 
					"twenty-docs@workspace:packages/twenty-docs":
 | 
				
			||||||
  version: 0.0.0-use.local
 | 
					  version: 0.0.0-use.local
 | 
				
			||||||
  resolution: "twenty-docs@workspace:packages/twenty-docs"
 | 
					  resolution: "twenty-docs@workspace:packages/twenty-docs"
 | 
				
			||||||
@@ -43893,6 +44150,13 @@ __metadata:
 | 
				
			|||||||
  languageName: node
 | 
					  languageName: node
 | 
				
			||||||
  linkType: hard
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"uberproto@npm:^1.1.0":
 | 
				
			||||||
 | 
					  version: 1.2.0
 | 
				
			||||||
 | 
					  resolution: "uberproto@npm:1.2.0"
 | 
				
			||||||
 | 
					  checksum: 0071dbc7b3b71b4fedd4de5c914a6851df8ce11f6d98cf84ef8a1973afd8562027d111db97c047e2e42894bd5f99b24c6d07058d338d3204b3aea2c3c75421d2
 | 
				
			||||||
 | 
					  languageName: node
 | 
				
			||||||
 | 
					  linkType: hard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"uc.micro@npm:^1.0.1, uc.micro@npm:^1.0.5":
 | 
					"uc.micro@npm:^1.0.1, uc.micro@npm:^1.0.5":
 | 
				
			||||||
  version: 1.0.6
 | 
					  version: 1.0.6
 | 
				
			||||||
  resolution: "uc.micro@npm:1.0.6"
 | 
					  resolution: "uc.micro@npm:1.0.6"
 | 
				
			||||||
 
 | 
				
			|||||||