Initial commit — basic tab grouping works

This commit is contained in:
Lukas Bauer 2025-06-12 20:00:00 +00:00
commit f0f3a7e4ea
6 changed files with 137 additions and 0 deletions

21
README.md Normal file
View file

@ -0,0 +1,21 @@
# tab-tidy
A small browser extension that groups and sorts your open tabs by domain.
Built for Firefox. Side project — scratching my own itch.
## Install (dev)
npm install
npm run build
# Load dist/ as a temporary extension in about:debugging
## Features
- Groups tabs by domain
- Sorts groups alphabetically or by last-used
- One-click close for all tabs in a group
- Keyboard shortcut: Alt+T to open the popup
## Status
Works for me™. Not on the extension store yet.

21
manifest.json Normal file
View file

@ -0,0 +1,21 @@
{
"manifest_version": 3,
"name": "Tab Tidy",
"version": "0.3.1",
"description": "Group and sort your tabs by domain",
"permissions": ["tabs"],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png"
}
},
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Alt+T"
}
}
}
}

16
package.json Normal file
View file

@ -0,0 +1,16 @@
{
"name": "tab-tidy",
"version": "0.3.1",
"description": "Group and sort browser tabs by domain",
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "webpack --config webpack.config.js --watch",
"lint": "eslint src/"
},
"devDependencies": {
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"eslint": "^8.57.0",
"copy-webpack-plugin": "^12.0.2"
}
}

18
popup.html Normal file
View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tab Tidy</title>
<style>
body { font-family: system-ui; width: 320px; padding: 8px; }
h2 { font-size: 0.85rem; margin: 8px 0 4px; color: #555; display: flex; justify-content: space-between; }
button { font-size: 0.7rem; cursor: pointer; border: none; background: #eee; padding: 2px 6px; border-radius: 3px; }
.tab-item { font-size: 0.8rem; padding: 3px 6px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tab-item:hover { background: #f0f0f0; }
</style>
</head>
<body>
<div id="groups"></div>
<script src="src/popup.js"></script>
</body>
</html>

12
src/background.js Normal file
View file

@ -0,0 +1,12 @@
// Background service worker
// Listens for tab updates and triggers a re-group
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (changeInfo.status === 'complete') {
chrome.runtime.sendMessage({ type: 'TABS_CHANGED' }).catch(() => {});
}
});
chrome.tabs.onRemoved.addListener(() => {
chrome.runtime.sendMessage({ type: 'TABS_CHANGED' }).catch(() => {});
});

49
src/popup.js Normal file
View file

@ -0,0 +1,49 @@
const groupByDomain = (tabs) => {
return tabs.reduce((groups, tab) => {
try {
const domain = new URL(tab.url).hostname;
if (!groups[domain]) groups[domain] = [];
groups[domain].push(tab);
} catch (_) {}
return groups;
}, {});
};
const render = async () => {
const tabs = await chrome.tabs.query({ currentWindow: true });
const groups = groupByDomain(tabs);
const sorted = Object.entries(groups).sort(([a], [b]) => a.localeCompare(b));
const container = document.getElementById('groups');
container.innerHTML = '';
for (const [domain, domainTabs] of sorted) {
const section = document.createElement('section');
const header = document.createElement('h2');
header.textContent = `${domain} (${domainTabs.length})`;
const closeBtn = document.createElement('button');
closeBtn.textContent = 'close all';
closeBtn.onclick = () => {
chrome.tabs.remove(domainTabs.map(t => t.id)).then(render);
};
header.appendChild(closeBtn);
section.appendChild(header);
for (const tab of domainTabs) {
const item = document.createElement('div');
item.className = 'tab-item';
item.textContent = tab.title || tab.url;
item.title = tab.url;
item.onclick = () => chrome.tabs.update(tab.id, { active: true });
section.appendChild(item);
}
container.appendChild(section);
}
};
document.addEventListener('DOMContentLoaded', render);
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === 'TABS_CHANGED') render();
});