chore: basic extension support

This commit is contained in:
Abhijith Vijayan [FLUXON] 2026-01-03 19:18:17 +05:30
parent f19a5580a3
commit b96c1969df
12 changed files with 556 additions and 101 deletions

2
package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "3.0.0",
"license": "MIT",
"dependencies": {
"advanced-css-reset": "2.1.3",
"advanced-css-reset": "^2.1.3",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"webextension-polyfill": "^0.12.0"

View File

@ -24,7 +24,7 @@
"lint:fix": "eslint . --ext .ts,.tsx --fix"
},
"dependencies": {
"advanced-css-reset": "2.1.3",
"advanced-css-reset": "^2.1.3",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"webextension-polyfill": "^0.12.0"

View File

@ -1,5 +1,12 @@
import browser from "webextension-polyfill";
import browser from 'webextension-polyfill';
import {ExtensionMessage} from '../types/messages';
browser.runtime.onInstalled.addListener((): void => {
console.log('🦄', 'extension installed');
console.log('extension installed');
});
// Listen for messages from popup or content scripts
browser.runtime.onMessage.addListener((message: unknown): void => {
const msg = message as ExtensionMessage;
console.log('Background received message:', msg.type);
});

View File

@ -1,3 +1,26 @@
console.log('helloworld from content script');
import browser from 'webextension-polyfill';
import {ExtensionMessage, PongMessage} from '../types/messages';
import {getStorage} from '../utils/storage';
export {};
// Listen for messages from popup or background
browser.runtime.onMessage.addListener(
(message: unknown): Promise<PongMessage> | undefined => {
const msg = message as ExtensionMessage;
if (msg.type === 'PING') {
return Promise.resolve({
type: 'PONG',
timestamp: Date.now(),
});
}
return undefined;
}
);
// Log when content script loads (if logging is enabled)
getStorage(['enableLogging']).then(({enableLogging}) => {
if (enableLogging) {
console.log('[Web Extension Starter] Content script loaded on:', window.location.href);
}
});

View File

@ -1,27 +1,81 @@
import * as React from 'react';
import {useEffect, useState} from 'react';
import {getStorage, setStorage} from '../utils/storage';
const Options: React.FC = () => {
const [username, setUsername] = useState('');
const [enableLogging, setEnableLogging] = useState(false);
const [saved, setSaved] = useState(false);
useEffect(() => {
getStorage(['username', 'enableLogging']).then((result) => {
setUsername(result.username);
setEnableLogging(result.enableLogging);
});
}, []);
const handleSave = async (e: React.FormEvent): Promise<void> => {
e.preventDefault();
await setStorage({username, enableLogging});
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
return (
<div>
<form>
<p>
<label htmlFor="username">Your Name</label>
<br />
<div className="options">
<header className="options__header">
<h1>Extension Settings</h1>
<p>Configure your extension preferences</p>
</header>
<form onSubmit={handleSave} className="options__card">
<div className="options__section">
<label htmlFor="username" className="options__label">
Your Name
</label>
<input
type="text"
id="username"
name="username"
className="options__input"
placeholder="Enter your name"
spellCheck="false"
autoComplete="off"
required
value={username}
onChange={(e): void => setUsername(e.target.value)}
/>
</p>
<p>
<label htmlFor="logging">
<input type="checkbox" name="logging" /> Show the features enabled
on each page in the console
</div>
<div className="options__section">
<label
htmlFor="logging"
className="options__checkbox-wrapper"
onClick={(e): void => {
if ((e.target as HTMLElement).tagName !== 'INPUT') {
setEnableLogging(!enableLogging);
}
}}
>
<input
type="checkbox"
name="logging"
id="logging"
className="options__checkbox"
checked={enableLogging}
onChange={(e): void => setEnableLogging(e.target.checked)}
/>
<span className="options__checkbox-text">
Show the features enabled on each page in the console
</span>
</label>
</p>
</div>
<div className="options__actions">
<button type="submit" className="options__button options__button--primary">
Save Settings
</button>
{saved && <span className="options__status">Settings saved</span>}
</div>
</form>
</div>
);

View File

@ -5,4 +5,153 @@
body {
color: variables.$black;
background-color: variables.$greyWhite;
}
min-height: 100vh;
display: flex;
justify-content: center;
padding: 40px 20px;
}
.options {
width: 100%;
max-width: 500px;
&__header {
margin-bottom: 30px;
text-align: center;
h1 {
font-size: 24px;
font-weight: 700;
margin-bottom: 8px;
}
p {
color: variables.$skyBlue;
font-size: 14px;
}
}
&__card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
&__section {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
}
&__label {
display: block;
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
color: variables.$black;
}
&__input {
width: 100%;
padding: 12px 14px;
font-size: 14px;
background-color: white;
color: variables.$black;
border: 1px solid #e0e0e0;
border-radius: 8px;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
&:focus {
outline: none;
border-color: #4a90d9;
box-shadow: 0 0 0 3px rgba(74, 144, 217, 0.15);
}
&::placeholder {
color: #a0a0a0;
}
}
&__checkbox-wrapper {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px;
background: #f8f9fa;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
&:hover {
background: #f0f1f3;
}
}
&__checkbox {
width: 18px;
height: 18px;
margin-top: 2px;
cursor: pointer;
accent-color: #4a90d9;
}
&__checkbox-text {
font-size: 14px;
line-height: 1.5;
color: #444;
}
&__actions {
display: flex;
align-items: center;
gap: 16px;
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #eee;
}
&__button {
padding: 12px 24px;
font-size: 14px;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: opacity 0.2s, transform 0.1s;
&:hover {
opacity: 0.9;
}
&:active {
transform: scale(0.98);
}
&--primary {
background: #4a90d9;
color: white;
}
}
&__status {
font-size: 14px;
color: #2d8a2d;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
&::before {
content: "";
display: inline-block;
width: 8px;
height: 8px;
background: #2d8a2d;
border-radius: 50%;
}
}
}

View File

@ -1,50 +1,103 @@
import * as React from 'react';
import browser, {Tabs} from "webextension-polyfill";
import {useEffect, useState} from 'react';
import browser, {Tabs} from 'webextension-polyfill';
function openWebPage(url: string): Promise<Tabs.Tab> {
return browser.tabs.create({url});
}
interface TabInfo {
title: string;
url: string;
favIconUrl?: string;
}
const Popup: React.FC = () => {
const [tabInfo, setTabInfo] = useState<TabInfo | null>(null);
useEffect(() => {
browser.tabs.query({active: true, currentWindow: true}).then((tabs) => {
const tab = tabs[0];
if (tab) {
setTabInfo({
title: tab.title || 'Unknown',
url: tab.url || 'Unknown',
favIconUrl: tab.favIconUrl,
});
}
});
}, []);
const handleReloadTab = async (): Promise<void> => {
const tabs = await browser.tabs.query({active: true, currentWindow: true});
const tab = tabs[0];
if (tab?.id) {
await browser.tabs.reload(tab.id);
}
};
const getInitial = (title: string): string => {
return title.charAt(0).toUpperCase();
};
return (
<section id="popup">
<h2>WEB-EXTENSION-STARTER</h2>
<button
id="options__button"
type="button"
onClick={(): Promise<Tabs.Tab> => {
return openWebPage('/Options/options.html');
}}
>
Options Page
</button>
<div className="links__holder">
<ul>
<li>
<button
type="button"
onClick={(): Promise<Tabs.Tab> => {
return openWebPage(
'https://github.com/abhijithvijayan/web-extension-starter'
);
}}
>
GitHub
</button>
</li>
<li>
<button
type="button"
onClick={(): Promise<Tabs.Tab> => {
return openWebPage(
'https://www.buymeacoffee.com/abhijithvijayan'
);
}}
>
Buy Me A Coffee
</button>
</li>
</ul>
<section className="popup">
<header className="popup__header">
<h1 className="popup__title">Web Extension Starter</h1>
</header>
{tabInfo && (
<div className="popup__card">
<div className="popup__card-header">
<span className="popup__card-title">Current Tab</span>
</div>
<div className="popup__tab-content">
{tabInfo.favIconUrl ? (
<img src={tabInfo.favIconUrl} alt="" className="popup__favicon" />
) : (
<div className="popup__favicon-placeholder">{getInitial(tabInfo.title)}</div>
)}
<div className="popup__tab-details">
<p className="popup__tab-title">{tabInfo.title}</p>
<p className="popup__tab-url">{tabInfo.url}</p>
</div>
</div>
<button
type="button"
onClick={handleReloadTab}
className="popup__btn popup__btn--secondary"
>
Reload Tab
</button>
</div>
)}
<div className="popup__footer">
<button
type="button"
className="popup__footer-btn popup__footer-btn--settings"
onClick={(): Promise<Tabs.Tab> => openWebPage('/Options/options.html')}
>
Settings
</button>
<button
type="button"
className="popup__footer-btn popup__footer-btn--github"
onClick={(): Promise<Tabs.Tab> =>
openWebPage('https://github.com/abhijithvijayan/web-extension-starter')
}
>
GitHub
</button>
<button
type="button"
className="popup__footer-btn popup__footer-btn--support"
onClick={(): Promise<Tabs.Tab> =>
openWebPage('https://www.buymeacoffee.com/abhijithvijayan')
}
>
Support
</button>
</div>
</section>
);

View File

@ -8,50 +8,167 @@ body {
width: 100%;
}
::placeholder {
color: blue;
}
.popup {
width: 380px;
padding: 20px;
#popup {
min-width: 350px;
padding: 30px 20px;
h2 {
font-size: 25px;
&__header {
text-align: center;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #eee;
}
#options__button {
width: 50%;
background: green;
color: white;
font-weight: 500;
border-radius: 15px;
padding: 5px 10px;
justify-content: center;
margin: 20px auto;
cursor: pointer;
opacity: 0.8;
&__title {
font-size: 16px;
font-weight: 700;
letter-spacing: 0.5px;
color: variables.$black;
}
&__card {
background: white;
border-radius: 10px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
&__card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.links__holder {
ul {
display: flex;
margin-top: 1em;
justify-content: space-around;
&__card-title {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: variables.$skyBlue;
}
li {
button {
border-radius: 25px;
font-size: 20px;
font-weight: 600;
padding: 10px 17px;
background-color: rgba(0, 0, 255, 0.7);
color: white;
cursor: pointer;
}
&__tab-content {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 14px;
}
&__favicon {
width: 40px;
height: 40px;
border-radius: 8px;
flex-shrink: 0;
background: #f0f0f0;
object-fit: cover;
}
&__favicon-placeholder {
width: 40px;
height: 40px;
border-radius: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
font-weight: 700;
flex-shrink: 0;
}
&__tab-details {
flex: 1;
min-width: 0;
}
&__tab-title {
font-size: 14px;
font-weight: 600;
color: variables.$black;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__tab-url {
font-size: 12px;
color: variables.$skyBlue;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__btn {
width: 100%;
padding: 10px 14px;
font-size: 13px;
font-weight: 500;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
&:hover {
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
&--secondary {
background: #f0f1f3;
color: #444;
&:hover {
background: #e4e5e7;
}
}
}
}
&__footer {
display: flex;
gap: 8px;
margin-top: 4px;
}
&__footer-btn {
flex: 1;
padding: 12px;
font-size: 13px;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
&:hover {
transform: translateY(-1px);
}
&--settings {
background: #4a90d9;
color: white;
}
&--github {
background: #24292e;
color: white;
}
&--support {
background: #ff813f;
color: white;
}
}
}

View File

@ -1,10 +1,13 @@
@import '~/advanced-css-reset/dist/reset.min.css';
// Add your custom reset rules here
// Override dark mode color scheme that causes dark inputs
:root {
color-scheme: light;
}
* {
margin: 0;
padding: 0;
border: 0;
outline: 0;
}
// Reset removes borders, restore for form elements
input,
textarea,
select {
border: 1px solid #ccc;
}

10
source/types/messages.ts Normal file
View File

@ -0,0 +1,10 @@
export interface PingMessage {
type: 'PING';
}
export interface PongMessage {
type: 'PONG';
timestamp: number;
}
export type ExtensionMessage = PingMessage | PongMessage;

9
source/types/storage.ts Normal file
View File

@ -0,0 +1,9 @@
export interface StorageSchema {
username: string;
enableLogging: boolean;
}
export const defaultStorage: StorageSchema = {
username: '',
enableLogging: false,
};

30
source/utils/storage.ts Normal file
View File

@ -0,0 +1,30 @@
import browser from 'webextension-polyfill';
import {StorageSchema, defaultStorage} from '../types/storage';
export async function getStorage<K extends keyof StorageSchema>(
keys: K[]
): Promise<Pick<StorageSchema, K>> {
const result = await browser.storage.local.get(keys);
const output = {} as Pick<StorageSchema, K>;
for (const key of keys) {
output[key] = (result[key] as StorageSchema[K]) ?? defaultStorage[key];
}
return output;
}
export async function setStorage<K extends keyof StorageSchema>(
items: Pick<StorageSchema, K>
): Promise<void> {
await browser.storage.local.set(items);
}
export async function getAllStorage(): Promise<StorageSchema> {
const result = await browser.storage.local.get(null);
return {
...defaultStorage,
...result,
};
}