mirror of
https://github.com/lingble/twenty.git
synced 2025-10-30 20:27:55 +00:00
feat: replace iframe with chrome sidepanel (#5197)
fixes - #5201 https://github.com/twentyhq/twenty/assets/13139771/871019c6-6456-46b4-95dd-07ffb33eb4fd --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@@ -7,6 +7,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/options/index.tsx"></script>
|
<script type="module" src="/src/options/page-inaccessible-index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
22
packages/twenty-chrome-extension/sidepanel.html
Normal file
22
packages/twenty-chrome-extension/sidepanel.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<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>
|
||||||
|
<style>
|
||||||
|
/* Reset margin and padding */
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%; /* Ensure body takes full viewport height */
|
||||||
|
overflow: hidden; /* Prevents scrollbars from appearing */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/options/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,40 +1,55 @@
|
|||||||
import Crypto from 'crypto-js';
|
import Crypto from 'crypto-js';
|
||||||
|
|
||||||
import { openOptionsPage } from '~/background/utils/openOptionsPage';
|
|
||||||
import { exchangeAuthorizationCode } from '~/db/auth.db';
|
import { exchangeAuthorizationCode } from '~/db/auth.db';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
// Open options page programmatically in a new tab.
|
// Open options page programmatically in a new tab.
|
||||||
chrome.runtime.onInstalled.addListener((details) => {
|
// chrome.runtime.onInstalled.addListener((details) => {
|
||||||
if (details.reason === 'install') {
|
// if (details.reason === 'install') {
|
||||||
openOptionsPage();
|
// openOptionsPage();
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Open options page when extension icon is clicked.
|
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
|
||||||
chrome.action.onClicked.addListener((tab) => {
|
|
||||||
chrome.tabs.sendMessage(tab.id ?? 0, { action: 'TOGGLE' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// This listens for an event from other parts of the extension, such as the content script, and performs the required tasks.
|
// 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.
|
// The cases themselves are labelled such that their operations are reflected by their names.
|
||||||
chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
|
||||||
switch (message.action) {
|
switch (message.action) {
|
||||||
case 'getActiveTab': // e.g. "https://linkedin.com/company/twenty/"
|
case 'getActiveTab': {
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
// e.g. "https://linkedin.com/company/twenty/"
|
||||||
if (isDefined(tabs) && isDefined(tabs[0])) {
|
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
|
||||||
sendResponse({ tab: tabs[0] });
|
if (isDefined(tab) && isDefined(tab.id)) {
|
||||||
|
sendResponse({ tab });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'openOptionsPage':
|
}
|
||||||
openOptionsPage();
|
case 'launchOAuth': {
|
||||||
break;
|
|
||||||
case 'CONNECT':
|
|
||||||
launchOAuth(({ status, message }) => {
|
launchOAuth(({ status, message }) => {
|
||||||
sendResponse({ status, message });
|
sendResponse({ status, message });
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
case 'openSidepanel': {
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
|
||||||
|
if (isDefined(tab) && isDefined(tab.id)) {
|
||||||
|
chrome.sidePanel.open({ tabId: tab.id });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'changeSidepanelUrl': {
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
|
||||||
|
if (isDefined(tab) && isDefined(tab.id)) {
|
||||||
|
chrome.tabs.sendMessage(tab.id, {
|
||||||
|
action: 'changeSidepanelUrl',
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -101,13 +116,16 @@ const launchOAuth = (
|
|||||||
|
|
||||||
callback({ status: true, message: '' });
|
callback({ status: true, message: '' });
|
||||||
|
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
chrome.tabs.query(
|
||||||
if (isDefined(tabs) && isDefined(tabs[0])) {
|
{ active: true, currentWindow: true },
|
||||||
chrome.tabs.sendMessage(tabs[0].id ?? 0, {
|
([tab]) => {
|
||||||
action: 'AUTHENTICATED',
|
if (isDefined(tab) && isDefined(tab.id)) {
|
||||||
});
|
chrome.tabs.sendMessage(tab.id, {
|
||||||
}
|
action: 'executeContentScript',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -117,14 +135,22 @@ const launchOAuth = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
chrome.tabs.onUpdated.addListener(async (tabId, _, tab) => {
|
||||||
const isDesiredRoute =
|
const isDesiredRoute =
|
||||||
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) ||
|
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) ||
|
||||||
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/);
|
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/);
|
||||||
|
|
||||||
if (changeInfo.status === 'complete' && tab.active) {
|
if (tab.active === true) {
|
||||||
if (isDefined(isDesiredRoute)) {
|
if (isDefined(isDesiredRoute)) {
|
||||||
chrome.tabs.sendMessage(tabId, { action: 'executeContentScript' });
|
chrome.tabs.sendMessage(tabId, { action: 'executeContentScript' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await chrome.sidePanel.setOptions({
|
||||||
|
tabId,
|
||||||
|
path: tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com/)
|
||||||
|
? 'sidepanel.html'
|
||||||
|
: 'page-inaccessible.html',
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
const openOptionsPage = () => {
|
|
||||||
chrome.runtime.openOptionsPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
export { openOptionsPage };
|
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
interface CustomDiv extends HTMLDivElement {
|
||||||
|
onClickHandler: (newHandler: () => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export const createDefaultButton = (
|
export const createDefaultButton = (
|
||||||
buttonId: string,
|
buttonId: string,
|
||||||
onClickHandler?: () => void,
|
|
||||||
buttonText = '',
|
buttonText = '',
|
||||||
) => {
|
): CustomDiv => {
|
||||||
const btn = document.getElementById(buttonId);
|
const btn = document.getElementById(buttonId) as CustomDiv;
|
||||||
if (isDefined(btn)) return btn;
|
if (isDefined(btn)) return btn;
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div') as CustomDiv;
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
|
|
||||||
@@ -52,19 +55,18 @@ export const createDefaultButton = (
|
|||||||
Object.assign(div.style, divStyles);
|
Object.assign(div.style, divStyles);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle the click event.
|
div.onClickHandler = (newHandler) => {
|
||||||
div.addEventListener('click', async (e) => {
|
div.onclick = async () => {
|
||||||
e.preventDefault();
|
const store = await chrome.storage.local.get();
|
||||||
const store = await chrome.storage.local.get();
|
|
||||||
|
|
||||||
// If an api key is not set, the options page opens up to allow the user to configure an api key.
|
// If an api key is not set, the options page opens up to allow the user to configure an api key.
|
||||||
if (!store.accessToken) {
|
if (!store.accessToken) {
|
||||||
chrome.runtime.sendMessage({ action: 'openOptionsPage' });
|
chrome.runtime.sendMessage({ action: 'openSidepanel' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
newHandler();
|
||||||
onClickHandler?.();
|
};
|
||||||
});
|
};
|
||||||
|
|
||||||
div.id = buttonId;
|
div.id = buttonId;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createDefaultButton } from '~/contentScript/createButton';
|
import { createDefaultButton } from '~/contentScript/createButton';
|
||||||
|
import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl';
|
||||||
import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLinkedinLink';
|
import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLinkedinLink';
|
||||||
import extractDomain from '~/contentScript/utils/extractDomain';
|
import extractDomain from '~/contentScript/utils/extractDomain';
|
||||||
import { createCompany, fetchCompany } from '~/db/company.db';
|
import { createCompany, fetchCompany } from '~/db/company.db';
|
||||||
@@ -71,27 +72,19 @@ export const addCompany = async () => {
|
|||||||
const companyURL = extractCompanyLinkedinLink(activeTab.url);
|
const companyURL = extractCompanyLinkedinLink(activeTab.url);
|
||||||
companyInputData.linkedinLink = { url: companyURL, label: companyURL };
|
companyInputData.linkedinLink = { url: companyURL, label: companyURL };
|
||||||
|
|
||||||
const company = await createCompany(companyInputData);
|
const companyId = await createCompany(companyInputData);
|
||||||
return company;
|
|
||||||
|
if (isDefined(companyId)) {
|
||||||
|
await changeSidePanelUrl(
|
||||||
|
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return companyId;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const insertButtonForCompany = async () => {
|
export const insertButtonForCompany = async () => {
|
||||||
const companyButtonDiv = createDefaultButton(
|
const companyButtonDiv = createDefaultButton('twenty-company-btn');
|
||||||
'twenty-company-btn',
|
|
||||||
async () => {
|
|
||||||
if (isDefined(companyButtonDiv)) {
|
|
||||||
const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0];
|
|
||||||
companyBtnSpan.textContent = 'Saving...';
|
|
||||||
const company = await addCompany();
|
|
||||||
if (isDefined(company)) {
|
|
||||||
companyBtnSpan.textContent = 'Saved';
|
|
||||||
Object.assign(companyButtonDiv.style, { pointerEvents: 'none' });
|
|
||||||
} else {
|
|
||||||
companyBtnSpan.textContent = 'Try again';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const parentDiv: HTMLDivElement | null = document.querySelector(
|
const parentDiv: HTMLDivElement | null = document.querySelector(
|
||||||
'.org-top-card-primary-actions__inner',
|
'.org-top-card-primary-actions__inner',
|
||||||
@@ -105,13 +98,35 @@ export const insertButtonForCompany = async () => {
|
|||||||
parentDiv.prepend(companyButtonDiv);
|
parentDiv.prepend(companyButtonDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0];
|
const companyButtonSpan = companyButtonDiv.getElementsByTagName('span')[0];
|
||||||
const company = await checkIfCompanyExists();
|
const company = await checkIfCompanyExists();
|
||||||
|
|
||||||
|
const openCompanyOnSidePanel = (companyId: string) => {
|
||||||
|
companyButtonSpan.textContent = 'View in Twenty';
|
||||||
|
companyButtonDiv.onClickHandler(async () => {
|
||||||
|
await changeSidePanelUrl(
|
||||||
|
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`,
|
||||||
|
);
|
||||||
|
chrome.runtime.sendMessage({ action: 'openSidepanel' });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (isDefined(company)) {
|
if (isDefined(company)) {
|
||||||
companyBtnSpan.textContent = 'Saved';
|
await changeSidePanelUrl(
|
||||||
Object.assign(companyButtonDiv.style, { pointerEvents: 'none' });
|
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${company.id}`,
|
||||||
|
);
|
||||||
|
if (isDefined(company.id)) openCompanyOnSidePanel(company.id);
|
||||||
} else {
|
} else {
|
||||||
companyBtnSpan.textContent = 'Add to Twenty';
|
companyButtonSpan.textContent = 'Add to Twenty';
|
||||||
|
|
||||||
|
companyButtonDiv.onClickHandler(async () => {
|
||||||
|
companyButtonSpan.textContent = 'Saving...';
|
||||||
|
const companyId = await addCompany();
|
||||||
|
if (isDefined(companyId)) {
|
||||||
|
openCompanyOnSidePanel(companyId);
|
||||||
|
} else {
|
||||||
|
companyButtonSpan.textContent = 'Try again';
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createDefaultButton } from '~/contentScript/createButton';
|
import { createDefaultButton } from '~/contentScript/createButton';
|
||||||
|
import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl';
|
||||||
import extractFirstAndLastName from '~/contentScript/utils/extractFirstAndLastName';
|
import extractFirstAndLastName from '~/contentScript/utils/extractFirstAndLastName';
|
||||||
import { createPerson, fetchPerson } from '~/db/person.db';
|
import { createPerson, fetchPerson } from '~/db/person.db';
|
||||||
import { PersonInput } from '~/db/types/person.types';
|
import { PersonInput } from '~/db/types/person.types';
|
||||||
@@ -82,44 +83,58 @@ export const addPerson = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl };
|
personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl };
|
||||||
const person = await createPerson(personData);
|
const personId = await createPerson(personData);
|
||||||
return person;
|
|
||||||
|
if (isDefined(personId)) {
|
||||||
|
await changeSidePanelUrl(
|
||||||
|
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return personId;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const insertButtonForPerson = async () => {
|
export const insertButtonForPerson = async () => {
|
||||||
const personButtonDiv = createDefaultButton('twenty-person-btn', async () => {
|
const personButtonDiv = createDefaultButton('twenty-person-btn');
|
||||||
if (isDefined(personButtonDiv)) {
|
|
||||||
const personBtnSpan = personButtonDiv.getElementsByTagName('span')[0];
|
|
||||||
personBtnSpan.textContent = 'Saving...';
|
|
||||||
const person = await addPerson();
|
|
||||||
if (isDefined(person)) {
|
|
||||||
personBtnSpan.textContent = 'Saved';
|
|
||||||
Object.assign(personButtonDiv.style, { pointerEvents: 'none' });
|
|
||||||
} else {
|
|
||||||
personBtnSpan.textContent = 'Try again';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isDefined(personButtonDiv)) {
|
if (isDefined(personButtonDiv)) {
|
||||||
const parentDiv: HTMLDivElement | null = document.querySelector(
|
const addedProfileDiv: HTMLDivElement | null = document.querySelector(
|
||||||
'.pv-top-card-v2-ctas',
|
'.pv-top-card-v2-ctas__custom',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isDefined(parentDiv)) {
|
if (isDefined(addedProfileDiv)) {
|
||||||
Object.assign(personButtonDiv.style, {
|
Object.assign(personButtonDiv.style, {
|
||||||
marginRight: '.8rem',
|
marginRight: '.8rem',
|
||||||
});
|
});
|
||||||
parentDiv.prepend(personButtonDiv);
|
addedProfileDiv.prepend(personButtonDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
const personBtnSpan = personButtonDiv.getElementsByTagName('span')[0];
|
const personButtonSpan = personButtonDiv.getElementsByTagName('span')[0];
|
||||||
const person = await checkIfPersonExists();
|
const person = await checkIfPersonExists();
|
||||||
|
|
||||||
|
const openPersonOnSidePanel = (personId: string) => {
|
||||||
|
personButtonSpan.textContent = 'View in Twenty';
|
||||||
|
personButtonDiv.onClickHandler(async () => {
|
||||||
|
await changeSidePanelUrl(
|
||||||
|
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`,
|
||||||
|
);
|
||||||
|
chrome.runtime.sendMessage({ action: 'openSidepanel' });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (isDefined(person)) {
|
if (isDefined(person)) {
|
||||||
personBtnSpan.textContent = 'Saved';
|
await changeSidePanelUrl(
|
||||||
Object.assign(personButtonDiv.style, { pointerEvents: 'none' });
|
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${person.id}`,
|
||||||
|
);
|
||||||
|
if (isDefined(person.id)) openPersonOnSidePanel(person.id);
|
||||||
} else {
|
} else {
|
||||||
personBtnSpan.textContent = 'Add to Twenty';
|
personButtonSpan.textContent = 'Add to Twenty';
|
||||||
|
personButtonDiv.onClickHandler(async () => {
|
||||||
|
personButtonSpan.textContent = 'Saving...';
|
||||||
|
const personId = await addPerson();
|
||||||
|
if (isDefined(personId)) openPersonOnSidePanel(personId);
|
||||||
|
else personButtonSpan.textContent = 'Try again';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile';
|
import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile';
|
||||||
import { insertButtonForPerson } from '~/contentScript/extractPersonProfile';
|
import { insertButtonForPerson } from '~/contentScript/extractPersonProfile';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
// Inject buttons into the DOM when SPA is reloaded on the resource url.
|
// 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/
|
// e.g. reload the page when on https://www.linkedin.com/in/mabdullahabaid/
|
||||||
@@ -20,85 +19,5 @@ chrome.runtime.onMessage.addListener(async (message, _, sendResponse) => {
|
|||||||
await insertButtonForPerson();
|
await insertButtonForPerson();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.action === 'TOGGLE') {
|
|
||||||
await toggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.action === 'AUTHENTICATED') {
|
|
||||||
await authenticated();
|
|
||||||
}
|
|
||||||
|
|
||||||
sendResponse('Executing!');
|
sendResponse('Executing!');
|
||||||
});
|
});
|
||||||
|
|
||||||
const IFRAME_WIDTH = '400px';
|
|
||||||
|
|
||||||
const createIframe = () => {
|
|
||||||
const iframe = document.createElement('iframe');
|
|
||||||
iframe.style.background = 'lightgrey';
|
|
||||||
iframe.style.height = '100vh';
|
|
||||||
iframe.style.width = IFRAME_WIDTH;
|
|
||||||
iframe.style.position = 'fixed';
|
|
||||||
iframe.style.top = '0px';
|
|
||||||
iframe.style.right = `-${IFRAME_WIDTH}`;
|
|
||||||
iframe.style.zIndex = '9000000000000000000';
|
|
||||||
iframe.style.transition = 'ease-in-out 0.3s';
|
|
||||||
return iframe;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContentIframeLoadComplete = () => {
|
|
||||||
//If the pop-out window is already open then we replace loading iframe with our content iframe
|
|
||||||
if (optionsIframe.style.right === '0px') contentIframe.style.right = '0px';
|
|
||||||
optionsIframe.style.display = 'none';
|
|
||||||
contentIframe.style.display = 'block';
|
|
||||||
};
|
|
||||||
|
|
||||||
//Creating one iframe where we are loading our front end in the background
|
|
||||||
const contentIframe = createIframe();
|
|
||||||
contentIframe.style.display = 'none';
|
|
||||||
|
|
||||||
chrome.storage.local.get().then((store) => {
|
|
||||||
if (isDefined(store.loginToken)) {
|
|
||||||
contentIframe.src = `${import.meta.env.VITE_FRONT_BASE_URL}`;
|
|
||||||
contentIframe.onload = handleContentIframeLoadComplete;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const optionsIframe = createIframe();
|
|
||||||
optionsIframe.src = chrome.runtime.getURL('options.html');
|
|
||||||
|
|
||||||
document.body.appendChild(contentIframe);
|
|
||||||
document.body.appendChild(optionsIframe);
|
|
||||||
|
|
||||||
const toggleIframe = (iframe: HTMLIFrameElement) => {
|
|
||||||
if (
|
|
||||||
iframe.style.right === `-${IFRAME_WIDTH}` &&
|
|
||||||
iframe.style.display !== 'none'
|
|
||||||
) {
|
|
||||||
iframe.style.right = '0px';
|
|
||||||
} else if (iframe.style.right === '0px' && iframe.style.display !== 'none') {
|
|
||||||
iframe.style.right = `-${IFRAME_WIDTH}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggle = async () => {
|
|
||||||
const store = await chrome.storage.local.get();
|
|
||||||
if (isDefined(store.accessToken)) {
|
|
||||||
toggleIframe(contentIframe);
|
|
||||||
} else {
|
|
||||||
toggleIframe(optionsIframe);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const authenticated = async () => {
|
|
||||||
const store = await chrome.storage.local.get();
|
|
||||||
if (isDefined(store.loginToken)) {
|
|
||||||
contentIframe.src = `${
|
|
||||||
import.meta.env.VITE_FRONT_BASE_URL
|
|
||||||
}/verify?loginToken=${store.loginToken.token}`;
|
|
||||||
contentIframe.onload = handleContentIframeLoadComplete;
|
|
||||||
toggleIframe(contentIframe);
|
|
||||||
} else {
|
|
||||||
toggleIframe(optionsIframe);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
const changeSidePanelUrl = async (url: string) => {
|
||||||
|
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
||||||
|
action: 'getActiveTab',
|
||||||
|
});
|
||||||
|
if (isDefined(activeTab) && isDefined(url)) {
|
||||||
|
chrome.storage.local.set({ [`sidepanelUrl_${activeTab.id}`]: url });
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'changeSidepanelUrl',
|
||||||
|
message: { url },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default changeSidePanelUrl;
|
||||||
@@ -5,6 +5,7 @@ export const FIND_COMPANY = gql`
|
|||||||
companies(filter: $filter) {
|
companies(filter: $filter) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
|
id
|
||||||
name
|
name
|
||||||
linkedinLink {
|
linkedinLink {
|
||||||
url
|
url
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const FIND_PERSON = gql`
|
|||||||
people(filter: $filter) {
|
people(filter: $filter) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
|
id
|
||||||
name {
|
name {
|
||||||
firstName
|
firstName
|
||||||
lastName
|
lastName
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default defineManifest({
|
|||||||
action: {},
|
action: {},
|
||||||
|
|
||||||
//TODO: change this to a documenation page
|
//TODO: change this to a documenation page
|
||||||
options_page: 'options.html',
|
options_page: 'sidepanel.html',
|
||||||
|
|
||||||
background: {
|
background: {
|
||||||
service_worker: 'src/background/index.ts',
|
service_worker: 'src/background/index.ts',
|
||||||
@@ -43,12 +43,12 @@ export default defineManifest({
|
|||||||
|
|
||||||
web_accessible_resources: [
|
web_accessible_resources: [
|
||||||
{
|
{
|
||||||
resources: ['options.html'],
|
resources: ['sidepanel.html', 'page-inaccessible.html'],
|
||||||
matches: ['https://www.linkedin.com/*'],
|
matches: ['https://www.linkedin.com/*'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
permissions: ['activeTab', 'storage', 'identity'],
|
permissions: ['activeTab', 'storage', 'identity', 'sidePanel'],
|
||||||
|
|
||||||
host_permissions: host_permissions,
|
host_permissions: host_permissions,
|
||||||
|
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { Loader } from '@/ui/display/loader/components/Loader';
|
|
||||||
import { MainButton } from '@/ui/input/button/MainButton';
|
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
|
||||||
|
|
||||||
const StyledWrapper = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
background: ${({ theme }) => theme.background.noisy};
|
|
||||||
display: flex;
|
|
||||||
height: 100vh;
|
|
||||||
justify-content: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
|
||||||
background: ${({ theme }) => theme.background.primary};
|
|
||||||
width: 400px;
|
|
||||||
height: 350px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledActionContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
justify-content: center;
|
|
||||||
width: 300px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledLabel = styled.span`
|
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
|
||||||
font-size: ${({ theme }) => theme.font.size.md};
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
|
||||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
|
||||||
text-transform: uppercase;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Options = () => {
|
|
||||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
const [serverBaseUrl, setServerBaseUrl] = useState(
|
|
||||||
import.meta.env.VITE_SERVER_BASE_URL,
|
|
||||||
);
|
|
||||||
const authenticate = () => {
|
|
||||||
setIsAuthenticating(true);
|
|
||||||
setError('');
|
|
||||||
chrome.runtime.sendMessage({ action: 'CONNECT' }, ({ status, message }) => {
|
|
||||||
if (status === true) {
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
setIsAuthenticating(false);
|
|
||||||
chrome.storage.local.set({ isAuthenticated: true });
|
|
||||||
} else {
|
|
||||||
setError(message);
|
|
||||||
setIsAuthenticating(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getState = async () => {
|
|
||||||
const store = await chrome.storage.local.get();
|
|
||||||
if (store.serverBaseUrl !== '') {
|
|
||||||
setServerBaseUrl(store.serverBaseUrl);
|
|
||||||
} else {
|
|
||||||
setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (store.isAuthenticated === true) setIsAuthenticated(true);
|
|
||||||
};
|
|
||||||
void getState();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleBaseUrlChange = (value: string) => {
|
|
||||||
setServerBaseUrl(value);
|
|
||||||
setError('');
|
|
||||||
chrome.storage.local.set({ serverBaseUrl: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledWrapper>
|
|
||||||
<StyledContainer>
|
|
||||||
<img src="/logo/32-32.svg" alt="twenty-logo" height={64} width={64} />
|
|
||||||
<StyledActionContainer>
|
|
||||||
<TextInput
|
|
||||||
label="Server URL"
|
|
||||||
value={serverBaseUrl}
|
|
||||||
onChange={handleBaseUrlChange}
|
|
||||||
placeholder="My base server URL"
|
|
||||||
error={error}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
{isAuthenticating ? (
|
|
||||||
<Loader />
|
|
||||||
) : isAuthenticated ? (
|
|
||||||
<StyledLabel>Connected!</StyledLabel>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<MainButton
|
|
||||||
title="Connect your account"
|
|
||||||
onClick={() => authenticate()}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<MainButton
|
|
||||||
title="Sign up"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => window.open(`${serverBaseUrl}`, '_blank')}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</StyledActionContainer>
|
|
||||||
</StyledContainer>
|
|
||||||
</StyledWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Options;
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { MainButton } from '@/ui/input/button/MainButton';
|
||||||
|
|
||||||
|
const StyledWrapper = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
background: ${({ theme }) => theme.background.primary};
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
width: 400px;
|
||||||
|
height: 350px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: ${({ theme }) => theme.spacing(8)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTextContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLargeText = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.lg};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledMediumText = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const PageInaccessible = () => {
|
||||||
|
return (
|
||||||
|
<StyledWrapper>
|
||||||
|
<StyledContainer>
|
||||||
|
<img src="/logo/32-32.svg" alt="twenty-logo" height={40} width={40} />
|
||||||
|
<StyledTextContainer>
|
||||||
|
<StyledLargeText>
|
||||||
|
Extension not available on the website
|
||||||
|
</StyledLargeText>
|
||||||
|
<StyledMediumText>
|
||||||
|
Open LinkedIn to use the extension
|
||||||
|
</StyledMediumText>
|
||||||
|
</StyledTextContainer>
|
||||||
|
<MainButton
|
||||||
|
title="Go to LinkedIn"
|
||||||
|
onClick={() => window.open('https://www.linkedin.com/')}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageInaccessible;
|
||||||
169
packages/twenty-chrome-extension/src/options/Sidepanel.tsx
Normal file
169
packages/twenty-chrome-extension/src/options/Sidepanel.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { Loader } from '@/ui/display/loader/components/Loader';
|
||||||
|
import { MainButton } from '@/ui/input/button/MainButton';
|
||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
const StyledIframe = styled.iframe`
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
border: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledWrapper = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
background: ${({ theme }) => theme.background.primary};
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
width: 400px;
|
||||||
|
height: 350px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: ${({ theme }) => theme.spacing(8)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledActionContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 300px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Sidepanel = () => {
|
||||||
|
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
const [iframeSrc, setIframeSrc] = useState(
|
||||||
|
import.meta.env.VITE_FRONT_BASE_URL,
|
||||||
|
);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [serverBaseUrl, setServerBaseUrl] = useState('');
|
||||||
|
const authenticate = () => {
|
||||||
|
setIsAuthenticating(true);
|
||||||
|
setError('');
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{ action: 'launchOAuth' },
|
||||||
|
({ status, message }) => {
|
||||||
|
if (status === true) {
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
setIsAuthenticating(false);
|
||||||
|
chrome.storage.local.set({ isAuthenticated: true });
|
||||||
|
} else {
|
||||||
|
setError(message);
|
||||||
|
setIsAuthenticating(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getState = async () => {
|
||||||
|
const store = await chrome.storage.local.get();
|
||||||
|
if (isDefined(store.serverBaseUrl)) {
|
||||||
|
setServerBaseUrl(store.serverBaseUrl);
|
||||||
|
} else {
|
||||||
|
setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.isAuthenticated === true) setIsAuthenticated(true);
|
||||||
|
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
||||||
|
action: 'getActiveTab',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
isDefined(activeTab) &&
|
||||||
|
isDefined(store[`sidepanelUrl_${activeTab.id}`])
|
||||||
|
) {
|
||||||
|
const url = store[`sidepanelUrl_${activeTab.id}`];
|
||||||
|
setIframeSrc(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void getState();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleBrowserEvents = ({ action }: { action: string }) => {
|
||||||
|
if (action === 'changeSidepanelUrl') {
|
||||||
|
setIframeSrc('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
chrome.runtime.onMessage.addListener(handleBrowserEvents);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
chrome.runtime.onMessage.removeListener(handleBrowserEvents);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getIframeState = async () => {
|
||||||
|
const store = await chrome.storage.local.get();
|
||||||
|
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
||||||
|
action: 'getActiveTab',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
isDefined(activeTab) &&
|
||||||
|
isDefined(store[`sidepanelUrl_${activeTab.id}`])
|
||||||
|
) {
|
||||||
|
const url = store[`sidepanelUrl_${activeTab.id}`];
|
||||||
|
setIframeSrc(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void getIframeState();
|
||||||
|
}, [iframeSrc]);
|
||||||
|
|
||||||
|
const handleBaseUrlChange = (value: string) => {
|
||||||
|
setServerBaseUrl(value);
|
||||||
|
setError('');
|
||||||
|
chrome.storage.local.set({ serverBaseUrl: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return isAuthenticated ? (
|
||||||
|
<StyledIframe title="twenty-website" src={iframeSrc}></StyledIframe>
|
||||||
|
) : (
|
||||||
|
<StyledWrapper>
|
||||||
|
<StyledContainer>
|
||||||
|
<img src="/logo/32-32.svg" alt="twenty-logo" height={40} width={40} />
|
||||||
|
{isAuthenticating ? (
|
||||||
|
<Loader />
|
||||||
|
) : (
|
||||||
|
<StyledActionContainer>
|
||||||
|
<TextInput
|
||||||
|
label="Server URL"
|
||||||
|
value={serverBaseUrl}
|
||||||
|
onChange={handleBaseUrlChange}
|
||||||
|
placeholder="My base server URL"
|
||||||
|
error={error}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<MainButton
|
||||||
|
title="Connect your account"
|
||||||
|
onClick={() => authenticate()}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<MainButton
|
||||||
|
title="Sign up"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
window.open(`${import.meta.env.VITE_FRONT_BASE_URL}`, '_blank')
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</StyledActionContainer>
|
||||||
|
)}
|
||||||
|
</StyledContainer>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidepanel;
|
||||||
@@ -3,14 +3,14 @@ import ReactDOM from 'react-dom/client';
|
|||||||
|
|
||||||
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
|
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
|
||||||
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
|
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
|
||||||
import Options from '~/options/Options';
|
import Sidepanel from '~/options/Sidepanel';
|
||||||
|
|
||||||
import '~/index.css';
|
import '~/index.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
|
||||||
<AppThemeProvider>
|
<AppThemeProvider>
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Options />
|
<Sidepanel />
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
</AppThemeProvider>,
|
</AppThemeProvider>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const StyledButton = styled.button<
|
|||||||
|
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 'primary':
|
case 'primary':
|
||||||
return theme.background.radialGradient;
|
return theme.background.primaryInverted;
|
||||||
case 'secondary':
|
case 'secondary':
|
||||||
return theme.background.primary;
|
return theme.background.primary;
|
||||||
default:
|
default:
|
||||||
@@ -37,7 +37,7 @@ const StyledButton = styled.button<
|
|||||||
|
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 'primary':
|
case 'primary':
|
||||||
return theme.background.transparent.light;
|
return theme.background.transparent.strong;
|
||||||
case 'secondary':
|
case 'secondary':
|
||||||
return theme.border.color.medium;
|
return theme.border.color.medium;
|
||||||
default:
|
default:
|
||||||
@@ -59,7 +59,7 @@ const StyledButton = styled.button<
|
|||||||
|
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 'primary':
|
case 'primary':
|
||||||
return theme.grayScale.gray0;
|
return theme.font.color.inverted;
|
||||||
case 'secondary':
|
case 'secondary':
|
||||||
return theme.font.color.primary;
|
return theme.font.color.primary;
|
||||||
default:
|
default:
|
||||||
@@ -88,7 +88,7 @@ const StyledButton = styled.button<
|
|||||||
default:
|
default:
|
||||||
return `
|
return `
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${theme.background.radialGradientHover}};
|
background: ${theme.background.primaryInvertedHover}};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,4 +23,6 @@ export const BACKGROUND_DARK = {
|
|||||||
overlay: RGBA(GRAY_SCALE.gray80, 0.8),
|
overlay: RGBA(GRAY_SCALE.gray80, 0.8),
|
||||||
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
|
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
|
||||||
radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
|
radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
|
||||||
|
primaryInverted: GRAY_SCALE.gray20,
|
||||||
|
primaryInvertedHover: GRAY_SCALE.gray15,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,4 +23,6 @@ export const BACKGROUND_LIGHT = {
|
|||||||
overlay: RGBA(GRAY_SCALE.gray80, 0.8),
|
overlay: RGBA(GRAY_SCALE.gray80, 0.8),
|
||||||
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
|
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
|
||||||
radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
|
radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
|
||||||
|
primaryInverted: GRAY_SCALE.gray60,
|
||||||
|
primaryInvertedHover: GRAY_SCALE.gray55,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
|
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
|
||||||
|
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
|
||||||
|
import PageInaccessible from '~/options/PageInaccessible';
|
||||||
|
|
||||||
|
import '~/index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(
|
||||||
|
<AppThemeProvider>
|
||||||
|
<React.StrictMode>
|
||||||
|
<PageInaccessible />
|
||||||
|
</React.StrictMode>
|
||||||
|
</AppThemeProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
declare module '@emotion/react' {
|
||||||
|
export interface Theme extends ThemeType {}
|
||||||
|
}
|
||||||
@@ -38,6 +38,8 @@ export default defineConfig(() => {
|
|||||||
hmr: { port: 3002 },
|
hmr: { port: 3002 },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cacheDir: './node_modules/.vite',
|
||||||
|
|
||||||
plugins: [viteManifestHack, crx({ manifest }), react(), tsconfigPaths()],
|
plugins: [viteManifestHack, crx({ manifest }), react(), tsconfigPaths()],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user