diff --git a/.github/workflows/ci-chrome-extension.yaml b/.github/workflows/ci-chrome-extension.yaml
new file mode 100644
index 0000000000..d450cc4378
--- /dev/null
+++ b/.github/workflows/ci-chrome-extension.yaml
@@ -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
diff --git a/.vscode/twenty.code-workspace b/.vscode/twenty.code-workspace
index 8e4364504c..5abed2559c 100644
--- a/.vscode/twenty.code-workspace
+++ b/.vscode/twenty.code-workspace
@@ -4,6 +4,10 @@
"name": "ROOT",
"path": "../"
},
+ {
+ "name": "packages/twenty-chrome-extension",
+ "path": "../packages/twenty-chrome-extension"
+ },
{
"name": "packages/twenty-docker",
"path": "../packages/twenty-docker"
diff --git a/package.json b/package.json
index 66d5e176aa..3bfe6fea22 100644
--- a/package.json
+++ b/package.json
@@ -283,6 +283,7 @@
"version": "0.2.1",
"workspaces": {
"packages": [
+ "packages/twenty-chrome-extension",
"packages/twenty-front",
"packages/twenty-docs",
"packages/twenty-server",
diff --git a/packages/twenty-chrome-extension/.env.example b/packages/twenty-chrome-extension/.env.example
new file mode 100644
index 0000000000..4ab48f1d92
--- /dev/null
+++ b/packages/twenty-chrome-extension/.env.example
@@ -0,0 +1,2 @@
+VITE_SERVER_BASE_URL=http://localhost:3000
+VITE_FRONT_BASE_URL=http://localhost:3001
\ No newline at end of file
diff --git a/packages/twenty-chrome-extension/.eslintrc.cjs b/packages/twenty-chrome-extension/.eslintrc.cjs
new file mode 100644
index 0000000000..d6c9537953
--- /dev/null
+++ b/packages/twenty-chrome-extension/.eslintrc.cjs
@@ -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 },
+ ],
+ },
+}
diff --git a/packages/twenty-chrome-extension/.gitignore b/packages/twenty-chrome-extension/.gitignore
new file mode 100644
index 0000000000..a547bf36d8
--- /dev/null
+++ b/packages/twenty-chrome-extension/.gitignore
@@ -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?
diff --git a/packages/twenty-chrome-extension/README.md b/packages/twenty-chrome-extension/README.md
new file mode 100644
index 0000000000..fbe86fdfa9
--- /dev/null
+++ b/packages/twenty-chrome-extension/README.md
@@ -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.
+
+
+
+
+
+- STEP 5: Turn on the `Developer mode` from the top-right corner and click `Load unpacked`.
+
+
+
+
+
+- STEP 6: Select the `dist` folder from `twenty-chrome-extension`.
+
+
+
+
+
+- STEP 7: This opens up the `options` page, where you must enter your API key.
+
+
+
+
+
+- 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.
+
+
+
+
+
+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`.
diff --git a/packages/twenty-chrome-extension/options.html b/packages/twenty-chrome-extension/options.html
new file mode 100644
index 0000000000..f36fb44721
--- /dev/null
+++ b/packages/twenty-chrome-extension/options.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Twenty
+
+
+
+
+
+
diff --git a/packages/twenty-chrome-extension/package.json b/packages/twenty-chrome-extension/package.json
new file mode 100644
index 0000000000..153c255dae
--- /dev/null
+++ b/packages/twenty-chrome-extension/package.json
@@ -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"
+ }
+}
diff --git a/packages/twenty-chrome-extension/public/logo/32-32.png b/packages/twenty-chrome-extension/public/logo/32-32.png
new file mode 100644
index 0000000000..b373357081
Binary files /dev/null and b/packages/twenty-chrome-extension/public/logo/32-32.png differ
diff --git a/packages/twenty-chrome-extension/public/readme-images/01-img-one.png b/packages/twenty-chrome-extension/public/readme-images/01-img-one.png
new file mode 100644
index 0000000000..220d400ad5
Binary files /dev/null and b/packages/twenty-chrome-extension/public/readme-images/01-img-one.png differ
diff --git a/packages/twenty-chrome-extension/public/readme-images/02-img-two.png b/packages/twenty-chrome-extension/public/readme-images/02-img-two.png
new file mode 100644
index 0000000000..a80e7adc1e
Binary files /dev/null and b/packages/twenty-chrome-extension/public/readme-images/02-img-two.png differ
diff --git a/packages/twenty-chrome-extension/public/readme-images/03-img-three.png b/packages/twenty-chrome-extension/public/readme-images/03-img-three.png
new file mode 100644
index 0000000000..2bcc5ec06d
Binary files /dev/null and b/packages/twenty-chrome-extension/public/readme-images/03-img-three.png differ
diff --git a/packages/twenty-chrome-extension/public/readme-images/04-img-four.png b/packages/twenty-chrome-extension/public/readme-images/04-img-four.png
new file mode 100644
index 0000000000..eaea637fb9
Binary files /dev/null and b/packages/twenty-chrome-extension/public/readme-images/04-img-four.png differ
diff --git a/packages/twenty-chrome-extension/public/readme-images/05-img-five.png b/packages/twenty-chrome-extension/public/readme-images/05-img-five.png
new file mode 100644
index 0000000000..ab88203cde
Binary files /dev/null and b/packages/twenty-chrome-extension/public/readme-images/05-img-five.png differ
diff --git a/packages/twenty-chrome-extension/public/vite.svg b/packages/twenty-chrome-extension/public/vite.svg
new file mode 100644
index 0000000000..e7b8dfb1b2
--- /dev/null
+++ b/packages/twenty-chrome-extension/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/twenty-chrome-extension/src/background/index.ts b/packages/twenty-chrome-extension/src/background/index.ts
new file mode 100644
index 0000000000..8106da1637
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/background/index.ts
@@ -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 = 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.
+ }
+ }
+});
diff --git a/packages/twenty-chrome-extension/src/background/utils/openOptionsPage.ts b/packages/twenty-chrome-extension/src/background/utils/openOptionsPage.ts
new file mode 100644
index 0000000000..a86b5fd665
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/background/utils/openOptionsPage.ts
@@ -0,0 +1,5 @@
+const openOptionsPage = () => {
+ chrome.runtime.openOptionsPage();
+};
+
+export { openOptionsPage };
diff --git a/packages/twenty-chrome-extension/src/contentScript/createButton.ts b/packages/twenty-chrome-extension/src/contentScript/createButton.ts
new file mode 100644
index 0000000000..855ea0e9c9
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/contentScript/createButton.ts
@@ -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;
diff --git a/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts b/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts
new file mode 100644
index 0000000000..1717ae6cf5
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts
@@ -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;
diff --git a/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts b/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts
new file mode 100644
index 0000000000..367820dea6
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts
@@ -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;
diff --git a/packages/twenty-chrome-extension/src/contentScript/index.ts b/packages/twenty-chrome-extension/src/contentScript/index.ts
new file mode 100644
index 0000000000..10a3208afd
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/contentScript/index.ts
@@ -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!');
+});
diff --git a/packages/twenty-chrome-extension/src/contentScript/utils/extractCompanyLinkedinLink.ts b/packages/twenty-chrome-extension/src/contentScript/utils/extractCompanyLinkedinLink.ts
new file mode 100644
index 0000000000..28e4f2d3ff
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/contentScript/utils/extractCompanyLinkedinLink.ts
@@ -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;
diff --git a/packages/twenty-chrome-extension/src/contentScript/utils/extractDomain.ts b/packages/twenty-chrome-extension/src/contentScript/utils/extractDomain.ts
new file mode 100644
index 0000000000..3cb5f8d00f
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/contentScript/utils/extractDomain.ts
@@ -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;
diff --git a/packages/twenty-chrome-extension/src/contentScript/utils/extractFirstAndLastName.ts b/packages/twenty-chrome-extension/src/contentScript/utils/extractFirstAndLastName.ts
new file mode 100644
index 0000000000..5074609e91
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/contentScript/utils/extractFirstAndLastName.ts
@@ -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;
diff --git a/packages/twenty-chrome-extension/src/global.d.ts b/packages/twenty-chrome-extension/src/global.d.ts
new file mode 100644
index 0000000000..dbb4c627d2
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/global.d.ts
@@ -0,0 +1,3 @@
+///
+
+declare const __APP_VERSION__: string;
diff --git a/packages/twenty-chrome-extension/src/index.css b/packages/twenty-chrome-extension/src/index.css
new file mode 100644
index 0000000000..164e9067cc
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/index.css
@@ -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;
+}
diff --git a/packages/twenty-chrome-extension/src/manifest.ts b/packages/twenty-chrome-extension/src/manifest.ts
new file mode 100644
index 0000000000..0905ca494a
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/manifest.ts
@@ -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/*'],
+});
diff --git a/packages/twenty-chrome-extension/src/options/Options.tsx b/packages/twenty-chrome-extension/src/options/Options.tsx
new file mode 100644
index 0000000000..8b5c310d9f
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/Options.tsx
@@ -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 (
+
+
+
+ );
+};
+
+export default Options;
diff --git a/packages/twenty-chrome-extension/src/options/index.tsx b/packages/twenty-chrome-extension/src/options/index.tsx
new file mode 100644
index 0000000000..2c19c33ebd
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/index.tsx
@@ -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(
+
+
+
+
+ ,
+);
+
+declare module '@emotion/react' {
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
+ export interface Theme extends ThemeType {}
+}
diff --git a/packages/twenty-chrome-extension/src/options/modules/api-key/components/ApiKeyForm.tsx b/packages/twenty-chrome-extension/src/options/modules/api-key/components/ApiKeyForm.tsx
new file mode 100644
index 0000000000..66631cca47
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/api-key/components/ApiKeyForm.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showSection && (
+
+ )}
+
+
+ );
+};
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/display/typography/components/H2Title.tsx b/packages/twenty-chrome-extension/src/options/modules/ui/display/typography/components/H2Title.tsx
new file mode 100644
index 0000000000..2a3cfbf345
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/display/typography/components/H2Title.tsx
@@ -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) => (
+
+
+ {title}
+ {addornment}
+
+ {description && {description} }
+
+);
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/input/button/Button.tsx b/packages/twenty-chrome-extension/src/options/modules/ui/input/button/Button.tsx
new file mode 100644
index 0000000000..aa54bdf5c9
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/input/button/Button.tsx
@@ -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) => void;
+};
+
+const StyledButton = styled.button`
+ 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) => (
+
+ {Icon && Icon}
+ {title}
+ {soon && 'Soon'}
+
+);
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/input/components/TextInput.tsx b/packages/twenty-chrome-extension/src/options/modules/ui/input/components/TextInput.tsx
new file mode 100644
index 0000000000..1577fef72d
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/input/components/TextInput.tsx
@@ -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 = ({
+ label,
+ value,
+ onChange,
+ fullWidth,
+ error,
+ placeholder,
+ icon,
+}) => {
+ return (
+
+ {label && {label} }
+
+ {icon && {icon} }
+ onChange(e.target.value)}
+ placeholder={placeholder}
+ />
+
+ {error && {error} }
+
+ );
+};
+
+export { TextInput };
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/input/components/Toggle.tsx b/packages/twenty-chrome-extension/src/options/modules/ui/input/components/Toggle.tsx
new file mode 100644
index 0000000000..61345232ef
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/input/components/Toggle.tsx
@@ -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`
+ 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 (
+
+
+
+ );
+};
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/assets/dark-noise.jpg b/packages/twenty-chrome-extension/src/options/modules/ui/theme/assets/dark-noise.jpg
new file mode 100644
index 0000000000..6fc95715e9
Binary files /dev/null and b/packages/twenty-chrome-extension/src/options/modules/ui/theme/assets/dark-noise.jpg differ
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/assets/light-noise.png b/packages/twenty-chrome-extension/src/options/modules/ui/theme/assets/light-noise.png
new file mode 100644
index 0000000000..d7b3bc2c06
Binary files /dev/null and b/packages/twenty-chrome-extension/src/options/modules/ui/theme/assets/light-noise.png differ
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/components/AppThemeProvider.tsx b/packages/twenty-chrome-extension/src/options/modules/ui/theme/components/AppThemeProvider.tsx
new file mode 100644
index 0000000000..7beffa15fd
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/components/AppThemeProvider.tsx
@@ -0,0 +1,15 @@
+import { ThemeProvider } from '@emotion/react';
+
+import { lightTheme } from '../constants/theme';
+
+type AppThemeProviderProps = {
+ children: JSX.Element;
+};
+
+const AppThemeProvider: React.FC = ({ children }) => {
+ const theme = lightTheme;
+
+ return {children} ;
+};
+
+export { AppThemeProvider };
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/accent.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/accent.ts
new file mode 100644
index 0000000000..4272974766
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/accent.ts
@@ -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,
+};
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/animation.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/animation.ts
new file mode 100644
index 0000000000..04adcee2b0
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/animation.ts
@@ -0,0 +1,9 @@
+export const animation = {
+ duration: {
+ instant: 0.075,
+ fast: 0.15,
+ normal: 0.3,
+ },
+};
+
+export type AnimationDuration = 'instant' | 'fast' | 'normal';
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/background.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/background.ts
new file mode 100644
index 0000000000..5f54b5590d
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/background.ts
@@ -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%)`,
+};
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/blur.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/blur.ts
new file mode 100644
index 0000000000..9028343902
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/blur.ts
@@ -0,0 +1,4 @@
+export const blur = {
+ light: 'blur(6px)',
+ strong: 'blur(20px)',
+};
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/border.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/border.ts
new file mode 100644
index 0000000000..d98e2b2958
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/border.ts
@@ -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,
+};
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/boxShadow.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/boxShadow.ts
new file mode 100644
index 0000000000..f31f2608e8
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/boxShadow.ts
@@ -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)}`,
+};
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/colors.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/colors.ts
new file mode 100644
index 0000000000..1425b06f68
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/colors.ts
@@ -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})`;
+};
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/effects.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/effects.ts
new file mode 100644
index 0000000000..a14d0d09cd
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/effects.ts
@@ -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};
+ }
+ `;
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/font.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/font.ts
new file mode 100644
index 0000000000..031f6c786a
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/font.ts
@@ -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,
+};
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/icon.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/icon.ts
new file mode 100644
index 0000000000..a6654de668
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/icon.ts
@@ -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,
+ },
+};
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/modal.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/modal.ts
new file mode 100644
index 0000000000..30099fde89
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/modal.ts
@@ -0,0 +1,7 @@
+export const modal = {
+ size: {
+ sm: '300px',
+ md: '400px',
+ lg: '53%',
+ },
+};
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/tag.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/tag.ts
new file mode 100644
index 0000000000..b873d18b27
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/tag.ts
@@ -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,
+ },
+};
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/text.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/text.ts
new file mode 100644
index 0000000000..0f571de7c1
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/text.ts
@@ -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,
+};
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/theme.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/theme.ts
new file mode 100644
index 0000000000..4ed41a6c1c
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/theme.ts
@@ -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;
diff --git a/packages/twenty-chrome-extension/src/utils/handleQueryParams.ts b/packages/twenty-chrome-extension/src/utils/handleQueryParams.ts
new file mode 100644
index 0000000000..0157fe0a7d
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/utils/handleQueryParams.ts
@@ -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;
diff --git a/packages/twenty-chrome-extension/src/utils/requestDb.ts b/packages/twenty-chrome-extension/src/utils/requestDb.ts
new file mode 100644
index 0000000000..ecafcf8bcc
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/utils/requestDb.ts
@@ -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;
diff --git a/packages/twenty-chrome-extension/src/vite-env.d.ts b/packages/twenty-chrome-extension/src/vite-env.d.ts
new file mode 100644
index 0000000000..7bbbacc297
--- /dev/null
+++ b/packages/twenty-chrome-extension/src/vite-env.d.ts
@@ -0,0 +1,10 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_SERVER_BASE_URL: string;
+ readonly VITE_FRONT_BASE_URL: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/packages/twenty-chrome-extension/tsconfig.json b/packages/twenty-chrome-extension/tsconfig.json
new file mode 100644
index 0000000000..dbac1c34d9
--- /dev/null
+++ b/packages/twenty-chrome-extension/tsconfig.json
@@ -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" }]
+}
diff --git a/packages/twenty-chrome-extension/tsconfig.node.json b/packages/twenty-chrome-extension/tsconfig.node.json
new file mode 100644
index 0000000000..a9e8995327
--- /dev/null
+++ b/packages/twenty-chrome-extension/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts", "package.json", "src/*"]
+}
diff --git a/packages/twenty-chrome-extension/vite.config.ts b/packages/twenty-chrome-extension/vite.config.ts
new file mode 100644
index 0000000000..9096cd8363
--- /dev/null
+++ b/packages/twenty-chrome-extension/vite.config.ts
@@ -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()],
+ };
+});
diff --git a/yarn.lock b/yarn.lock
index b9de1da499..bd5231bf06 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1544,6 +1544,29 @@ __metadata:
languageName: node
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":
version: 7.23.6
resolution: "@babel/generator@npm:7.23.6"
@@ -1858,6 +1881,17 @@ __metadata:
languageName: node
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":
version: 7.23.4
resolution: "@babel/highlight@npm:7.23.4"
@@ -1887,6 +1921,15 @@ __metadata:
languageName: node
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":
version: 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
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
resolution: "@babel/plugin-transform-react-jsx-self@npm:7.23.3"
dependencies:
@@ -2780,7 +2823,7 @@ __metadata:
languageName: node
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
resolution: "@babel/plugin-transform-react-jsx-source@npm:7.23.3"
dependencies:
@@ -3281,6 +3324,17 @@ __metadata:
languageName: node
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":
version: 7.23.5
resolution: "@babel/traverse@npm:7.23.5"
@@ -3335,6 +3389,24 @@ __metadata:
languageName: node
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":
version: 7.23.6
resolution: "@babel/types@npm:7.23.6"
@@ -3357,6 +3429,17 @@ __metadata:
languageName: node
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":
version: 1.0.1
resolution: "@base2/pretty-print-object@npm:1.0.1"
@@ -3873,6 +3956,34 @@ __metadata:
languageName: node
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":
version: 0.8.1
resolution: "@cspotcode/source-map-support@npm:0.8.1"
@@ -10786,6 +10897,16 @@ __metadata:
languageName: node
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":
version: 5.1.0
resolution: "@rollup/pluginutils@npm:5.1.0"
@@ -14530,7 +14651,7 @@ __metadata:
languageName: node
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
resolution: "@types/babel__core@npm:7.20.5"
dependencies:
@@ -14644,6 +14765,16 @@ __metadata:
languageName: node
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":
version: 0.0.90
resolution: "@types/codemirror@npm:0.0.90"
@@ -14959,6 +15090,22 @@ __metadata:
languageName: node
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":
version: 3.2.1
resolution: "@types/find-cache-dir@npm:3.2.1"
@@ -15013,7 +15160,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/har-format@npm:^1.2.10":
+"@types/har-format@npm:*, @types/har-format@npm:^1.2.10":
version: 1.2.15
resolution: "@types/har-format@npm:1.2.15"
checksum: 43ede55c3947e5cf59561f165930dc2eb3ae983fd162980cdc7274be1e7f528a6f77e65fee9a02a20d91b09bde10bed832b0470724f5c744ef6669015d00564e
@@ -16361,6 +16508,21 @@ __metadata:
languageName: node
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":
version: 3.1.0
resolution: "@vitejs/plugin-react@npm:3.1.0"
@@ -16558,6 +16720,13 @@ __metadata:
languageName: node
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":
version: 0.0.3
resolution: "@whatwg-node/events@npm:0.0.3"
@@ -16854,7 +17023,7 @@ __metadata:
languageName: node
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
resolution: "acorn-walk@npm:8.3.1"
checksum: a23d2f7c6b6cad617f4c77f14dfeb062a239208d61753e9ba808d916c550add92b39535467d2e6028280761ac4f5a904cc9df21530b84d3f834e3edef74ddde5
@@ -20273,7 +20442,7 @@ __metadata:
languageName: node
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
resolution: "cheerio@npm:1.0.0-rc.12"
dependencies:
@@ -21230,6 +21399,18 @@ __metadata:
languageName: node
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":
version: 2.15.3
resolution: "consola@npm:2.15.3"
@@ -22314,7 +22495,7 @@ __metadata:
languageName: node
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
resolution: "debug@npm:2.6.9"
dependencies:
@@ -23497,6 +23678,13 @@ __metadata:
languageName: node
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":
version: 0.9.3
resolution: "es-module-lexer@npm:0.9.3"
@@ -24339,7 +24527,7 @@ __metadata:
languageName: node
linkType: hard
-"estree-walker@npm:^2.0.2":
+"estree-walker@npm:^2.0.1, estree-walker@npm:^2.0.2":
version: 2.0.2
resolution: "estree-walker@npm:2.0.2"
checksum: 53a6c54e2019b8c914dc395890153ffdc2322781acf4bd7d1a32d7aedc1710807bdcd866ac133903d5629ec601fbb50abe8c2e5553c7f5a0afdd9b6af6c945af
@@ -25549,7 +25737,7 @@ __metadata:
languageName: node
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
resolution: "fs-extra@npm:10.1.0"
dependencies:
@@ -30290,6 +30478,15 @@ __metadata:
languageName: node
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":
version: 0.5.0
resolution: "jsesc@npm:0.5.0"
@@ -31538,6 +31735,15 @@ __metadata:
languageName: node
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":
version: 0.27.0
resolution: "magic-string@npm:0.27.0"
@@ -36647,7 +36853,7 @@ __metadata:
languageName: node
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
resolution: "picomatch@npm:2.3.1"
checksum: 26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be
@@ -38017,6 +38223,13 @@ __metadata:
languageName: node
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":
version: 6.11.0
resolution: "qs@npm:6.11.0"
@@ -38622,6 +38835,13 @@ __metadata:
languageName: node
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":
version: 0.14.0
resolution: "react-refresh@npm:0.14.0"
@@ -40100,6 +40320,20 @@ __metadata:
languageName: node
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":
version: 4.9.2
resolution: "rollup@npm:4.9.2"
@@ -41177,6 +41411,13 @@ __metadata:
languageName: node
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":
version: 1.1.5
resolution: "space-separated-tokens@npm:1.1.5"
@@ -41541,6 +41782,13 @@ __metadata:
languageName: node
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":
version: 1.1.1
resolution: "stream-combiner2@npm:1.1.1"
@@ -43179,6 +43427,15 @@ __metadata:
languageName: node
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":
version: 0.0.0-use.local
resolution: "twenty-docs@workspace:packages/twenty-docs"
@@ -43893,6 +44150,13 @@ __metadata:
languageName: node
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":
version: 1.0.6
resolution: "uc.micro@npm:1.0.6"