chore(extensions): pull in chrome extension (#7703)

This commit is contained in:
Jamison Lahman
2026-01-23 10:17:05 -08:00
committed by GitHub
parent 31db112de9
commit fded81dc28
27 changed files with 3559 additions and 0 deletions

21
extensions/chrome/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 DanswerAI, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,30 @@
# Onyx Chrome Extension
The Onyx chrome extension lets you research, create, and automate with LLMs powered by your team's unique knowledge. Just hit Ctrl + O on Mac or Alt + O on Windows to instantly access Onyx in your browser:
💡 Know what your company knows, instantly with the Onyx sidebar
💬 Chat: Onyx provides a natural language chat interface as the main way of interacting with the features.
🌎 Internal Search: Ask questions and get answers from all your team's knowledge, powered by Onyx's 50+ connectors to all the tools your team uses
🚀 With a simple Ctrl + O on Mac or Alt + O on Windows - instantly summarize information from any work application
⚡️ Get quick access to the work resources you need.
🆕 Onyx new tab page puts all of your companys knowledge at your fingertips
🤖 Access custom AI Agents for unique use cases, and give them access to tools to take action.
Onyx connects with dozens of popular workplace apps like Google Drive, Jira, Confluence, Slack, and more. Use this extension if you have an account created by your team admin.
## Installation
For Onyx Cloud Users, please visit the Chrome Plugin Store (pending approval still)
## Development
- Load unpacked extension in your browser
- Modify files in `src` directory
- Refresh extension in Chrome
## Contributing
Submit issues or pull requests for improvements

View File

@@ -0,0 +1,70 @@
{
"manifest_version": 3,
"name": "Onyx",
"version": "1.0",
"description": "Onyx lets you research, create, and automate with LLMs powered by your team's unique knowledge",
"permissions": [
"sidePanel",
"storage",
"activeTab",
"tabs"
],
"host_permissions": ["<all_urls>"],
"background": {
"service_worker": "service_worker.js",
"type": "module"
},
"action": {
"default_icon": {
"16": "public/icon16.png",
"48": "public/icon48.png",
"128": "public/icon128.png"
},
"default_popup": "src/pages/popup.html"
},
"icons": {
"16": "public/icon16.png",
"48": "public/icon48.png",
"128": "public/icon128.png"
},
"options_page": "src/pages/options.html",
"chrome_url_overrides": {
"newtab": "src/pages/onyx_home.html"
},
"commands": {
"toggleNewTabOverride": {
"suggested_key": {
"default": "Ctrl+Shift+O",
"mac": "Command+Shift+O"
},
"description": "Toggle Onyx New Tab Override"
},
"openSidePanel": {
"suggested_key": {
"default": "Ctrl+O",
"windows": "Alt+O",
"mac": "MacCtrl+O"
},
"description": "Open Onyx Side Panel"
}
},
"side_panel": {
"default_path": "src/pages/panel.html"
},
"omnibox": {
"keyword": "onyx"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/utils/selection-icon.js"],
"css": ["src/styles/selection-icon.css"]
}
],
"web_accessible_resources": [
{
"resources": ["public/icon32.png"],
"matches": ["<all_urls>"]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -0,0 +1,276 @@
import {
DEFAULT_ONYX_DOMAIN,
CHROME_SPECIFIC_STORAGE_KEYS,
ACTIONS,
SIDE_PANEL_PATH,
} from "./src/utils/constants.js";
// Track side panel state per window
const sidePanelOpenState = new Map();
// Open welcome page on first install
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === "install") {
chrome.storage.local.get(
{ [CHROME_SPECIFIC_STORAGE_KEYS.ONBOARDING_COMPLETE]: false },
(result) => {
if (!result[CHROME_SPECIFIC_STORAGE_KEYS.ONBOARDING_COMPLETE]) {
chrome.tabs.create({ url: "src/pages/welcome.html" });
}
},
);
}
});
async function setupSidePanel() {
if (chrome.sidePanel) {
try {
// Don't auto-open side panel on action click since we have a popup menu
await chrome.sidePanel.setPanelBehavior({
openPanelOnActionClick: false,
});
} catch (error) {
console.error("Error setting up side panel:", error);
}
}
}
async function openSidePanel(tabId) {
try {
await chrome.sidePanel.open({ tabId });
} catch (error) {
console.error("Error opening side panel:", error);
}
}
async function sendToOnyx(info, tab) {
const selectedText = encodeURIComponent(info.selectionText);
const currentUrl = encodeURIComponent(tab.url);
try {
const result = await chrome.storage.local.get({
[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN,
});
const url = `${
result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]
}${SIDE_PANEL_PATH}?user-prompt=${selectedText}`;
await openSidePanel(tab.id);
chrome.runtime.sendMessage({
action: ACTIONS.OPEN_SIDE_PANEL_WITH_INPUT,
url: url,
pageUrl: tab.url,
});
} catch (error) {
console.error("Error sending to Onyx:", error);
}
}
async function toggleNewTabOverride() {
try {
const result = await chrome.storage.local.get(
CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB,
);
const newValue =
!result[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB];
await chrome.storage.local.set({
[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: newValue,
});
chrome.notifications.create({
type: "basic",
iconUrl: "icon.png",
title: "Onyx New Tab",
message: `New Tab Override ${newValue ? "enabled" : "disabled"}`,
});
// Send a message to inform all tabs about the change
chrome.tabs.query({}, (tabs) => {
tabs.forEach((tab) => {
chrome.tabs.sendMessage(tab.id, {
action: "newTabOverrideToggled",
value: newValue,
});
});
});
} catch (error) {
console.error("Error toggling new tab override:", error);
}
}
// Note: This listener won't fire when a popup is defined in manifest.json
// The popup will show instead. This is kept as a fallback if popup is removed.
chrome.action.onClicked.addListener((tab) => {
openSidePanel(tab.id);
});
chrome.commands.onCommand.addListener(async (command) => {
if (command === ACTIONS.SEND_TO_ONYX) {
try {
const [tab] = await chrome.tabs.query({
active: true,
lastFocusedWindow: true,
});
if (tab) {
const response = await chrome.tabs.sendMessage(tab.id, {
action: ACTIONS.GET_SELECTED_TEXT,
});
const selectedText = response?.selectedText || "";
sendToOnyx({ selectionText: selectedText }, tab);
}
} catch (error) {
console.error("Error sending to Onyx:", error);
}
} else if (command === ACTIONS.TOGGLE_NEW_TAB_OVERRIDE) {
toggleNewTabOverride();
} else if (command === ACTIONS.CLOSE_SIDE_PANEL) {
try {
await chrome.sidePanel.hide();
} catch (error) {
console.error("Error closing side panel via command:", error);
}
} else if (command === ACTIONS.OPEN_SIDE_PANEL) {
chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => {
if (tabs && tabs.length > 0) {
const tab = tabs[0];
const windowId = tab.windowId;
const isOpen = sidePanelOpenState.get(windowId) || false;
if (isOpen) {
chrome.sidePanel.setOptions({ enabled: false }, () => {
chrome.sidePanel.setOptions({ enabled: true });
sidePanelOpenState.set(windowId, false);
});
} else {
chrome.sidePanel.open({ tabId: tab.id });
sidePanelOpenState.set(windowId, true);
}
}
});
return;
} else {
console.log("Unhandled command:", command);
}
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === ACTIONS.GET_CURRENT_ONYX_DOMAIN) {
chrome.storage.local.get(
{ [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN },
(result) => {
sendResponse({
[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]:
result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN],
});
},
);
return true;
}
if (request.action === ACTIONS.CLOSE_SIDE_PANEL) {
closeSidePanel();
chrome.storage.local.get(
{ [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN },
(result) => {
chrome.tabs.create({
url: `${result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]}/auth/login`,
active: true,
});
},
);
return true;
}
if (request.action === ACTIONS.OPEN_SIDE_PANEL_WITH_INPUT) {
const { selectedText, pageUrl } = request;
const tabId = sender.tab?.id;
const windowId = sender.tab?.windowId;
if (tabId && windowId) {
chrome.storage.local.get(
{ [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN },
(result) => {
const encodedText = encodeURIComponent(selectedText);
const onyxDomain = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN];
const url = `${onyxDomain}${SIDE_PANEL_PATH}?user-prompt=${encodedText}`;
chrome.storage.session.set({
pendingInput: {
url: url,
pageUrl: pageUrl,
timestamp: Date.now(),
},
});
chrome.sidePanel
.open({ windowId })
.then(() => {
chrome.runtime.sendMessage({
action: ACTIONS.OPEN_ONYX_WITH_INPUT,
url: url,
pageUrl: pageUrl,
});
})
.catch((error) => {
console.error(
"[Onyx SW] Error opening side panel with text:",
error,
);
});
},
);
} else {
console.error("[Onyx SW] Missing tabId or windowId");
}
return true;
}
});
chrome.storage.onChanged.addListener((changes, namespace) => {
if (
namespace === "local" &&
changes[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]
) {
const newValue =
changes[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]
.newValue;
if (newValue === false) {
chrome.runtime.openOptionsPage();
}
}
});
chrome.windows.onRemoved.addListener((windowId) => {
sidePanelOpenState.delete(windowId);
});
chrome.omnibox.setDefaultSuggestion({
description: 'Search Onyx for "%s"',
});
chrome.omnibox.onInputEntered.addListener(async (text) => {
try {
const result = await chrome.storage.local.get({
[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN,
});
const domain = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN];
const searchUrl = `${domain}/chat?user-prompt=${encodeURIComponent(text)}`;
chrome.tabs.update({ url: searchUrl });
} catch (error) {
console.error("Error handling omnibox search:", error);
}
});
chrome.omnibox.onInputChanged.addListener((text, suggest) => {
if (text.trim()) {
suggest([
{
content: text,
description: `Search Onyx for "<match>${text}</match>"`,
},
]);
}
});
setupSidePanel();

BIN
extensions/chrome/src/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,76 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Permissions-Policy" content="clipboard-write=(self)" />
<title>Onyx Home</title>
<link rel="stylesheet" href="../styles/shared.css" />
<style>
body,
html {
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
overflow: hidden;
}
@media (prefers-color-scheme: dark) {
html,
body {
background-color: #000;
}
}
@media (prefers-color-scheme: light) {
html,
body {
background-color: #f6f6f6;
}
}
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transition: opacity 0.5s ease-in-out;
}
#content {
position: relative;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
iframe {
border: none;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
visibility: hidden;
}
</style>
</head>
<body>
<div id="background"></div>
<div id="content">
<iframe
id="onyx-iframe"
allowfullscreen
allow="clipboard-read; clipboard-write"
></iframe>
</div>
<script src="onyx_home.js" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,248 @@
import {
CHROME_MESSAGE,
CHROME_SPECIFIC_STORAGE_KEYS,
WEB_MESSAGE,
} from "../utils/constants.js";
import {
showErrorModal,
hideErrorModal,
initErrorModal,
} from "../utils/error-modal.js";
import { getOnyxDomain } from "../utils/storage.js";
(function () {
let mainIframe = document.getElementById("onyx-iframe");
let preloadedIframe = null;
const background = document.getElementById("background");
const content = document.getElementById("content");
const DEFAULT_LIGHT_BACKGROUND_IMAGE =
"https://images.unsplash.com/photo-1692520883599-d543cfe6d43d?q=80&w=2666&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
const DEFAULT_DARK_BACKGROUND_IMAGE =
"https://images.unsplash.com/photo-1692520883599-d543cfe6d43d?q=80&w=2666&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
let iframeLoadTimeout;
let iframeLoaded = false;
initErrorModal();
async function preloadChatInterface() {
preloadedIframe = document.createElement("iframe");
const domain = await getOnyxDomain();
preloadedIframe.src = domain + "/chat";
preloadedIframe.style.opacity = "0";
preloadedIframe.style.visibility = "hidden";
preloadedIframe.style.transition = "opacity 0.3s ease-in";
preloadedIframe.style.border = "none";
preloadedIframe.style.width = "100%";
preloadedIframe.style.height = "100%";
preloadedIframe.style.position = "absolute";
preloadedIframe.style.top = "0";
preloadedIframe.style.left = "0";
preloadedIframe.style.zIndex = "1";
content.appendChild(preloadedIframe);
}
function setIframeSrc(url) {
mainIframe.src = url;
startIframeLoadTimeout();
iframeLoaded = false;
}
function startIframeLoadTimeout() {
clearTimeout(iframeLoadTimeout);
iframeLoadTimeout = setTimeout(() => {
if (!iframeLoaded) {
try {
if (
mainIframe.contentWindow.location.pathname.includes("/auth/login")
) {
showLoginPage();
} else {
showErrorModal(mainIframe.src);
}
} catch (error) {
showErrorModal(mainIframe.src);
}
}
}, 2500);
}
function showLoginPage() {
background.style.opacity = "0";
mainIframe.style.opacity = "1";
mainIframe.style.visibility = "visible";
content.style.opacity = "1";
hideErrorModal();
}
function setTheme(theme, customBackgroundImage) {
const imageUrl =
customBackgroundImage ||
(theme === "dark"
? DEFAULT_DARK_BACKGROUND_IMAGE
: DEFAULT_LIGHT_BACKGROUND_IMAGE);
background.style.backgroundImage = `url('${imageUrl}')`;
}
function fadeInContent() {
content.style.transition = "opacity 0.5s ease-in";
mainIframe.style.transition = "opacity 0.5s ease-in";
content.style.opacity = "0";
mainIframe.style.opacity = "0";
mainIframe.style.visibility = "visible";
requestAnimationFrame(() => {
content.style.opacity = "1";
mainIframe.style.opacity = "1";
setTimeout(() => {
background.style.transition = "opacity 0.3s ease-out";
background.style.opacity = "0";
}, 500);
});
}
function checkOnyxPreference() {
chrome.storage.local.get(
[
CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB,
CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN,
],
(items) => {
let useOnyxAsDefaultNewTab =
items[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB];
if (useOnyxAsDefaultNewTab === undefined) {
useOnyxAsDefaultNewTab = !!(
localStorage.getItem(
CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB,
) === "1"
);
chrome.storage.local.set({
[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]:
useOnyxAsDefaultNewTab,
});
}
if (!useOnyxAsDefaultNewTab) {
chrome.tabs.update({
url: "chrome://new-tab-page",
});
return;
}
setIframeSrc(
items[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN] + "/chat/nrf",
);
},
);
}
function loadThemeAndBackground() {
chrome.storage.local.get(
[
CHROME_SPECIFIC_STORAGE_KEYS.THEME,
CHROME_SPECIFIC_STORAGE_KEYS.BACKGROUND_IMAGE,
CHROME_SPECIFIC_STORAGE_KEYS.DARK_BG_URL,
CHROME_SPECIFIC_STORAGE_KEYS.LIGHT_BG_URL,
],
function (result) {
const theme = result[CHROME_SPECIFIC_STORAGE_KEYS.THEME] || "light";
const customBackgroundImage =
result[CHROME_SPECIFIC_STORAGE_KEYS.BACKGROUND_IMAGE];
const darkBgUrl = result[CHROME_SPECIFIC_STORAGE_KEYS.DARK_BG_URL];
const lightBgUrl = result[CHROME_SPECIFIC_STORAGE_KEYS.LIGHT_BG_URL];
let backgroundImage;
if (customBackgroundImage) {
backgroundImage = customBackgroundImage;
} else if (theme === "dark" && darkBgUrl) {
backgroundImage = darkBgUrl;
} else if (theme === "light" && lightBgUrl) {
backgroundImage = lightBgUrl;
}
setTheme(theme, backgroundImage);
checkOnyxPreference();
},
);
}
function loadNewPage(newSrc) {
if (preloadedIframe && preloadedIframe.contentWindow) {
preloadedIframe.contentWindow.postMessage(
{ type: WEB_MESSAGE.PAGE_CHANGE, href: newSrc },
"*",
);
} else {
console.error("Preloaded iframe not available");
}
}
function completePendingPageLoad() {
if (preloadedIframe) {
preloadedIframe.style.visibility = "visible";
preloadedIframe.style.opacity = "1";
preloadedIframe.style.zIndex = "1";
mainIframe.style.zIndex = "2";
mainIframe.style.opacity = "0";
setTimeout(() => {
if (content.contains(mainIframe)) {
content.removeChild(mainIframe);
}
mainIframe = preloadedIframe;
mainIframe.id = "onyx-iframe";
mainIframe.style.zIndex = "";
iframeLoaded = true;
clearTimeout(iframeLoadTimeout);
}, 200);
} else {
console.warn("No preloaded iframe available");
}
}
chrome.storage.onChanged.addListener(function (changes, namespace) {
if (namespace === "local" && changes.useOnyxAsDefaultNewTab) {
checkOnyxPreference();
}
});
window.addEventListener("message", function (event) {
if (event.data.type === CHROME_MESSAGE.SET_DEFAULT_NEW_TAB) {
chrome.storage.local.set({ useOnyxAsDefaultNewTab: event.data.value });
} else if (event.data.type === CHROME_MESSAGE.ONYX_APP_LOADED) {
clearTimeout(iframeLoadTimeout);
hideErrorModal();
fadeInContent();
iframeLoaded = true;
} else if (event.data.type === CHROME_MESSAGE.PREFERENCES_UPDATED) {
const { theme, backgroundUrl } = event.data.payload;
chrome.storage.local.set(
{
[CHROME_SPECIFIC_STORAGE_KEYS.THEME]: theme,
[CHROME_SPECIFIC_STORAGE_KEYS.BACKGROUND_IMAGE]: backgroundUrl,
},
() => {},
);
} else if (event.data.type === CHROME_MESSAGE.LOAD_NEW_PAGE) {
loadNewPage(event.data.href);
} else if (event.data.type === CHROME_MESSAGE.LOAD_NEW_CHAT_PAGE) {
completePendingPageLoad();
}
});
mainIframe.onload = function () {
clearTimeout(iframeLoadTimeout);
startIframeLoadTimeout();
};
mainIframe.onerror = function (error) {
showErrorModal(mainIframe.src);
};
loadThemeAndBackground();
preloadChatInterface();
})();

View File

@@ -0,0 +1,515 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Permissions-Policy" content="clipboard-write=(self)" />
<title>Onyx - Settings</title>
<link rel="stylesheet" href="../styles/shared.css" />
<style>
:root {
--background-900: #0a0a0a;
--background-800: #1a1a1a;
--text-light-05: rgba(255, 255, 255, 0.95);
--text-light-03: rgba(255, 255, 255, 0.6);
--white-10: rgba(255, 255, 255, 0.1);
--white-15: rgba(255, 255, 255, 0.15);
--white-20: rgba(255, 255, 255, 0.2);
--white-30: rgba(255, 255, 255, 0.3);
--white-40: rgba(255, 255, 255, 0.4);
--white-80: rgba(255, 255, 255, 0.8);
--black-40: rgba(0, 0, 0, 0.4);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: var(--font-hanken-grotesk);
background: linear-gradient(
135deg,
var(--background-900) 0%,
var(--background-800) 100%
);
min-height: 100vh;
color: var(--text-light-05);
transition: background 0.3s ease;
}
body.light-theme {
--background-900: #f5f5f5;
--background-800: #ffffff;
--text-light-05: rgba(0, 0, 0, 0.95);
--text-light-03: rgba(0, 0, 0, 0.6);
background: linear-gradient(135deg, #f5f5f5 0%, #ffffff 100%);
}
body.light-theme .settings-panel {
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.95),
rgba(245, 245, 245, 0.95)
);
border: 1px solid rgba(0, 0, 0, 0.1);
}
body.light-theme .settings-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
body.light-theme .settings-icon {
background: rgba(0, 0, 0, 0.05);
}
body.light-theme .theme-toggle {
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.1);
}
body.light-theme .theme-toggle:hover {
background: rgba(0, 0, 0, 0.08);
}
body.light-theme .theme-toggle svg {
stroke: rgba(0, 0, 0, 0.95);
}
body.light-theme .settings-group {
background: rgba(0, 0, 0, 0.03);
}
body.light-theme .setting-divider {
background: rgba(0, 0, 0, 0.1);
}
body.light-theme .input-field {
border: 1px solid rgba(0, 0, 0, 0.1);
background: rgba(0, 0, 0, 0.05);
color: rgba(0, 0, 0, 0.95);
}
body.light-theme .input-field:focus {
outline: none;
border-color: rgba(0, 0, 0, 0.25);
background: rgba(0, 0, 0, 0.08);
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05);
color: rgba(0, 0, 0, 0.95);
}
body.light-theme .status-container {
background: rgba(0, 0, 0, 0.03);
}
body.light-theme .button.secondary {
background: rgba(0, 0, 0, 0.05);
color: rgba(0, 0, 0, 0.95);
}
body.light-theme .button.secondary:hover {
background: rgba(0, 0, 0, 0.08);
}
body.light-theme .toggle-slider {
background-color: rgba(0, 0, 0, 0.15);
}
body.light-theme input:checked + .toggle-slider {
background-color: rgba(0, 0, 0, 0.3);
}
body.light-theme .toggle-slider:before {
background-color: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.settings-container {
max-width: 500px;
width: 100%;
margin: 0 auto;
padding: 40px 20px;
}
.settings-panel {
background: linear-gradient(
to bottom,
rgba(10, 10, 10, 0.95),
rgba(26, 26, 26, 0.95)
);
backdrop-filter: blur(24px);
border-radius: 16px;
border: 1px solid var(--white-10);
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.settings-header {
padding: 24px;
border-bottom: 1px solid var(--white-10);
display: flex;
align-items: center;
justify-content: space-between;
background: transparent;
}
.settings-header-left {
display: flex;
align-items: center;
gap: 12px;
}
.settings-icon {
width: 40px;
height: 40px;
border-radius: 12px;
background: white;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.settings-icon img {
width: 100%;
height: 100%;
object-fit: contain;
padding: 6px;
}
.settings-title {
font-size: 20px;
font-weight: 600;
color: var(--text-light-05);
margin: 0;
}
.theme-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 999px;
background: var(--white-10);
border: 1px solid var(--white-10);
cursor: pointer;
transition: all 0.2s;
}
.theme-toggle:hover {
background: var(--white-15);
}
.theme-toggle svg {
width: 16px;
height: 16px;
stroke: var(--text-light-05);
}
.settings-content {
padding: 24px;
}
.settings-section {
margin-bottom: 32px;
}
.settings-section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-light-03);
margin-bottom: 12px;
}
.settings-group {
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
padding: 4px;
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
}
.setting-row-content {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.setting-label {
font-size: 14px;
font-weight: 400;
color: var(--text-light-05);
}
.setting-description {
font-size: 12px;
color: var(--text-light-03);
}
.setting-divider {
height: 1px;
background: var(--white-10);
margin: 0 4px;
}
.input-field {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--white-10);
border-radius: 8px;
font-size: 14px;
background: rgba(255, 255, 255, 0.05);
color: var(--text-light-05);
font-family: var(--font-hanken-grotesk);
transition: all 0.2s;
margin: 0;
}
.input-field:focus {
outline: none;
border-color: var(--white-30);
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
color: var(--text-light-05);
}
.input-field::placeholder {
color: var(--text-light-03);
}
.setting-row .input-field {
margin-top: 0;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.2);
transition: 0.3s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: rgba(255, 255, 255, 0.4);
}
input:checked + .toggle-slider:before {
transform: translateX(20px);
}
.status-container {
margin-top: 20px;
padding: 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
opacity: 0;
transition: opacity 0.3s;
}
.status-container.show {
opacity: 1;
}
.status-message {
margin: 0 0 12px 0;
color: var(--text-light-05);
font-size: 14px;
line-height: 1.5;
}
.button {
padding: 10px 20px;
border-radius: 8px;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
font-family: var(--font-hanken-grotesk);
}
.button.secondary {
background: var(--white-10);
color: var(--text-light-05);
width: 100%;
}
.button.secondary:hover {
background: var(--white-15);
}
kbd {
background: rgba(255, 255, 255, 0.1);
border: 1px solid var(--white-10);
border-radius: 4px;
padding: 2px 6px;
font-family: monospace;
font-weight: 500;
color: var(--text-light-05);
font-size: 11px;
}
@media (max-width: 600px) {
.settings-container {
padding: 20px 16px;
}
.settings-header {
padding: 20px;
}
.settings-content {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="settings-container">
<div class="settings-panel">
<div class="settings-header">
<div class="settings-header-left">
<div class="settings-icon">
<img src="../../public/icon48.png" alt="Onyx" />
</div>
<h1 class="settings-title">Settings</h1>
</div>
<button
class="theme-toggle"
id="themeToggle"
aria-label="Toggle theme"
>
<svg
id="themeIcon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<circle cx="12" cy="12" r="4"></circle>
<path
d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"
></path>
</svg>
</button>
</div>
<div class="settings-content">
<!-- General Section -->
<section class="settings-section">
<div class="section-title">General</div>
<div class="settings-group">
<div class="setting-row">
<div class="setting-row-content">
<label class="setting-label" for="onyxDomain"
>Root Domain</label
>
<div class="setting-description">
The root URL for your Onyx instance
</div>
</div>
</div>
<div class="setting-divider"></div>
<div class="setting-row" style="padding: 12px">
<input
type="text"
id="onyxDomain"
class="input-field"
placeholder="https://cloud.onyx.app"
/>
</div>
<div class="setting-divider"></div>
<div class="setting-row">
<div class="setting-row-content">
<label class="setting-label" for="useOnyxAsDefault"
>Use Onyx as new tab page</label
>
</div>
<label class="toggle-switch">
<input type="checkbox" id="useOnyxAsDefault" />
<span class="toggle-slider"></span>
</label>
</div>
</div>
</section>
<!-- Search Engine Section -->
<section class="settings-section">
<div class="section-title">Search Engine</div>
<div class="settings-group">
<div class="setting-row">
<div class="setting-row-content">
<label class="setting-label">Use Onyx in Address Bar</label>
<div class="setting-description">
Type <kbd>onyx</kbd> followed by a space in Chrome's address
bar, then enter your search query and press Enter
</div>
</div>
</div>
<div class="setting-divider"></div>
<div class="setting-row">
<div class="setting-row-content">
<div class="setting-description">
Searches will be directed to your configured Onyx instance
at the Root Domain above
</div>
</div>
</div>
</div>
</section>
<!-- Status Message -->
<div id="statusContainer" class="status-container">
<p id="status" class="status-message"></p>
<button id="newTab" class="button secondary" style="display: none">
Open New Tab to Test
</button>
</div>
</div>
</div>
</div>
<script type="module" src="options.js"></script>
</body>
</html>

View File

@@ -0,0 +1,142 @@
import {
CHROME_SPECIFIC_STORAGE_KEYS,
DEFAULT_ONYX_DOMAIN,
} from "../utils/constants.js";
document.addEventListener("DOMContentLoaded", function () {
const domainInput = document.getElementById("onyxDomain");
const useOnyxAsDefaultToggle = document.getElementById("useOnyxAsDefault");
const statusContainer = document.getElementById("statusContainer");
const statusElement = document.getElementById("status");
const newTabButton = document.getElementById("newTab");
const themeToggle = document.getElementById("themeToggle");
const themeIcon = document.getElementById("themeIcon");
let currentTheme = "dark";
function updateThemeIcon(theme) {
if (!themeIcon) return;
if (theme === "light") {
themeIcon.innerHTML = `
<circle cx="12" cy="12" r="4"></circle>
<path d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"></path>
`;
} else {
themeIcon.innerHTML = `
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
`;
}
}
function loadStoredValues() {
chrome.storage.local.get(
{
[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN,
[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: false,
[CHROME_SPECIFIC_STORAGE_KEYS.THEME]: "dark",
},
(result) => {
if (domainInput)
domainInput.value = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN];
if (useOnyxAsDefaultToggle)
useOnyxAsDefaultToggle.checked =
result[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB];
currentTheme = result[CHROME_SPECIFIC_STORAGE_KEYS.THEME] || "dark";
updateThemeIcon(currentTheme);
document.body.className = currentTheme === "light" ? "light-theme" : "";
},
);
}
function saveSettings() {
const domain = domainInput.value.trim();
const useOnyxAsDefault = useOnyxAsDefaultToggle
? useOnyxAsDefaultToggle.checked
: false;
chrome.storage.local.set(
{
[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: domain,
[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]:
useOnyxAsDefault,
[CHROME_SPECIFIC_STORAGE_KEYS.THEME]: currentTheme,
},
() => {
showStatusMessage(
useOnyxAsDefault
? "Settings updated. Open a new tab to test it out. Click on the extension icon to bring up Onyx from any page."
: "Settings updated.",
);
},
);
}
function showStatusMessage(message) {
if (statusElement) {
const useOnyxAsDefault = useOnyxAsDefaultToggle
? useOnyxAsDefaultToggle.checked
: false;
statusElement.textContent =
message ||
(useOnyxAsDefault
? "Settings updated. Open a new tab to test it out. Click on the extension icon to bring up Onyx from any page."
: "Settings updated.");
if (newTabButton) {
newTabButton.style.display = useOnyxAsDefault ? "block" : "none";
}
}
if (statusContainer) {
statusContainer.classList.add("show");
}
setTimeout(hideStatusMessage, 5000);
}
function hideStatusMessage() {
if (statusContainer) {
statusContainer.classList.remove("show");
}
}
function toggleTheme() {
currentTheme = currentTheme === "light" ? "dark" : "light";
updateThemeIcon(currentTheme);
document.body.className = currentTheme === "light" ? "light-theme" : "";
chrome.storage.local.set({
[CHROME_SPECIFIC_STORAGE_KEYS.THEME]: currentTheme,
});
}
function openNewTab() {
chrome.tabs.create({});
}
if (domainInput) {
domainInput.addEventListener("input", () => {
clearTimeout(domainInput.saveTimeout);
domainInput.saveTimeout = setTimeout(saveSettings, 1000);
});
}
if (useOnyxAsDefaultToggle) {
useOnyxAsDefaultToggle.addEventListener("change", saveSettings);
}
if (themeToggle) {
themeToggle.addEventListener("click", toggleTheme);
}
if (newTabButton) {
newTabButton.addEventListener("click", openNewTab);
}
loadStoredValues();
});

View File

@@ -0,0 +1,91 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Permissions-Policy" content="clipboard-write=(self)" />
<title>Onyx Panel</title>
<link rel="stylesheet" href="../styles/shared.css" />
<style>
body,
html {
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
overflow: hidden;
}
#loading-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1000;
transition: opacity 0.5s ease-in-out;
}
#logo {
width: 100px;
height: 100px;
background-image: url("/public/logo.png");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
#loading-text {
color: #0a0a0a;
margin-top: 20px;
font-size: 1.125rem;
font-weight: 600;
text-align: center;
}
iframe {
border: none;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
</style>
</head>
<body>
<div id="loading-screen">
<div id="logo"></div>
<div id="loading-text">Loading Onyx...</div>
</div>
<iframe
id="onyx-panel-iframe"
allow="clipboard-read; clipboard-write"
></iframe>
<script src="../utils/error-modal.js" type="module"></script>
<script src="panel.js" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,127 @@
import { showErrorModal, showAuthModal } from "../utils/error-modal.js";
import {
ACTIONS,
CHROME_MESSAGE,
WEB_MESSAGE,
CHROME_SPECIFIC_STORAGE_KEYS,
SIDE_PANEL_PATH,
} from "../utils/constants.js";
(function () {
const iframe = document.getElementById("onyx-panel-iframe");
const loadingScreen = document.getElementById("loading-screen");
let currentUrl = "";
let iframeLoaded = false;
let iframeLoadTimeout;
let authRequired = false;
async function checkPendingInput() {
try {
const result = await chrome.storage.session.get("pendingInput");
if (result.pendingInput) {
const { url, pageUrl, timestamp } = result.pendingInput;
if (Date.now() - timestamp < 5000) {
setIframeSrc(url, pageUrl);
await chrome.storage.session.remove("pendingInput");
return true;
}
await chrome.storage.session.remove("pendingInput");
}
} catch (error) {
console.error("[Onyx Panel] Error checking pending input:", error);
}
return false;
}
async function initializePanel() {
loadingScreen.style.display = "flex";
loadingScreen.style.opacity = "1";
iframe.style.opacity = "0";
// Check for pending input first (from selection icon click)
const hasPendingInput = await checkPendingInput();
if (!hasPendingInput) {
loadOnyxDomain();
}
}
function setIframeSrc(url, pageUrl) {
iframe.src = url;
currentUrl = pageUrl;
}
function sendWebsiteToIframe(pageUrl) {
if (iframe.contentWindow && pageUrl !== currentUrl) {
iframe.contentWindow.postMessage(
{
type: WEB_MESSAGE.PAGE_CHANGE,
url: pageUrl,
},
"*",
);
currentUrl = pageUrl;
}
}
function startIframeLoadTimeout() {
iframeLoadTimeout = setTimeout(() => {
if (!iframeLoaded) {
if (authRequired) {
showAuthModal();
} else {
showErrorModal(iframe.src);
}
}
}, 2500);
}
function handleMessage(event) {
if (event.data.type === CHROME_MESSAGE.ONYX_APP_LOADED) {
clearTimeout(iframeLoadTimeout);
iframeLoaded = true;
showIframe();
if (iframe.contentWindow) {
iframe.contentWindow.postMessage({ type: "PANEL_READY" }, "*");
}
} else if (event.data.type === CHROME_MESSAGE.AUTH_REQUIRED) {
authRequired = true;
}
}
function showIframe() {
iframe.style.opacity = "1";
loadingScreen.style.opacity = "0";
setTimeout(() => {
loadingScreen.style.display = "none";
}, 500);
}
async function loadOnyxDomain() {
const response = await chrome.runtime.sendMessage({
action: ACTIONS.GET_CURRENT_ONYX_DOMAIN,
});
if (response && response[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]) {
setIframeSrc(
response[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN] + SIDE_PANEL_PATH,
"",
);
} else {
console.warn("Onyx domain not found, using default");
const domain = await getOnyxDomain();
setIframeSrc(domain + SIDE_PANEL_PATH, "");
}
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === ACTIONS.OPEN_ONYX_WITH_INPUT) {
setIframeSrc(request.url, request.pageUrl);
} else if (request.action === ACTIONS.UPDATE_PAGE_URL) {
sendWebsiteToIframe(request.pageUrl);
}
});
window.addEventListener("message", handleMessage);
initializePanel();
startIframeLoadTimeout();
})();

View File

@@ -0,0 +1,252 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Permissions-Policy" content="clipboard-write=(self)" />
<title>Onyx</title>
<link rel="stylesheet" href="../styles/shared.css" />
<style>
:root {
--background-900: #0a0a0a;
--background-800: #1a1a1a;
--text-light-05: rgba(255, 255, 255, 0.95);
--text-light-03: rgba(255, 255, 255, 0.6);
--white-10: rgba(255, 255, 255, 0.1);
--white-15: rgba(255, 255, 255, 0.15);
--white-20: rgba(255, 255, 255, 0.2);
}
* {
box-sizing: border-box;
}
body {
width: 300px;
margin: 0;
padding: 0;
font-family: var(--font-hanken-grotesk);
background: linear-gradient(
135deg,
var(--background-900) 0%,
var(--background-800) 100%
);
color: var(--text-light-05);
}
.popup-container {
padding: 16px;
}
.popup-header {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 16px;
border-bottom: 1px solid var(--white-10);
margin-bottom: 16px;
}
.popup-icon {
width: 36px;
height: 36px;
border-radius: 10px;
background: white;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.popup-icon img {
width: 100%;
height: 100%;
object-fit: contain;
padding: 4px;
}
.popup-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--text-light-05);
}
.menu-button-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.menu-button-text {
display: flex;
align-items: center;
gap: 10px;
}
.menu-button-shortcut {
font-size: 11px;
color: var(--text-light-03);
font-weight: 400;
margin-left: auto;
}
.settings-group {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 4px;
margin-bottom: 12px;
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
}
.setting-label {
font-size: 14px;
font-weight: 400;
color: var(--text-light-05);
}
.setting-divider {
height: 1px;
background: var(--white-10);
margin: 0 4px;
}
.menu-button {
background: rgba(255, 255, 255, 0.05);
border: none;
padding: 12px;
width: 100%;
text-align: left;
cursor: pointer;
font-size: 14px;
color: var(--text-light-05);
font-weight: 400;
transition: background 0.2s;
border-radius: 12px;
font-family: var(--font-hanken-grotesk);
}
.menu-button:hover {
background: rgba(255, 255, 255, 0.1);
}
.menu-button svg {
width: 18px;
height: 18px;
stroke: var(--text-light-05);
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.button-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.2);
transition: 0.3s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: rgba(255, 255, 255, 0.4);
}
input:checked + .toggle-slider:before {
transform: translateX(20px);
}
</style>
</head>
<body>
<div class="popup-container">
<div class="popup-header">
<div class="popup-icon">
<img src="../../public/icon48.png" alt="Onyx" />
</div>
<h2 class="popup-title">Onyx</h2>
</div>
<div class="settings-group">
<div class="setting-row">
<label class="setting-label" for="defaultNewTabToggle">
Use Onyx as new tab page
</label>
<label class="toggle-switch">
<input type="checkbox" id="defaultNewTabToggle" />
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="button-group">
<button class="menu-button" id="openSidePanel">
<div class="menu-button-content">
<div class="menu-button-text">
<svg viewBox="0 0 24 24">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="15" y1="3" x2="15" y2="21"></line>
</svg>
Open Onyx Panel
</div>
<span class="menu-button-shortcut">Ctrl+O</span>
</div>
</button>
<button class="menu-button" id="openOptions">
<div class="menu-button-text">
<svg viewBox="0 0 24 24">
<circle cx="12" cy="12" r="3"></circle>
<path
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
></path>
</svg>
Extension Settings
</div>
</button>
</div>
</div>
<script type="module" src="popup.js"></script>
</body>
</html>

View File

@@ -0,0 +1,58 @@
import { CHROME_SPECIFIC_STORAGE_KEYS } from "../utils/constants.js";
document.addEventListener("DOMContentLoaded", async function () {
const defaultNewTabToggle = document.getElementById("defaultNewTabToggle");
const openSidePanelButton = document.getElementById("openSidePanel");
const openOptionsButton = document.getElementById("openOptions");
async function loadSetting() {
const result = await chrome.storage.local.get({
[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: false,
});
if (defaultNewTabToggle) {
defaultNewTabToggle.checked =
result[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB];
}
}
async function toggleSetting() {
const currentValue = defaultNewTabToggle.checked;
await chrome.storage.local.set({
[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: currentValue,
});
}
async function openSidePanel() {
try {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true,
});
if (tab && chrome.sidePanel) {
await chrome.sidePanel.open({ tabId: tab.id });
window.close();
}
} catch (error) {
console.error("Error opening side panel:", error);
}
}
function openOptions() {
chrome.runtime.openOptionsPage();
window.close();
}
await loadSetting();
if (defaultNewTabToggle) {
defaultNewTabToggle.addEventListener("change", toggleSetting);
}
if (openSidePanelButton) {
openSidePanelButton.addEventListener("click", openSidePanel);
}
if (openOptionsButton) {
openOptionsButton.addEventListener("click", openOptions);
}
});

View File

@@ -0,0 +1,618 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to Onyx</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="../styles/shared.css" />
<style>
:root {
--background-900: #0a0a0a;
--background-800: #1a1a1a;
--text-light-05: rgba(255, 255, 255, 0.95);
--text-light-03: rgba(255, 255, 255, 0.6);
--white-10: rgba(255, 255, 255, 0.1);
--white-15: rgba(255, 255, 255, 0.15);
--white-20: rgba(255, 255, 255, 0.2);
--white-30: rgba(255, 255, 255, 0.3);
--white-40: rgba(255, 255, 255, 0.4);
--white-80: rgba(255, 255, 255, 0.8);
--black-40: rgba(0, 0, 0, 0.4);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: var(--font-hanken-grotesk);
background: linear-gradient(
135deg,
var(--background-900) 0%,
var(--background-800) 100%
);
min-height: 100vh;
color: var(--text-light-05);
transition: background 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
body.light-theme {
--background-900: #f5f5f5;
--background-800: #ffffff;
--text-light-05: rgba(0, 0, 0, 0.95);
--text-light-03: rgba(0, 0, 0, 0.6);
background: linear-gradient(135deg, #f5f5f5 0%, #ffffff 100%);
}
body.light-theme .welcome-panel {
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.95),
rgba(245, 245, 245, 0.95)
);
border: 1px solid rgba(0, 0, 0, 0.1);
}
body.light-theme .welcome-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
body.light-theme .logo-container {
background: rgba(0, 0, 0, 0.05);
}
body.light-theme .theme-toggle {
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.1);
}
body.light-theme .theme-toggle:hover {
background: rgba(0, 0, 0, 0.08);
}
body.light-theme .theme-toggle svg {
stroke: rgba(0, 0, 0, 0.95);
}
body.light-theme .input-field {
border: 1px solid rgba(0, 0, 0, 0.1);
background: rgba(0, 0, 0, 0.05);
color: rgba(0, 0, 0, 0.95);
}
body.light-theme .input-field:focus {
outline: none;
border-color: rgba(0, 0, 0, 0.25);
background: rgba(0, 0, 0, 0.08);
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05);
}
body.light-theme .input-field::placeholder {
color: rgba(0, 0, 0, 0.4);
}
body.light-theme .toggle-slider {
background-color: rgba(0, 0, 0, 0.15);
}
body.light-theme input:checked + .toggle-slider {
background-color: rgba(0, 0, 0, 0.3);
}
body.light-theme .toggle-slider:before {
background-color: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
body.light-theme .step-dot {
background: rgba(0, 0, 0, 0.2);
}
body.light-theme .step-dot.active {
background: rgba(0, 0, 0, 0.6);
}
body.light-theme .btn-primary {
background: rgba(0, 0, 0, 0.9);
color: white;
}
body.light-theme .btn-primary:hover {
background: rgba(0, 0, 0, 0.8);
}
body.light-theme .btn-secondary {
background: rgba(0, 0, 0, 0.05);
color: rgba(0, 0, 0, 0.95);
}
body.light-theme .btn-secondary:hover {
background: rgba(0, 0, 0, 0.08);
}
body.light-theme .settings-group {
background: rgba(0, 0, 0, 0.03);
}
body.light-theme .setting-divider {
background: rgba(0, 0, 0, 0.1);
}
.welcome-container {
max-width: 480px;
width: 100%;
margin: 0 auto;
padding: 40px 20px;
}
.welcome-panel {
background: linear-gradient(
to bottom,
rgba(10, 10, 10, 0.95),
rgba(26, 26, 26, 0.95)
);
backdrop-filter: blur(24px);
border-radius: 20px;
border: 1px solid var(--white-10);
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
animation: panelFadeIn 0.5s ease-out;
}
@keyframes panelFadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.welcome-header {
padding: 24px;
border-bottom: 1px solid var(--white-10);
display: flex;
align-items: center;
justify-content: space-between;
}
.header-left {
display: flex;
align-items: center;
gap: 14px;
}
.logo-container {
width: 48px;
height: 48px;
border-radius: 14px;
background: white;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.logo-container img {
width: 100%;
height: 100%;
object-fit: contain;
padding: 8px;
}
.welcome-title {
font-size: 22px;
font-weight: 600;
color: var(--text-light-05);
margin: 0;
}
.theme-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 999px;
background: var(--white-10);
border: 1px solid var(--white-10);
cursor: pointer;
transition: all 0.2s;
}
.theme-toggle:hover {
background: var(--white-15);
}
.theme-toggle svg {
width: 18px;
height: 18px;
stroke: var(--text-light-05);
}
.welcome-content {
padding: 32px 24px;
}
/* Step indicator */
.step-indicator {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 32px;
}
.step-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--white-20);
transition: all 0.3s ease;
}
.step-dot.active {
background: var(--white-80);
transform: scale(1.2);
}
/* Steps */
.step {
display: none;
animation: stepFadeIn 0.4s ease-out;
}
.step.active {
display: block;
}
@keyframes stepFadeIn {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.step-title {
font-size: 24px;
font-weight: 600;
margin: 0 0 8px 0;
text-align: center;
}
.step-description {
font-size: 15px;
color: var(--text-light-03);
text-align: center;
margin: 0 0 28px 0;
line-height: 1.5;
}
/* Form elements */
.input-group {
margin-bottom: 24px;
}
.input-label {
display: block;
font-size: 13px;
font-weight: 500;
color: var(--text-light-03);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.input-field {
width: 100%;
padding: 14px 16px;
border: 1px solid var(--white-10);
border-radius: 12px;
font-size: 15px;
background: rgba(255, 255, 255, 0.95);
color: rgba(0, 0, 0, 0.9);
font-family: var(--font-hanken-grotesk);
transition: all 0.2s;
}
.input-field:focus {
outline: none;
border-color: var(--white-30);
background: rgba(255, 255, 255, 1);
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.15);
}
.input-field::placeholder {
color: rgba(0, 0, 0, 0.4);
}
/* Settings group for step 2 */
.settings-group {
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
padding: 4px;
margin-bottom: 24px;
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
}
.setting-content {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
padding-right: 16px;
}
.setting-label {
font-size: 15px;
font-weight: 500;
color: var(--text-light-05);
}
.setting-description {
font-size: 13px;
color: var(--text-light-03);
line-height: 1.4;
}
.setting-divider {
height: 1px;
background: var(--white-10);
margin: 0 8px;
}
/* Toggle switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 52px;
height: 28px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.2);
transition: 0.3s;
border-radius: 28px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 22px;
width: 22px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: rgba(255, 255, 255, 0.4);
}
input:checked + .toggle-slider:before {
transform: translateX(24px);
}
/* Buttons */
.button-group {
display: flex;
gap: 12px;
margin-top: 8px;
}
.btn {
flex: 1;
padding: 14px 24px;
border-radius: 12px;
border: none;
cursor: pointer;
font-size: 15px;
font-weight: 500;
font-family: var(--font-hanken-grotesk);
transition: all 0.2s;
}
.btn-primary {
background: rgba(255, 255, 255, 0.95);
color: #0a0a0a;
}
.btn-primary:hover {
background: rgba(255, 255, 255, 0.85);
transform: translateY(-1px);
}
.btn-secondary {
background: var(--white-10);
color: var(--text-light-05);
}
.btn-secondary:hover {
background: var(--white-15);
}
.btn:active {
transform: translateY(0);
}
/* Success animation for completion */
.success-icon {
width: 64px;
height: 64px;
margin: 0 auto 24px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
animation: successPop 0.5s ease-out;
}
.success-icon svg {
width: 32px;
height: 32px;
stroke: var(--text-light-05);
stroke-width: 2.5;
}
@keyframes successPop {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
opacity: 1;
}
}
@media (max-width: 500px) {
.welcome-container {
padding: 20px 16px;
}
.welcome-content {
padding: 24px 20px;
}
.step-title {
font-size: 20px;
}
.button-group {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="welcome-container">
<div class="welcome-panel">
<div class="welcome-header">
<div class="header-left">
<div class="logo-container">
<img src="../../public/icon48.png" alt="Onyx" />
</div>
<h1 class="welcome-title">Onyx</h1>
</div>
<button
class="theme-toggle"
id="themeToggle"
aria-label="Toggle theme"
>
<svg
id="themeIcon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
</div>
<div class="welcome-content">
<div class="step-indicator">
<div class="step-dot active" data-step="1"></div>
<div class="step-dot" data-step="2"></div>
</div>
<!-- Step 1: Root Domain -->
<div class="step active" id="step1">
<h2 class="step-title">Welcome to Onyx</h2>
<p class="step-description">
Enter your Onyx instance URL to get started. This is where your
Onyx deployment is hosted.
</p>
<div class="input-group">
<label class="input-label" for="onyxDomain">Root Domain</label>
<input
type="text"
id="onyxDomain"
class="input-field"
placeholder="https://cloud.onyx.app"
/>
</div>
<div class="button-group">
<button class="btn btn-primary" id="continueBtn">Continue</button>
</div>
</div>
<!-- Step 2: New Tab Setting -->
<div class="step" id="step2">
<h2 class="step-title">Customize Your Experience</h2>
<p class="step-description">
Set Onyx as your new tab page for quick access to your AI
assistant.
</p>
<div class="settings-group">
<div class="setting-row">
<div class="setting-content">
<span class="setting-label">Use Onyx as new tab page</span>
<span class="setting-description"
>Open Onyx every time you create a new tab</span
>
</div>
<label class="toggle-switch">
<input type="checkbox" id="useOnyxAsDefault" checked />
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="button-group">
<button class="btn btn-secondary" id="backBtn">Back</button>
<button class="btn btn-primary" id="finishBtn">
Get Started
</button>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="welcome.js"></script>
</body>
</html>

View File

@@ -0,0 +1,192 @@
import {
CHROME_SPECIFIC_STORAGE_KEYS,
DEFAULT_ONYX_DOMAIN,
} from "../utils/constants.js";
document.addEventListener("DOMContentLoaded", function () {
const domainInput = document.getElementById("onyxDomain");
const useOnyxAsDefaultToggle = document.getElementById("useOnyxAsDefault");
const continueBtn = document.getElementById("continueBtn");
const backBtn = document.getElementById("backBtn");
const finishBtn = document.getElementById("finishBtn");
const themeToggle = document.getElementById("themeToggle");
const themeIcon = document.getElementById("themeIcon");
const step1 = document.getElementById("step1");
const step2 = document.getElementById("step2");
const stepDots = document.querySelectorAll(".step-dot");
let currentStep = 1;
let currentTheme = "dark";
// Initialize theme based on system preference or stored value
function initTheme() {
chrome.storage.local.get(
{ [CHROME_SPECIFIC_STORAGE_KEYS.THEME]: null },
(result) => {
const storedTheme = result[CHROME_SPECIFIC_STORAGE_KEYS.THEME];
if (storedTheme) {
currentTheme = storedTheme;
} else {
// Check system preference
currentTheme = window.matchMedia("(prefers-color-scheme: light)")
.matches
? "light"
: "dark";
}
applyTheme();
},
);
}
function applyTheme() {
document.body.className = currentTheme === "light" ? "light-theme" : "";
updateThemeIcon();
}
function updateThemeIcon() {
if (!themeIcon) return;
if (currentTheme === "light") {
themeIcon.innerHTML = `
<circle cx="12" cy="12" r="4"></circle>
<path d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"></path>
`;
} else {
themeIcon.innerHTML = `
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
`;
}
}
function toggleTheme() {
currentTheme = currentTheme === "light" ? "dark" : "light";
applyTheme();
chrome.storage.local.set({
[CHROME_SPECIFIC_STORAGE_KEYS.THEME]: currentTheme,
});
}
function goToStep(step) {
if (step === 1) {
step2.classList.remove("active");
setTimeout(() => {
step1.classList.add("active");
}, 50);
} else if (step === 2) {
step1.classList.remove("active");
setTimeout(() => {
step2.classList.add("active");
}, 50);
}
stepDots.forEach((dot) => {
const dotStep = parseInt(dot.dataset.step);
if (dotStep === step) {
dot.classList.add("active");
} else {
dot.classList.remove("active");
}
});
currentStep = step;
}
// Validate domain input
function validateDomain(domain) {
if (!domain) return false;
try {
new URL(domain);
return true;
} catch {
return false;
}
}
function handleContinue() {
const domain = domainInput.value.trim();
if (domain && !validateDomain(domain)) {
domainInput.style.borderColor = "rgba(255, 100, 100, 0.5)";
domainInput.focus();
return;
}
domainInput.style.borderColor = "";
goToStep(2);
}
function handleBack() {
goToStep(1);
}
function handleFinish() {
const domain = domainInput.value.trim() || DEFAULT_ONYX_DOMAIN;
const useOnyxAsDefault = useOnyxAsDefaultToggle.checked;
chrome.storage.local.set(
{
[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: domain,
[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]:
useOnyxAsDefault,
[CHROME_SPECIFIC_STORAGE_KEYS.THEME]: currentTheme,
[CHROME_SPECIFIC_STORAGE_KEYS.ONBOARDING_COMPLETE]: true,
},
() => {
// Open a new tab if they enabled the new tab feature, otherwise just close
if (useOnyxAsDefault) {
chrome.tabs.create({}, () => {
window.close();
});
} else {
window.close();
}
},
);
}
// Load any existing values (in case user returns to this page)
function loadStoredValues() {
chrome.storage.local.get(
{
[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: "",
[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: true,
},
(result) => {
if (result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]) {
domainInput.value = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN];
}
useOnyxAsDefaultToggle.checked =
result[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB];
},
);
}
if (themeToggle) {
themeToggle.addEventListener("click", toggleTheme);
}
if (continueBtn) {
continueBtn.addEventListener("click", handleContinue);
}
if (backBtn) {
backBtn.addEventListener("click", handleBack);
}
if (finishBtn) {
finishBtn.addEventListener("click", handleFinish);
}
// Allow Enter key to proceed
if (domainInput) {
domainInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
handleContinue();
}
});
}
initTheme();
loadStoredValues();
});

View File

@@ -0,0 +1,42 @@
#onyx-selection-icon {
position: fixed;
z-index: 2147483647;
width: 32px;
height: 32px;
border-radius: 50%;
background-color: #ffffff;
border: 1px solid #e0e0e0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transform: scale(0.8);
transition:
opacity 0.15s ease,
transform 0.15s ease,
box-shadow 0.15s ease;
pointer-events: none;
}
#onyx-selection-icon.visible {
opacity: 1;
transform: scale(1);
pointer-events: auto;
}
#onyx-selection-icon:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
transform: scale(1.1);
}
#onyx-selection-icon:active {
transform: scale(0.95);
}
#onyx-selection-icon img {
width: 20px;
height: 20px;
pointer-events: none;
}

View File

@@ -0,0 +1,169 @@
/* Import Hanken Grotesk font */
@import url("https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@300;400;500;600;700&display=swap");
:root {
--primary-color: #4285f4;
--primary-hover-color: #3367d6;
--secondary-color: #f1f3f4;
--secondary-hover-color: #e8eaed;
--text-color: #333;
--text-light-color: #666;
--background-color: #f1f3f4;
--card-background-color: #fff;
--border-color: #ccc;
--font-family: Arial, sans-serif;
--font-hanken-grotesk: "Hanken Grotesk", sans-serif;
}
body {
font-family: var(--font-hanken-grotesk);
margin: 0;
padding: 0;
}
.container {
max-width: 500px;
width: 90%;
margin: 0 auto;
}
.card {
background-color: var(--card-background-color);
padding: 25px;
border-radius: 10px;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1);
}
h1 {
color: var(--text-color);
font-size: 24px;
font-weight: 600;
margin-top: 0;
margin-bottom: 20px;
}
.option-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
color: var(--text-light-color);
font-weight: 400;
font-size: 16px;
}
input[type="text"] {
width: 100%;
padding: 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
background-color: var(--card-background-color);
color: var(--text-color);
}
.button {
width: 100%;
padding: 10px 20px;
border-radius: 5px;
border: none;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: background-color 0.3s;
}
.button.primary {
background-color: var(--primary-color);
color: #fff;
}
.button.primary:hover {
background-color: var(--primary-hover-color);
}
.button.secondary {
background-color: var(--secondary-color);
color: var(--text-color);
}
.button.secondary:hover {
background-color: var(--secondary-hover-color);
}
.status-container {
margin-top: 10px;
margin-bottom: 15px;
}
.status-message {
margin: 0 0 10px 0;
color: var(--text-color);
font-weight: 500;
text-align: center;
font-size: 16px;
transition: opacity 0.5s ease-in-out;
}
kbd {
background-color: var(--secondary-color);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 2px 5px;
font-family: monospace;
font-weight: 500;
color: var(--text-color);
}
.toggle-label {
display: flex;
justify-content: space-between;
align-items: center;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--secondary-color);
transition: 0.4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 2px;
bottom: 2px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--primary-color);
}
input:checked + .slider:before {
transform: translateX(26px);
}

View File

@@ -0,0 +1,43 @@
export const THEMES = {
LIGHT: "light",
DARK: "dark",
};
export const DEFAULT_ONYX_DOMAIN = "http://localhost:3000";
export const SIDE_PANEL_PATH = "/chat/nrf/side-panel";
export const ACTIONS = {
GET_SELECTED_TEXT: "getSelectedText",
GET_CURRENT_ONYX_DOMAIN: "getCurrentOnyxDomain",
UPDATE_PAGE_URL: "updatePageUrl",
SEND_TO_ONYX: "sendToOnyx",
OPEN_SIDE_PANEL: "openSidePanel",
TOGGLE_NEW_TAB_OVERRIDE: "toggleNewTabOverride",
OPEN_SIDE_PANEL_WITH_INPUT: "openSidePanelWithInput",
OPEN_ONYX_WITH_INPUT: "openOnyxWithInput",
CLOSE_SIDE_PANEL: "closeSidePanel",
};
export const CHROME_SPECIFIC_STORAGE_KEYS = {
ONYX_DOMAIN: "onyxExtensionDomain",
USE_ONYX_AS_DEFAULT_NEW_TAB: "onyxExtensionDefaultNewTab",
THEME: "onyxExtensionTheme",
BACKGROUND_IMAGE: "onyxExtensionBackgroundImage",
DARK_BG_URL: "onyxExtensionDarkBgUrl",
LIGHT_BG_URL: "onyxExtensionLightBgUrl",
ONBOARDING_COMPLETE: "onyxExtensionOnboardingComplete",
};
export const CHROME_MESSAGE = {
PREFERENCES_UPDATED: "PREFERENCES_UPDATED",
ONYX_APP_LOADED: "ONYX_APP_LOADED",
SET_DEFAULT_NEW_TAB: "SET_DEFAULT_NEW_TAB",
LOAD_NEW_CHAT_PAGE: "LOAD_NEW_CHAT_PAGE",
LOAD_NEW_PAGE: "LOAD_NEW_PAGE",
AUTH_REQUIRED: "AUTH_REQUIRED",
};
export const WEB_MESSAGE = {
PAGE_CHANGE: "PAGE_CHANGE",
};

View File

@@ -0,0 +1,34 @@
let sidePanel = null;
function createSidePanel() {
sidePanel = document.createElement("div");
sidePanel.id = "onyx-side-panel";
sidePanel.style.cssText = `
position: fixed;
top: 0;
right: -400px;
width: 400px;
height: 100%;
background-color: white;
box-shadow: -2px 0 5px rgba(0,0,0,0.2);
transition: right 0.3s ease-in-out;
z-index: 9999;
`;
const iframe = document.createElement("iframe");
iframe.style.cssText = `
width: 100%;
height: 100%;
border: none;
`;
chrome.runtime.sendMessage(
{ action: ACTIONS.GET_CURRENT_ONYX_DOMAIN },
function (response) {
iframe.src = response[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN];
},
);
sidePanel.appendChild(iframe);
document.body.appendChild(sidePanel);
}

View File

@@ -0,0 +1,379 @@
import {
CHROME_SPECIFIC_STORAGE_KEYS,
DEFAULT_ONYX_DOMAIN,
ACTIONS,
} from "./constants.js";
const errorModalHTML = `
<div id="error-modal">
<div class="modal-backdrop"></div>
<div class="modal-content">
<div class="modal-header">
<div class="modal-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
</div>
<h2>Configuration Error</h2>
</div>
<div class="modal-body">
<p class="modal-description">The Onyx configuration needs to be updated. Please check your settings or contact your Onyx administrator.</p>
<div class="url-display">
<span class="url-label">Attempted to load:</span>
<span id="attempted-url" class="url-value"></span>
</div>
</div>
<div class="modal-footer">
<div class="button-container">
<button id="open-options" class="button primary">Open Extension Options</button>
<button id="disable-override" class="button secondary">Disable New Tab Override</button>
</div>
</div>
</div>
</div>
`;
const style = document.createElement("style");
style.textContent = `
:root {
--background-900: #0a0a0a;
--background-800: #1a1a1a;
--text-light-05: rgba(255, 255, 255, 0.95);
--text-light-03: rgba(255, 255, 255, 0.6);
--white-10: rgba(255, 255, 255, 0.1);
--white-15: rgba(255, 255, 255, 0.15);
--white-20: rgba(255, 255, 255, 0.2);
--white-30: rgba(255, 255, 255, 0.3);
}
#error-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
align-items: center;
justify-content: center;
z-index: 2000;
font-family: var(--font-hanken-grotesk), 'Hanken Grotesk', sans-serif;
}
#error-modal .modal-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
}
#error-modal .modal-content {
position: relative;
background: linear-gradient(to bottom, rgba(10, 10, 10, 0.95), rgba(26, 26, 26, 0.95));
backdrop-filter: blur(24px);
border-radius: 16px;
border: 1px solid var(--white-10);
max-width: 95%;
width: 500px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
overflow: hidden;
}
#error-modal .modal-header {
padding: 24px;
border-bottom: 1px solid var(--white-10);
display: flex;
align-items: center;
gap: 12px;
}
#error-modal .modal-icon {
width: 40px;
height: 40px;
border-radius: 12px;
background: rgba(255, 87, 87, 0.15);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
#error-modal .modal-icon svg {
width: 24px;
height: 24px;
stroke: #ff5757;
}
#error-modal .modal-icon.auth-icon {
background: rgba(66, 133, 244, 0.15);
}
#error-modal .modal-icon.auth-icon svg {
stroke: #4285f4;
}
#error-modal h2 {
margin: 0;
color: var(--text-light-05);
font-size: 20px;
font-weight: 600;
}
#error-modal .modal-body {
padding: 24px;
}
#error-modal .modal-description {
color: var(--text-light-05);
margin: 0 0 20px 0;
font-size: 14px;
line-height: 1.6;
font-weight: 400;
}
#error-modal .url-display {
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 12px;
border: 1px solid var(--white-10);
}
#error-modal .url-label {
display: block;
font-size: 12px;
color: var(--text-light-03);
margin-bottom: 6px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
}
#error-modal .url-value {
display: block;
font-size: 13px;
color: var(--text-light-05);
word-break: break-all;
font-family: monospace;
line-height: 1.5;
}
#error-modal .modal-footer {
padding: 0 24px 24px 24px;
}
#error-modal .button-container {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 16px;
}
#error-modal .button {
padding: 12px 20px;
border-radius: 8px;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
font-family: var(--font-hanken-grotesk), 'Hanken Grotesk', sans-serif;
}
#error-modal .button.primary {
background: rgba(255, 255, 255, 0.15);
color: var(--text-light-05);
border: 1px solid var(--white-10);
}
#error-modal .button.primary:hover {
background: rgba(255, 255, 255, 0.2);
border-color: var(--white-20);
}
#error-modal .button.secondary {
background: rgba(255, 255, 255, 0.05);
color: var(--text-light-05);
border: 1px solid var(--white-10);
}
#error-modal .button.secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: var(--white-15);
}
#error-modal kbd {
background: rgba(255, 255, 255, 0.1);
border: 1px solid var(--white-10);
border-radius: 4px;
padding: 2px 6px;
font-family: monospace;
font-weight: 500;
color: var(--text-light-05);
font-size: 11px;
}
@media (min-width: 768px) {
#error-modal .button-container {
flex-direction: row;
}
#error-modal .button {
flex: 1;
}
}
`;
const authModalHTML = `
<div id="error-modal">
<div class="modal-backdrop"></div>
<div class="modal-content">
<div class="modal-header">
<div class="modal-icon auth-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</div>
<h2>Authentication Required</h2>
</div>
<div class="modal-body">
<p class="modal-description">You need to log in to access Onyx. Click the button below to authenticate.</p>
</div>
<div class="modal-footer">
<div class="button-container">
<button id="open-auth" class="button primary">Log In to Onyx</button>
</div>
</div>
</div>
</div>
`;
let errorModal, attemptedUrlSpan, openOptionsButton, disableOverrideButton;
let authModal, openAuthButton;
export function initErrorModal() {
if (!document.getElementById("error-modal")) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "../styles/shared.css";
document.head.appendChild(link);
document.body.insertAdjacentHTML("beforeend", errorModalHTML);
document.head.appendChild(style);
errorModal = document.getElementById("error-modal");
authModal = document.getElementById("error-modal");
attemptedUrlSpan = document.getElementById("attempted-url");
openOptionsButton = document.getElementById("open-options");
disableOverrideButton = document.getElementById("disable-override");
openOptionsButton.addEventListener("click", (e) => {
e.preventDefault();
chrome.runtime.openOptionsPage();
});
disableOverrideButton.addEventListener("click", () => {
chrome.storage.local.set({ useOnyxAsDefaultNewTab: false }, () => {
chrome.tabs.update({ url: "chrome://new-tab-page" });
});
});
}
}
export function showErrorModal(url) {
if (!errorModal) {
initErrorModal();
}
if (errorModal) {
errorModal.style.display = "flex";
errorModal.style.zIndex = "9999";
attemptedUrlSpan.textContent = url;
document.body.style.overflow = "hidden";
}
}
export function hideErrorModal() {
if (errorModal) {
errorModal.style.display = "none";
document.body.style.overflow = "auto";
}
}
export function checkModalVisibility() {
return errorModal
? window.getComputedStyle(errorModal).display !== "none"
: false;
}
export function initAuthModal() {
if (!document.getElementById("error-modal")) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "../styles/shared.css";
document.head.appendChild(link);
document.body.insertAdjacentHTML("beforeend", authModalHTML);
document.head.appendChild(style);
authModal = document.getElementById("error-modal");
openAuthButton = document.getElementById("open-auth");
openAuthButton.addEventListener("click", (e) => {
e.preventDefault();
chrome.storage.local.get(
{ [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN },
(result) => {
const onyxDomain = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN];
chrome.runtime.sendMessage(
{ action: ACTIONS.CLOSE_SIDE_PANEL },
() => {
if (chrome.runtime.lastError) {
console.error(
"Error closing side panel:",
chrome.runtime.lastError,
);
}
chrome.tabs.create(
{
url: `${onyxDomain}/auth/login`,
active: true,
},
(_) => {
if (chrome.runtime.lastError) {
console.error(
"Error opening auth tab:",
chrome.runtime.lastError,
);
}
},
);
},
);
},
);
});
}
}
export function showAuthModal() {
if (!authModal) {
initAuthModal();
}
if (authModal) {
authModal.style.display = "flex";
authModal.style.zIndex = "9999";
document.body.style.overflow = "hidden";
}
}
export function hideAuthModal() {
if (authModal) {
authModal.style.display = "none";
document.body.style.overflow = "auto";
}
}

View File

@@ -0,0 +1,152 @@
(function () {
const OPEN_SIDE_PANEL_WITH_INPUT = "openSidePanelWithInput";
let selectionIcon = null;
let currentSelectedText = "";
function createSelectionIcon() {
if (selectionIcon) return;
selectionIcon = document.createElement("div");
selectionIcon.id = "onyx-selection-icon";
const img = document.createElement("img");
img.src = chrome.runtime.getURL("public/icon32.png");
img.alt = "Search with Onyx";
selectionIcon.appendChild(img);
document.body.appendChild(selectionIcon);
selectionIcon.addEventListener("mousedown", handleIconClick);
}
function showIcon(text) {
if (!selectionIcon) {
createSelectionIcon();
}
currentSelectedText = text;
const selection = window.getSelection();
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
const iconSize = 32;
const offset = 4;
let posX = rect.right + offset;
let posY = rect.bottom + offset;
if (posX + iconSize > window.innerWidth) {
posX = rect.left - iconSize - offset;
}
if (posY + iconSize > window.innerHeight) {
posY = rect.top - iconSize - offset;
}
posX = Math.max(
offset,
Math.min(posX, window.innerWidth - iconSize - offset),
);
posY = Math.max(
offset,
Math.min(posY, window.innerHeight - iconSize - offset),
);
selectionIcon.style.left = `${posX}px`;
selectionIcon.style.top = `${posY}px`;
selectionIcon.classList.add("visible");
}
function hideIcon() {
if (selectionIcon) {
selectionIcon.classList.remove("visible");
}
currentSelectedText = "";
}
function handleIconClick(e) {
e.preventDefault();
e.stopPropagation();
const textToSend = currentSelectedText;
if (textToSend) {
chrome.runtime.sendMessage(
{
action: OPEN_SIDE_PANEL_WITH_INPUT,
selectedText: textToSend,
pageUrl: window.location.href,
},
(response) => {
if (chrome.runtime.lastError) {
console.error(
"[Onyx] Error sending message:",
chrome.runtime.lastError.message,
);
} else {
}
},
);
}
hideIcon();
}
document.addEventListener("mouseup", (e) => {
if (
e.target.id === "onyx-selection-icon" ||
e.target.closest("#onyx-selection-icon")
) {
return;
}
setTimeout(() => {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (selectedText && selectedText.length > 0) {
showIcon(selectedText);
} else {
hideIcon();
}
}, 10);
});
document.addEventListener("mousedown", (e) => {
if (
e.target.id !== "onyx-selection-icon" &&
!e.target.closest("#onyx-selection-icon")
) {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (!selectedText) {
hideIcon();
}
}
});
document.addEventListener(
"scroll",
() => {
hideIcon();
},
true,
);
document.addEventListener("selectionchange", () => {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (!selectedText) {
hideIcon();
}
});
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", createSelectionIcon);
} else {
createSelectionIcon();
}
})();

View File

@@ -0,0 +1,24 @@
import {
DEFAULT_ONYX_DOMAIN,
CHROME_SPECIFIC_STORAGE_KEYS,
} from "./constants.js";
export async function getOnyxDomain() {
const result = await chrome.storage.local.get({
[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN,
});
return result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN];
}
export function setOnyxDomain(domain, callback) {
chrome.storage.local.set(
{ [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: domain },
callback,
);
}
export function getOnyxDomainSync() {
return new Promise((resolve) => {
getOnyxDomain(resolve);
});
}