Merge dev/initial-build: Outcut v1.0.0

This commit is contained in:
Joel Brock
2026-04-27 14:56:00 -07:00
21 changed files with 3709 additions and 0 deletions

29
PRIVACY.md Normal file
View File

@@ -0,0 +1,29 @@
# Outcut Privacy Policy
Outcut is a browser extension that adds keyboard shortcuts to Outlook Web App.
## Data Collection
Outcut does **not** collect, transmit, or store any personal data.
## Storage
Outcut stores your preferences (keyboard shortcut preset selection and toggle states) using Chrome's built-in `chrome.storage.sync` API. This data:
- Stays on your device and within your Chrome profile
- Syncs across your Chrome instances if you use Chrome Sync (a Google feature, not ours)
- Is never sent to any external server
## Permissions
- **storage**: Save your extension preferences locally
- No other permissions are required
## Email Content
Outcut never reads, processes, stores, or transmits the content of your emails. It only interacts with the Outlook Web App's user interface elements (buttons, message list items) to provide keyboard shortcut functionality.
## Contact
For questions about this privacy policy, open an issue on the project's GitHub repository.
*Last updated: April 2026*

62
README.md Normal file
View File

@@ -0,0 +1,62 @@
# Outcut
Keyboard shortcuts for Outlook Web App — Gmail-style multi-select, delete, archive, and more.
## Features
- **Gmail or Outlook shortcut presets** — choose your preferred key bindings
- **Multi-select** — select multiple messages and act on them at once
- **Batch actions** — delete, archive, flag, pin, move, read/unread on selected messages
- **Works everywhere** — outlook.office.com, outlook.cloud.microsoft, and Outlook PWA
### Gmail Preset (default)
| Key | Action |
|-----|--------|
| j / Down | Next message |
| k / Up | Previous message |
| x / Space | Toggle select |
| Shift+Down/Up | Extend selection |
| # | Delete |
| e | Archive |
| Shift+i / Shift+u | Toggle read/unread |
| v | Move to folder |
| f | Flag/unflag |
| p | Pin/unpin |
| Escape | Deselect all |
| Enter / o | Open message |
### Outlook Preset
| Key | Action |
|-----|--------|
| Down / j | Next message |
| Up / k | Previous message |
| Space | Toggle select |
| Shift+Down/Up | Extend selection |
| Delete / Backspace | Delete |
| e | Archive |
| q / Shift+i | Toggle read/unread |
| v | Move to folder |
| f / Insert | Flag/unflag |
| p | Pin/unpin |
| Escape | Deselect all |
| Enter | Open message |
## Install
### From Chrome Web Store
Coming soon.
### Development
1. Clone this repo
2. Open `chrome://extensions` in Chrome
3. Enable "Developer mode"
4. Click "Load unpacked" and select this directory
5. Open Outlook Web App — the extension activates automatically
## Usage
Click the extension icon to choose your shortcut preset (Gmail or Outlook) and toggle keyboard navigation on/off.
## Privacy
Outcut stores only your preferences locally. No data is collected or transmitted. See [PRIVACY.md](PRIVACY.md).

292
content/behavior.js Normal file
View File

@@ -0,0 +1,292 @@
// Outlook Relook — Behavior Patches
// JS-based UX improvements, each gated by a setting key.
window.OutlookRelook = window.OutlookRelook || {};
window.OutlookRelook.Behavior = (function () {
'use strict';
var OR = window.OutlookRelook;
var currentSettings = {};
var cleanupFns = [];
// --- Auto-collapse ribbon on page load ---
function setupAutoCollapseRibbon() {
if (!currentSettings.autoCollapseRibbon) return;
// Wait for OWA to finish rendering, then collapse the ribbon
var timer = setTimeout(function () {
var elements = OR.resolveSelector('ribbon-collapse-button');
for (var i = 0; i < elements.length; i++) {
if (elements[i].getAttribute('aria-expanded') === 'true') {
elements[i].click();
console.log('[Outcut] Auto-collapsed ribbon');
}
}
}, 2000);
cleanupFns.push(function () { clearTimeout(timer); });
}
// --- Remember sidebar collapsed/expanded state ---
function setupRememberSidebar() {
if (!currentSettings.rememberSidebarState) return;
chrome.storage.local.get({ sidebarCollapsed: false }, function (result) {
if (result.sidebarCollapsed) {
var timer = setTimeout(function () {
var pane = OR.resolveSelector('folder-pane');
for (var i = 0; i < pane.length; i++) {
var toggle = pane[i].closest('[aria-expanded]') || pane[i].querySelector('[aria-expanded]');
if (toggle && toggle.getAttribute('aria-expanded') === 'true') {
toggle.click();
}
}
}, 2000);
cleanupFns.push(function () { clearTimeout(timer); });
}
});
// Watch for sidebar toggle changes
var sidebarObserver = new MutationObserver(function () {
var pane = OR.resolveSelector('folder-pane');
if (pane.length > 0) {
var isVisible = pane[0].offsetWidth > 50;
chrome.storage.local.set({ sidebarCollapsed: !isVisible });
}
});
var timer2 = setTimeout(function () {
var pane = OR.resolveSelector('folder-pane');
if (pane.length > 0 && pane[0].parentElement) {
sidebarObserver.observe(pane[0].parentElement, { attributes: true, subtree: true });
}
}, 3000);
cleanupFns.push(function () {
clearTimeout(timer2);
sidebarObserver.disconnect();
});
}
// --- Suppress contact card hover popups ---
function setupSuppressContactHover() {
if (!currentSettings.suppressContactHover) return;
var handler = function (e) {
// Check if the hovered element or its ancestors trigger a contact card
var target = e.target.closest('[data-lpc-hover-target], [aria-haspopup="dialog"]');
if (target) {
e.stopPropagation();
e.preventDefault();
}
};
document.addEventListener('mouseenter', handler, true);
cleanupFns.push(function () { document.removeEventListener('mouseenter', handler, true); });
}
// --- Auto-advance to next email after delete ---
function setupAutoAdvance() {
if (!currentSettings.autoAdvanceAfterDelete) return;
var handler = function (e) {
// Detect delete key press
if (e.key === 'Delete' || e.key === 'Backspace') {
var selected = document.querySelector(
'[role="option"][aria-selected="true"], [role="listitem"][aria-selected="true"]'
);
if (selected) {
var next = selected.nextElementSibling;
if (next) {
// Wait for OWA to process the delete, then focus next
setTimeout(function () {
next.click();
next.focus();
}, 200);
}
}
}
};
document.addEventListener('keydown', handler, true);
cleanupFns.push(function () { document.removeEventListener('keydown', handler, true); });
}
// --- Auto-dismiss notification toasts ---
function setupAutoDismissToasts() {
if (currentSettings.autoDismissToasts === 'off') return;
var delay = parseInt(currentSettings.autoDismissToasts, 10) * 1000;
if (isNaN(delay) || delay <= 0) return;
var toastObserver = new MutationObserver(function (mutations) {
for (var m = 0; m < mutations.length; m++) {
var addedNodes = mutations[m].addedNodes;
for (var n = 0; n < addedNodes.length; n++) {
var node = addedNodes[n];
if (node.nodeType !== 1) continue;
var toasts = [];
if (node.matches && node.matches('[role="alert"], [role="status"][aria-live]')) {
toasts.push(node);
}
if (node.querySelectorAll) {
var found = node.querySelectorAll('[role="alert"], [role="status"][aria-live]');
for (var t = 0; t < found.length; t++) toasts.push(found[t]);
}
for (var i = 0; i < toasts.length; i++) {
(function (toast) {
setTimeout(function () {
// Try to find a dismiss button
var dismiss = toast.querySelector('[aria-label*="Close" i], [aria-label*="Dismiss" i]');
if (dismiss) {
dismiss.click();
} else {
toast.style.display = 'none';
}
}, delay);
})(toasts[i]);
}
}
}
});
toastObserver.observe(document.body, { childList: true, subtree: true });
cleanupFns.push(function () { toastObserver.disconnect(); });
}
// --- Reposition toast notifications ---
function setupToastPosition() {
if (currentSettings.toastPosition === 'bottom-left') return; // OWA default
var style = document.createElement('style');
style.id = 'or-toast-position';
style.textContent = [
'[role="alert"], [role="status"][aria-live] {',
' position: fixed !important;',
' top: 8px !important;',
' right: 8px !important;',
' bottom: auto !important;',
' left: auto !important;',
' z-index: 999999 !important;',
'}'
].join('\n');
document.head.appendChild(style);
cleanupFns.push(function () { style.remove(); });
}
// --- Sticky Reply/Forward bar ---
function setupStickyReplyBar() {
if (!currentSettings.stickyReplyBar) return;
var style = document.createElement('style');
style.id = 'or-sticky-reply';
style.textContent = [
'[aria-label*="Reply all" i][role="button"],',
'[aria-label*="Reply" i][role="button"],',
'[aria-label*="Forward" i][role="button"] {',
' position: sticky !important;',
' bottom: 0 !important;',
' z-index: 10 !important;',
' background-color: var(--or-bg-primary, #fff) !important;',
'}'
].join('\n');
document.head.appendChild(style);
cleanupFns.push(function () { style.remove(); });
}
// --- Auto-resize compose window ---
function setupAutoResizeCompose() {
if (!currentSettings.autoResizeCompose) return;
var composeObserver = new MutationObserver(function (mutations) {
for (var m = 0; m < mutations.length; m++) {
var addedNodes = mutations[m].addedNodes;
for (var n = 0; n < addedNodes.length; n++) {
var node = addedNodes[n];
if (node.nodeType !== 1) continue;
var composeWindows = [];
if (node.matches && node.matches('[aria-label*="compose" i][role="dialog"], [aria-label*="New message" i]')) {
composeWindows.push(node);
}
if (node.querySelectorAll) {
var found = node.querySelectorAll('[aria-label*="compose" i][role="dialog"], [aria-label*="New message" i]');
for (var i = 0; i < found.length; i++) composeWindows.push(found[i]);
}
for (var w = 0; w < composeWindows.length; w++) {
composeWindows[w].style.minHeight = '60vh';
composeWindows[w].style.height = '60vh';
console.log('[Outcut] Auto-resized compose window');
}
}
}
});
composeObserver.observe(document.body, { childList: true, subtree: true });
cleanupFns.push(function () { composeObserver.disconnect(); });
}
// --- Throttle desktop notifications ---
function setupThrottleNotifications() {
if (!currentSettings.throttleNotifications) return;
var OrigNotification = window.Notification;
// Only patch if Notification API exists
if (!OrigNotification) return;
var lastNotificationTime = 0;
var MIN_INTERVAL = 30000; // 30 seconds between notifications
window.Notification = function (title, options) {
var now = Date.now();
if (now - lastNotificationTime < MIN_INTERVAL) {
console.log('[Outcut] Throttled notification: "' + title + '"');
return {};
}
lastNotificationTime = now;
return new OrigNotification(title, options);
};
window.Notification.permission = OrigNotification.permission;
window.Notification.requestPermission = OrigNotification.requestPermission.bind(OrigNotification);
cleanupFns.push(function () {
window.Notification = OrigNotification;
});
}
// --- Public API ---
function start(settings) {
currentSettings = settings;
setupAutoCollapseRibbon();
setupRememberSidebar();
setupSuppressContactHover();
setupAutoAdvance();
setupAutoDismissToasts();
setupToastPosition();
setupStickyReplyBar();
setupAutoResizeCompose();
setupThrottleNotifications();
console.log('[Outcut] Behavior patches applied');
}
function updateSettings(settings) {
// Tear down existing behaviors and re-apply
stop();
currentSettings = settings;
start(settings);
}
function stop() {
for (var i = 0; i < cleanupFns.length; i++) {
try { cleanupFns[i](); } catch (e) { /* ignore */ }
}
cleanupFns = [];
}
return { start: start, updateSettings: updateSettings, stop: stop };
})();

165
content/content.js Normal file
View File

@@ -0,0 +1,165 @@
// Outlook Relook — Content Script Entry Point
//
// Primary feature: Keyboard navigation (always active)
// Experimental: Design tweaks (themes, density, hiding, behavior) — gated by enableDesignTweaks
(function () {
'use strict';
const OR = window.OutlookRelook;
// Map setting keys to data attributes on <html> (design tweaks only)
const SETTING_TO_ATTR = {
compactTopBar: 'data-or-compact-topbar',
compactCommandBar: 'data-or-compact-commandbar',
compactMessageList: 'data-or-compact-messagelist',
compactReadingPane: 'data-or-compact-readingpane',
compactFolderPane: 'data-or-compact-folderpane',
narrowDateColumn: 'data-or-narrow-datecol',
compressComposeToolbar:'data-or-compact-compose',
readingPaneMaxWidth: 'data-or-reading-maxwidth',
unifiedHeader: 'data-or-unified-header',
hideCopilot: 'data-or-hide-copilot',
hideSuggestedReplies: 'data-or-hide-suggestedreplies',
hidePromoBanners: 'data-or-hide-promos',
hideFocusedOtherTabs: 'data-or-hide-focusedtabs',
hideSidebarAppIcons: 'data-or-hide-sidebaricons',
hideGroupsSection: 'data-or-hide-groups',
hideMyDayButtons: 'data-or-hide-myday',
hideSenderAvatars: 'data-or-hide-avatars',
hideFeatureDiscovery: 'data-or-hide-discovery',
hideVivaInsights: 'data-or-hide-viva',
hideUnreadOtherBanner: 'data-or-hide-unreadother',
hideActivityFeed: 'data-or-hide-activity',
unreadDistinction: 'data-or-unread-distinction',
previewOwnLine: 'data-or-preview-own-line',
normalizeFontWeight: 'data-or-normalize-font',
darkModeEmailFix: 'data-or-darkmode-fix',
};
const SETTING_TO_ATTR_VALUE = {
messageListFontSize: 'data-or-fontsize',
};
let designTweaksActive = false;
function applyColorScheme(scheme) {
let resolved = scheme;
if (scheme === 'system') {
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
document.documentElement.setAttribute('data-outlook-relook-scheme', resolved);
}
function applySettingsToDOM(settings) {
for (const [key, attr] of Object.entries(SETTING_TO_ATTR)) {
document.documentElement.setAttribute(attr, String(!!settings[key]));
}
for (const [key, attr] of Object.entries(SETTING_TO_ATTR_VALUE)) {
document.documentElement.setAttribute(attr, settings[key] || '');
}
applyColorScheme(settings.colorScheme);
if (settings.accentColor) {
document.documentElement.style.setProperty('--or-accent-override', settings.accentColor);
}
console.log('[Outcut] Settings applied to DOM');
}
function clearDesignFromDOM() {
// Remove all data-or-* attributes
for (const attr of Object.values(SETTING_TO_ATTR)) {
document.documentElement.removeAttribute(attr);
}
for (const attr of Object.values(SETTING_TO_ATTR_VALUE)) {
document.documentElement.removeAttribute(attr);
}
document.documentElement.removeAttribute('data-outlook-relook-scheme');
document.documentElement.style.removeProperty('--or-accent-override');
// Remove injected theme CSS
var themeLink = document.getElementById('outlook-relook-theme');
if (themeLink) themeLink.remove();
console.log('[Outcut] Design tweaks cleared from DOM');
}
function injectThemeCSS(theme) {
var existing = document.getElementById('outlook-relook-theme');
if (existing) existing.remove();
var link = document.createElement('link');
link.id = 'outlook-relook-theme';
link.rel = 'stylesheet';
link.href = chrome.runtime.getURL('themes/' + theme + '.css');
document.head.appendChild(link);
console.log('[Outcut] Theme loaded: ' + theme);
}
function startDesignTweaks(settings) {
applySettingsToDOM(settings);
injectThemeCSS(settings.theme);
OR.Observer.start(settings);
OR.Behavior.start(settings);
OR.Injector.start(settings);
designTweaksActive = true;
console.log('[Outcut] Design tweaks enabled');
}
function stopDesignTweaks() {
OR.Observer.stop();
OR.Behavior.stop();
OR.Injector.stop();
clearDesignFromDOM();
designTweaksActive = false;
console.log('[Outcut] Design tweaks disabled');
}
async function init() {
const settings = await OR.loadSettings();
console.log('[Outcut] Loaded settings:', settings);
// Keyboard navigation — always starts (gated by its own toggle internally)
OR.Keyboard.start(settings);
// Design tweaks — only if enabled
if (settings.enableDesignTweaks) {
startDesignTweaks(settings);
}
// Listen for setting changes from popup
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'sync') return;
OR.loadSettings().then((updated) => {
// Keyboard — always update
OR.Keyboard.updateSettings(updated);
// Design tweaks — toggle on/off or update
if (updated.enableDesignTweaks) {
if (!designTweaksActive) {
startDesignTweaks(updated);
} else {
applySettingsToDOM(updated);
OR.Observer.updateSettings(updated);
OR.Behavior.updateSettings(updated);
OR.Injector.updateSettings(updated);
if (changes.theme) {
injectThemeCSS(changes.theme.newValue);
}
}
} else if (designTweaksActive) {
stopDesignTweaks();
}
});
});
}
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', async () => {
const settings = await OR.loadSettings();
if (settings.enableDesignTweaks && settings.colorScheme === 'system') {
applyColorScheme('system');
}
});
init();
})();

264
content/injector.js Normal file
View File

@@ -0,0 +1,264 @@
// Outlook Relook — DOM Injector
// Injects custom UI elements: mark-all-read button, folder jump dialog.
window.OutlookRelook = window.OutlookRelook || {};
window.OutlookRelook.Injector = (function () {
'use strict';
var OR = window.OutlookRelook;
var currentSettings = {};
var cleanupFns = [];
// --- "Mark all as read" button ---
function setupMarkAllRead() {
if (!currentSettings.markAllReadButton) return;
function injectButton() {
// Find folder headers that don't already have our button
var headers = OR.resolveSelector('folder-header');
for (var i = 0; i < headers.length; i++) {
var header = headers[i];
if (header.querySelector('.or-mark-all-read')) continue;
var btn = document.createElement('button');
btn.className = 'or-mark-all-read';
btn.textContent = '\u2713\u2713'; // double checkmark
btn.title = 'Mark all as read';
btn.style.cssText = [
'background: none;',
'border: 1px solid var(--or-border, #ccc);',
'color: var(--or-text-secondary, #666);',
'cursor: pointer;',
'font-size: 11px;',
'padding: 2px 6px;',
'margin-left: 8px;',
'border-radius: 3px;',
'line-height: 1;',
'vertical-align: middle;'
].join(' ');
btn.addEventListener('click', (function (hdr) {
return function (e) {
e.stopPropagation();
// Find the context menu "Mark all as read" option
var folder = hdr.closest('[role="treeitem"]');
if (folder) {
// Dispatch a contextmenu event to open OWA's context menu
var rect = folder.getBoundingClientRect();
var contextEvent = new MouseEvent('contextmenu', {
bubbles: true,
cancelable: true,
clientX: rect.x + 10,
clientY: rect.y + 10,
});
folder.dispatchEvent(contextEvent);
// Wait for context menu to render, then find and click "Mark all as read"
setTimeout(function () {
var menuItems = document.querySelectorAll('[role="menuitem"]');
for (var j = 0; j < menuItems.length; j++) {
if (/mark all as read/i.test(menuItems[j].textContent)) {
menuItems[j].click();
console.log('[Outcut] Marked all as read');
return;
}
}
// Close context menu if option not found
document.body.click();
console.warn('[Outcut] "Mark all as read" menu item not found');
}, 300);
}
};
})(header));
header.appendChild(btn);
}
}
// Inject on load and watch for new headers
var timer = setTimeout(injectButton, 3000);
var injectObserver = new MutationObserver(function () { injectButton(); });
injectObserver.observe(document.body, { childList: true, subtree: true });
cleanupFns.push(function () {
clearTimeout(timer);
injectObserver.disconnect();
var btns = document.querySelectorAll('.or-mark-all-read');
for (var k = 0; k < btns.length; k++) btns[k].remove();
});
}
// --- Quick folder jump (Ctrl+Shift+K) ---
function setupFolderJump() {
if (!currentSettings.quickFolderJump) return;
var dialog = null;
function createDialog() {
var overlay = document.createElement('div');
overlay.id = 'or-folder-jump';
overlay.style.cssText = [
'position: fixed;',
'top: 0; left: 0; right: 0; bottom: 0;',
'background: rgba(0,0,0,0.4);',
'z-index: 999999;',
'display: flex;',
'align-items: flex-start;',
'justify-content: center;',
'padding-top: 20vh;'
].join(' ');
var box = document.createElement('div');
box.style.cssText = [
'background: var(--or-bg-primary, #fff);',
'border: 1px solid var(--or-border, #ccc);',
'border-radius: 8px;',
'padding: 12px;',
'width: 400px;',
'max-height: 400px;',
'box-shadow: 0 8px 32px rgba(0,0,0,0.2);',
'font-family: inherit;'
].join(' ');
var input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Jump to folder...';
input.style.cssText = [
'width: 100%;',
'padding: 8px 12px;',
'border: 1px solid var(--or-border, #ccc);',
'border-radius: 4px;',
'font-size: 14px;',
'outline: none;',
'box-sizing: border-box;',
'background: var(--or-bg-secondary, #f5f5f5);',
'color: var(--or-text-primary, #000);'
].join(' ');
var results = document.createElement('div');
results.style.cssText = 'margin-top: 8px; max-height: 300px; overflow-y: auto;';
input.addEventListener('input', function () {
var query = input.value.toLowerCase().trim();
// Clear results using safe DOM methods
while (results.firstChild) {
results.removeChild(results.firstChild);
}
if (!query) return;
// Find all folder tree items
var folders = document.querySelectorAll('[role="treeitem"]');
var matches = [];
for (var i = 0; i < folders.length; i++) {
var name = (folders[i].textContent || '').trim();
if (name.toLowerCase().indexOf(query) !== -1) {
matches.push({ name: name, element: folders[i] });
}
}
var limit = Math.min(matches.length, 10);
for (var j = 0; j < limit; j++) {
(function (match) {
var item = document.createElement('div');
item.textContent = match.name;
item.style.cssText = [
'padding: 6px 12px;',
'cursor: pointer;',
'border-radius: 4px;',
'color: var(--or-text-primary, #000);'
].join(' ');
item.addEventListener('mouseenter', function () {
item.style.backgroundColor = 'var(--or-bg-hover, #e0e0e0)';
});
item.addEventListener('mouseleave', function () {
item.style.backgroundColor = '';
});
item.addEventListener('click', function () {
match.element.click();
closeDialog();
});
results.appendChild(item);
})(matches[j]);
}
});
box.appendChild(input);
box.appendChild(results);
overlay.appendChild(box);
overlay.addEventListener('click', function (e) {
if (e.target === overlay) closeDialog();
});
input.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeDialog();
if (e.key === 'Enter') {
var firstResult = results.querySelector('div');
if (firstResult) firstResult.click();
}
});
return { overlay: overlay, input: input };
}
function openDialog() {
if (dialog) return;
var created = createDialog();
document.body.appendChild(created.overlay);
dialog = created.overlay;
setTimeout(function () { created.input.focus(); }, 50);
}
function closeDialog() {
if (dialog) {
dialog.remove();
dialog = null;
}
}
var handler = function (e) {
// Ctrl+Shift+K (or Cmd+Shift+K on Mac)
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'K') {
e.preventDefault();
e.stopPropagation();
if (dialog) {
closeDialog();
} else {
openDialog();
}
}
};
document.addEventListener('keydown', handler, true);
cleanupFns.push(function () {
document.removeEventListener('keydown', handler, true);
closeDialog();
});
}
// --- Public API ---
function start(settings) {
currentSettings = settings;
setupMarkAllRead();
setupFolderJump();
console.log('[Outcut] Injector started');
}
function updateSettings(settings) {
stop();
currentSettings = settings;
start(settings);
}
function stop() {
for (var i = 0; i < cleanupFns.length; i++) {
try { cleanupFns[i](); } catch (e) { /* ignore */ }
}
cleanupFns = [];
}
return { start: start, updateSettings: updateSettings, stop: stop };
})();

537
content/keyboard.js Normal file
View File

@@ -0,0 +1,537 @@
// Outcut — Keyboard Navigation & Multi-Select
// Adds keyboard focus cursor and multi-select to OWA's message list.
// Gated by the keyboardMultiSelect setting.
//
// Tracks selections by stable message ID (data attribute or aria-label)
// rather than DOM element reference, since OWA frequently re-renders rows.
window.OutlookRelook = window.OutlookRelook || {};
// Keyboard shortcut presets
var KEY_PRESETS = {
gmail: {
nextMessage: [{key: 'j'}, {key: 'ArrowDown'}],
prevMessage: [{key: 'k'}, {key: 'ArrowUp'}],
selectExtendDown: [{key: 'j', shift: true}, {key: 'ArrowDown', shift: true}],
selectExtendUp: [{key: 'k', shift: true}, {key: 'ArrowUp', shift: true}],
toggleSelect: [{key: 'x'}, {key: ' '}],
delete: [{key: '#'}],
archive: [{key: 'e'}],
readUnread: [{key: 'I', shift: true}, {key: 'U', shift: true}],
move: [{key: 'v'}],
flag: [{key: 'f'}],
pin: [{key: 'p'}],
deselect: [{key: 'Escape'}],
open: [{key: 'Enter'}, {key: 'o'}],
label: 'Gmail-style: j/k nav, x select, # del, e archive'
},
outlook: {
nextMessage: [{key: 'ArrowDown'}, {key: 'j'}],
prevMessage: [{key: 'ArrowUp'}, {key: 'k'}],
selectExtendDown: [{key: 'ArrowDown', shift: true}],
selectExtendUp: [{key: 'ArrowUp', shift: true}],
toggleSelect: [{key: ' '}],
delete: [{key: 'Delete'}, {key: 'Backspace'}],
archive: [{key: 'e'}],
readUnread: [{key: 'q'}, {key: 'I', shift: true}],
move: [{key: 'v'}],
flag: [{key: 'Insert'}, {key: 'f'}],
pin: [{key: 'p'}],
deselect: [{key: 'Escape'}],
open: [{key: 'Enter'}],
label: 'Outlook-style: arrows nav, Space select, Del delete'
}
};
function matchesAction(e, actionBindings) {
if (!actionBindings) return false;
for (var i = 0; i < actionBindings.length; i++) {
var b = actionBindings[i];
if (e.key === b.key && !!e.shiftKey === !!b.shift && !!e.ctrlKey === !!b.ctrl) {
return true;
}
}
return false;
}
window.OutlookRelook.Keyboard = (function () {
'use strict';
var OR = window.OutlookRelook;
var currentSettings = {};
var cleanupFns = [];
// State — track by ID strings, not DOM references
var focusedId = null; // ID of the focused message
var selectedIds = new Set(); // Set of selected message IDs
var countBadge = null;
// --- Message ID extraction ---
// OWA messages have various attributes we can use as stable IDs.
// Try data-convid, data-itemid, id, or fall back to aria-label.
function getMessageId(el) {
return el.getAttribute('data-convid')
|| el.getAttribute('data-itemid')
|| el.getAttribute('data-tid')
|| el.getAttribute('id')
|| el.getAttribute('aria-label')
|| null;
}
function getMessageItems() {
var items = document.querySelectorAll(
'[role="listbox"] [role="option"], [role="list"] [role="listitem"]'
);
return Array.from(items);
}
function findItemById(items, id) {
if (!id) return -1;
for (var i = 0; i < items.length; i++) {
if (getMessageId(items[i]) === id) return i;
}
return -1;
}
function isComposeOrDialogActive() {
var active = document.activeElement;
if (!active) return false;
var tag = active.tagName.toLowerCase();
if (tag === 'input' || tag === 'textarea') return true;
if (active.getAttribute('contenteditable') === 'true') return true;
if (active.closest('[role="dialog"]')) return true;
if (active.closest('[role="search"]')) return true;
if (active.closest('[aria-label*="compose" i]')) return true;
if (active.closest('[aria-label*="New message" i]')) return true;
return false;
}
// --- Visual state application ---
// Re-applies focus and selection classes to current DOM elements
// based on the ID-based state. Called after every key action and
// by the MutationObserver when OWA re-renders.
function applyVisualState(items) {
// Clear all visual markers first
var oldFocused = document.querySelectorAll('.or-kb-focused');
for (var f = 0; f < oldFocused.length; f++) oldFocused[f].classList.remove('or-kb-focused');
var oldSelected = document.querySelectorAll('.or-kb-selected');
for (var s = 0; s < oldSelected.length; s++) oldSelected[s].classList.remove('or-kb-selected');
if (!items) items = getMessageItems();
// Apply focus
if (focusedId) {
var focusIdx = findItemById(items, focusedId);
if (focusIdx >= 0) {
items[focusIdx].classList.add('or-kb-focused');
}
}
// Apply selections
selectedIds.forEach(function (id) {
var idx = findItemById(items, id);
if (idx >= 0) {
items[idx].classList.add('or-kb-selected');
}
});
updateCountBadge();
}
// --- Focus management ---
function getFocusedIndex(items) {
if (!focusedId) return -1;
return findItemById(items, focusedId);
}
function setFocus(items, index) {
if (index < 0 || index >= items.length) return;
focusedId = getMessageId(items[index]);
applyVisualState(items);
items[index].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
// --- Selection management ---
function toggleSelect(items, index) {
if (index < 0 || index >= items.length) return;
var id = getMessageId(items[index]);
if (!id) return;
if (selectedIds.has(id)) {
selectedIds.delete(id);
} else {
selectedIds.add(id);
}
applyVisualState(items);
}
function clearSelection() {
selectedIds.clear();
var oldSelected = document.querySelectorAll('.or-kb-selected');
for (var i = 0; i < oldSelected.length; i++) oldSelected[i].classList.remove('or-kb-selected');
updateCountBadge();
}
function getActionTargets(items) {
var targets = [];
if (selectedIds.size > 0) {
selectedIds.forEach(function (id) {
var idx = findItemById(items, id);
if (idx >= 0) targets.push(items[idx]);
});
} else {
var fi = getFocusedIndex(items);
if (fi >= 0) targets.push(items[fi]);
}
return targets;
}
// --- Selection count badge ---
function createCountBadge() {
if (!document.body) return null;
var badge = document.createElement('div');
badge.className = 'or-kb-selection-count';
badge.style.opacity = '0';
document.body.appendChild(badge);
return badge;
}
function updateCountBadge() {
if (!countBadge) countBadge = createCountBadge();
if (!countBadge) return;
var count = selectedIds.size;
if (count === 0) {
countBadge.style.opacity = '0';
} else {
countBadge.textContent = count + ' selected';
countBadge.style.opacity = '1';
}
}
// --- Actions ---
// --- Find a button by aria-label, searching within a target element first,
// then the global toolbar, then the whole document ---
function findButton(target, label) {
// 1. Look for inline button within/near the message row
// OWA renders Delete/Archive/Flag buttons on each row on hover
var btn = target.querySelector('button[aria-label="' + label + '"]');
if (btn) return btn;
// 2. Check the parent (some buttons are siblings of the row)
if (target.parentElement) {
btn = target.parentElement.querySelector('button[aria-label="' + label + '"]');
if (btn) return btn;
}
// 3. Look in the global toolbar area
btn = document.querySelector('[role="toolbar"] button[aria-label="' + label + '"], .fui-Toolbar button[aria-label="' + label + '"]');
if (btn) return btn;
// 4. Anywhere in the document
btn = document.querySelector('button[aria-label="' + label + '"]');
return btn;
}
// Simulate hover on a message row to make OWA's inline action buttons appear
function triggerHover(target) {
var rect = target.getBoundingClientRect();
var enterEvent = new MouseEvent('mouseenter', {
bubbles: true, cancelable: true,
clientX: rect.x + rect.width - 30,
clientY: rect.y + rect.height / 2,
});
var overEvent = new MouseEvent('mouseover', {
bubbles: true, cancelable: true,
clientX: rect.x + rect.width - 30,
clientY: rect.y + rect.height / 2,
});
target.dispatchEvent(enterEvent);
target.dispatchEvent(overEvent);
}
function performAction(targets, actionFn) {
if (targets.length === 0) return;
var i = 0;
function processNext() {
if (i >= targets.length) {
clearSelection();
return;
}
var target = targets[i];
i++;
// First, try to find the inline button without clicking the row.
// Hover to make OWA's inline action buttons appear.
triggerHover(target);
setTimeout(function () {
// Try inline button first (avoids opening the email in fill-screen mode)
var inlineHandled = actionFn(target);
// If inline button wasn't found, fall back to clicking the row + toolbar
if (inlineHandled === false) {
target.click();
setTimeout(function () {
actionFn(target);
setTimeout(processNext, 200);
}, 150);
} else {
setTimeout(processNext, 200);
}
}, 100);
}
processNext();
}
function actionDelete(targets) {
performAction(targets, function (target) {
var btn = findButton(target, 'Delete');
if (btn) { btn.click(); return true; }
console.warn('[Outcut] Delete button not found');
return false;
});
}
function actionArchive(targets) {
performAction(targets, function (target) {
var btn = findButton(target, 'Archive');
if (btn) { btn.click(); return true; }
console.warn('[Outcut] Archive button not found');
return false;
});
}
function actionMarkReadUnread(targets) {
performAction(targets, function (target) {
var btn = findButton(target, 'Read / Unread');
if (btn) { btn.click(); return true; }
triggerContextMenuAction(target, /mark as read|mark as unread|read \/ unread/i);
return true; // context menu handles it
});
}
function actionMove(targets) {
if (targets.length > 0) {
triggerHover(targets[0]);
setTimeout(function () {
var btn = findButton(targets[0], 'Move to');
if (btn) {
btn.click();
} else {
triggerContextMenuAction(targets[0], /move to/i);
}
}, 150);
}
}
function actionFlag(targets) {
performAction(targets, function (target) {
var btn = findButton(target, 'Flag this message')
|| findButton(target, 'Flag / Unflag');
if (btn) { btn.click(); return true; }
console.warn('[Outcut] Flag button not found');
return false;
});
}
function actionPin(targets) {
performAction(targets, function (target) {
var btn = findButton(target, 'Pin / Unpin');
if (btn) { btn.click(); return true; }
console.warn('[Outcut] Pin button not found');
return false;
});
}
function triggerContextMenuAction(target, pattern) {
var el = target || document.querySelector('.or-kb-focused');
if (!el) return;
var rect = el.getBoundingClientRect();
var contextEvent = new MouseEvent('contextmenu', {
bubbles: true, cancelable: true,
clientX: rect.x + 10, clientY: rect.y + 10,
});
el.dispatchEvent(contextEvent);
setTimeout(function () {
var menuItems = document.querySelectorAll('[role="menuitem"]');
for (var j = 0; j < menuItems.length; j++) {
if (pattern.test(menuItems[j].textContent || menuItems[j].getAttribute('aria-label') || '')) {
menuItems[j].click();
return;
}
}
document.body.click();
}, 300);
}
// --- Key handler ---
function handleKeydown(e) {
if (!currentSettings.keyboardMultiSelect) return;
if (isComposeOrDialogActive()) return;
var items = getMessageItems();
if (items.length === 0) return;
var presetName = currentSettings.keyboardPreset || 'gmail';
var preset = KEY_PRESETS[presetName] || KEY_PRESETS.gmail;
// Initialize focus if not set
var currentIdx = getFocusedIndex(items);
if (currentIdx < 0) {
for (var f = 0; f < items.length; f++) {
if (items[f].getAttribute('aria-selected') === 'true' ||
items[f].classList.contains('is-selected')) {
currentIdx = f;
focusedId = getMessageId(items[f]);
break;
}
}
if (currentIdx < 0) {
currentIdx = 0;
focusedId = getMessageId(items[0]);
}
applyVisualState(items);
}
var handled = true;
var targets;
if (matchesAction(e, preset.selectExtendDown)) {
toggleSelect(items, currentIdx);
if (currentIdx < items.length - 1) {
setFocus(items, currentIdx + 1);
toggleSelect(items, currentIdx + 1);
}
} else if (matchesAction(e, preset.selectExtendUp)) {
toggleSelect(items, currentIdx);
if (currentIdx > 0) {
setFocus(items, currentIdx - 1);
toggleSelect(items, currentIdx - 1);
}
} else if (matchesAction(e, preset.nextMessage)) {
if (currentIdx < items.length - 1) setFocus(items, currentIdx + 1);
} else if (matchesAction(e, preset.prevMessage)) {
if (currentIdx > 0) setFocus(items, currentIdx - 1);
} else if (matchesAction(e, preset.toggleSelect)) {
e.preventDefault();
toggleSelect(items, currentIdx);
} else if (matchesAction(e, preset.delete)) {
targets = getActionTargets(items);
actionDelete(targets);
} else if (matchesAction(e, preset.archive)) {
targets = getActionTargets(items);
actionArchive(targets);
} else if (matchesAction(e, preset.readUnread)) {
targets = getActionTargets(items);
actionMarkReadUnread(targets);
} else if (matchesAction(e, preset.move)) {
targets = getActionTargets(items);
actionMove(targets);
} else if (matchesAction(e, preset.flag)) {
targets = getActionTargets(items);
actionFlag(targets);
} else if (matchesAction(e, preset.pin)) {
targets = getActionTargets(items);
actionPin(targets);
} else if (matchesAction(e, preset.deselect)) {
clearSelection();
} else if (matchesAction(e, preset.open)) {
if (currentIdx >= 0 && currentIdx < items.length) items[currentIdx].click();
} else {
handled = false;
}
if (handled) {
e.stopPropagation();
if (e.key !== 'Enter') e.preventDefault();
}
}
// --- Re-apply visual state when OWA re-renders the message list ---
function setupListObserver() {
var listObserver = new MutationObserver(function () {
// Prune IDs that no longer exist in DOM
var items = getMessageItems();
var currentIds = new Set();
for (var i = 0; i < items.length; i++) {
var id = getMessageId(items[i]);
if (id) currentIds.add(id);
}
selectedIds.forEach(function (id) {
if (!currentIds.has(id)) selectedIds.delete(id);
});
if (focusedId && !currentIds.has(focusedId)) focusedId = null;
// Re-apply classes to new DOM elements
applyVisualState(items);
});
var checkInterval = setInterval(function () {
var list = document.querySelector('[role="listbox"], [role="list"]');
if (list) {
clearInterval(checkInterval);
listObserver.observe(list, { childList: true, subtree: true });
}
}, 1000);
cleanupFns.push(function () {
clearInterval(checkInterval);
listObserver.disconnect();
});
}
// --- Public API ---
function start(settings) {
currentSettings = settings;
if (!settings.keyboardMultiSelect) return;
document.addEventListener('keydown', handleKeydown, true);
setupListObserver();
console.log('[Outcut] Keyboard navigation started');
cleanupFns.push(function () {
document.removeEventListener('keydown', handleKeydown, true);
});
}
function updateSettings(settings) {
var wasEnabled = currentSettings.keyboardMultiSelect;
var isEnabled = settings.keyboardMultiSelect;
var oldPreset = currentSettings.keyboardPreset;
var newPreset = settings.keyboardPreset;
currentSettings = settings;
if (wasEnabled !== isEnabled || oldPreset !== newPreset) {
stop();
start(settings);
}
}
function stop() {
for (var i = 0; i < cleanupFns.length; i++) {
try { cleanupFns[i](); } catch (e) { /* ignore */ }
}
cleanupFns = [];
clearSelection();
var focused = document.querySelector('.or-kb-focused');
if (focused) focused.classList.remove('or-kb-focused');
focusedId = null;
if (countBadge) {
countBadge.remove();
countBadge = null;
}
}
return { start: start, updateSettings: updateSettings, stop: stop };
})();

126
content/observer.js Normal file
View File

@@ -0,0 +1,126 @@
// Outlook Relook — MutationObserver
// Watches OWA's DOM and removes dynamically injected elements
// based on active settings.
window.OutlookRelook = window.OutlookRelook || {};
window.OutlookRelook.Observer = (function () {
'use strict';
let observer = null;
let currentSettings = {};
// Map setting keys to selector registry names
// Each setting can suppress one or more logical elements
const SETTING_TO_SELECTORS = {
hideCopilot: ['copilot-button', 'copilot-pane', 'copilot-compose-suggestions'],
hideSuggestedReplies: ['suggested-replies'],
hidePromoBanners: ['promo-banners'],
hideFocusedOtherTabs: ['focused-other-tabs'],
hideSidebarAppIcons: ['sidebar-app-icons'],
hideGroupsSection: ['groups-section'],
hideMyDayButtons: ['my-day-buttons'],
hideSenderAvatars: ['sender-avatars'],
hideFeatureDiscovery: ['feature-discovery'],
hideVivaInsights: ['viva-insights'],
hideUnreadOtherBanner: ['unread-other-banner'],
hideActivityFeed: ['activity-feed'],
};
// Text content patterns to match elements by their inner text.
// Used when aria/data selectors don't catch dynamically injected content.
const TEXT_PATTERNS = {
hidePromoBanners: [
/try the new outlook/i,
/upgrade to premium/i,
/get the outlook app/i,
/switch to the new/i,
],
hideFeatureDiscovery: [
/what's new/i,
/new feature/i,
/did you know/i,
],
};
function suppressElements() {
for (const [settingKey, selectorNames] of Object.entries(SETTING_TO_SELECTORS)) {
if (!currentSettings[settingKey]) continue;
for (const name of selectorNames) {
const elements = window.OutlookRelook.resolveSelector(name);
for (const el of elements) {
if (el.style.display !== 'none') {
el.style.display = 'none';
console.log('[Outcut] Suppressed: ' + name, el);
}
}
}
}
// Text-based suppression for elements missed by selectors
for (const [settingKey, patterns] of Object.entries(TEXT_PATTERNS)) {
if (!currentSettings[settingKey]) continue;
for (const pattern of patterns) {
// Check buttons, banners, and generic containers
const candidates = document.querySelectorAll(
'[role="alert"], [role="banner"], [role="dialog"], [role="status"], button, a'
);
for (const el of candidates) {
if (el.style.display === 'none') continue;
var text = el.textContent || '';
if (pattern.test(text) && text.length < 200) {
el.style.display = 'none';
console.log('[Outcut] Suppressed by text: "' + text.trim().substring(0, 50) + '"', el);
}
}
}
}
}
function start(settings) {
currentSettings = settings;
// Initial pass
suppressElements();
// Watch for DOM mutations
observer = new MutationObserver(function (mutations) {
// Debounce: only process if nodes were actually added
var hasAddedNodes = false;
for (var i = 0; i < mutations.length; i++) {
if (mutations[i].addedNodes.length > 0) {
hasAddedNodes = true;
break;
}
}
if (hasAddedNodes) {
suppressElements();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
console.log('[Outcut] Observer started');
}
function updateSettings(settings) {
currentSettings = settings;
// Re-run suppression with new settings
suppressElements();
}
function stop() {
if (observer) {
observer.disconnect();
observer = null;
console.log('[Outcut] Observer stopped');
}
}
return { start: start, updateSettings: updateSettings, stop: stop };
})();

165
content/selectors.js Normal file
View File

@@ -0,0 +1,165 @@
// Outlook Relook — Selector Registry
// Maps logical element names to resilient CSS selectors.
// Selectors use aria-label, data-*, role, and structural patterns
// to avoid depending on OWA's obfuscated class names.
//
// Selectors marked /* INSPECT OWA */ must be verified against the live DOM.
// Use Chrome DevTools on outlook.office.com to find correct selectors.
window.OutlookRelook = window.OutlookRelook || {};
window.OutlookRelook.SELECTORS = {
// --- Hide Elements ---
'copilot-button': {
primary: '[aria-label*="Copilot" i]',
fallbacks: ['[data-app-section="Copilot"]', '[title*="Copilot" i]'],
},
'copilot-pane': {
primary: '[aria-label*="Copilot" i][role="complementary"]',
fallbacks: [],
},
'copilot-compose-suggestions': {
primary: '[aria-label*="writing suggestion" i]',
fallbacks: ['[aria-label*="Copilot" i][role="listbox"]'],
},
'suggested-replies': {
primary: '[aria-label*="Suggested repl" i]',
fallbacks: ['[role="group"][aria-label*="Reply suggestion" i]'],
},
'promo-banners': {
primary: '[aria-label*="Try the new" i], [aria-label*="Upgrade" i], [aria-label*="Get the app" i], [aria-label*="premium" i]',
fallbacks: [],
},
'focused-other-tabs': {
primary: '[role="tablist"][aria-label*="Focused" i]',
fallbacks: ['[aria-label*="Focused Inbox" i]'],
},
'sidebar-app-icons': {
primary: 'nav[aria-label*="App" i], [role="navigation"][aria-label*="Module" i]',
fallbacks: [],
},
'groups-section': {
primary: '[aria-label*="Groups" i][role="tree"], [aria-label*="Groups" i][role="treeitem"]',
fallbacks: [],
},
'my-day-buttons': {
primary: '[aria-label*="My Day" i], [aria-label*="To Do" i][role="button"]',
fallbacks: [],
},
'sender-avatars': {
primary: '[role="listbox"] [aria-hidden="true"] img[src*="profile"], [role="listbox"] [aria-label*="avatar" i]',
fallbacks: [],
},
'feature-discovery': {
primary: '[role="dialog"][aria-label*="new feature" i], [role="dialog"][aria-label*="what\'s new" i], [aria-label*="teaching" i]',
fallbacks: [],
},
'viva-insights': {
primary: '[aria-label*="Viva" i], [aria-label*="Daily Briefing" i], [aria-label*="Briefing" i]',
fallbacks: [],
},
'unread-other-banner': {
primary: '[aria-label*="unread in Other" i]',
fallbacks: [],
},
'activity-feed': {
primary: '[aria-label*="Activity" i][role="complementary"], [aria-label*="mentioned you" i]',
fallbacks: [],
},
// --- Layout Regions (for density CSS) ---
'top-bar': {
primary: '[role="banner"]',
fallbacks: ['header'],
},
'command-bar': {
primary: '[role="toolbar"][aria-label*="command" i], [role="toolbar"][aria-label*="action" i]',
fallbacks: [],
},
'message-list': {
primary: '[role="listbox"][aria-label*="Message list" i], [role="list"][aria-label*="Message" i]',
fallbacks: [],
},
'reading-pane': {
primary: '[role="main"][aria-label*="Reading" i], [aria-label*="Message body" i]',
fallbacks: [],
},
'folder-pane': {
primary: '[role="navigation"][aria-label*="Folder" i], [role="tree"][aria-label*="Folder" i]',
fallbacks: [],
},
'compose-toolbar': {
primary: '[role="toolbar"][aria-label*="Format" i]',
fallbacks: [],
},
'search-bar': {
primary: '[role="search"], [aria-label*="Search" i] input',
fallbacks: [],
},
// --- Behavior targets ---
'ribbon-collapse-button': {
primary: '[aria-label*="Ribbon" i][aria-expanded], [aria-label*="collapse" i][role="button"]',
fallbacks: [],
},
'contact-hover-card': {
primary: '[role="dialog"][aria-label*="contact" i], [role="tooltip"][aria-label*="contact" i]',
fallbacks: [],
},
'notification-toast': {
primary: '[role="alert"], [role="status"][aria-live]',
fallbacks: [],
},
'reply-forward-bar': {
primary: '[aria-label*="Reply" i][role="button"], [aria-label*="Forward" i][role="button"]',
fallbacks: [],
},
'compose-window': {
primary: '[aria-label*="compose" i][role="dialog"], [aria-label*="New message" i]',
fallbacks: [],
},
'folder-header': {
primary: '[role="heading"][aria-level]',
fallbacks: [],
},
};
// Resolve a logical name to matching DOM elements.
// Tries primary selector first, then fallbacks in order.
// Returns an array of elements (possibly empty).
window.OutlookRelook.resolveSelector = function (name) {
const entry = window.OutlookRelook.SELECTORS[name];
if (!entry) {
console.warn('[Outlook Relook] Unknown selector: ' + name);
return [];
}
const selectors = [entry.primary, ...entry.fallbacks];
for (const selector of selectors) {
try {
const elements = document.querySelectorAll(selector);
if (elements.length > 0) return Array.from(elements);
} catch (e) {
console.warn('[Outlook Relook] Invalid selector for "' + name + '": ' + selector, e);
}
}
return [];
};
// Resolve a logical name to a CSS selector string that currently matches.
// Returns the first matching selector string, or null.
window.OutlookRelook.resolveSelectorString = function (name) {
const entry = window.OutlookRelook.SELECTORS[name];
if (!entry) return null;
const selectors = [entry.primary, ...entry.fallbacks];
for (const selector of selectors) {
try {
if (document.querySelector(selector)) return selector;
} catch (e) {
// skip invalid
}
}
return null;
};

View File

@@ -0,0 +1,116 @@
// Outlook Relook — Settings Defaults
// Loaded first by manifest. Exposes window.OutlookRelook.DEFAULTS and helpers.
window.OutlookRelook = window.OutlookRelook || {};
window.OutlookRelook.DEFAULTS = {
// Keyboard Navigation (primary feature, always visible)
keyboardMultiSelect: true,
keyboardPreset: 'gmail',
// Design Tweaks (experimental, hidden by default)
enableDesignTweaks: false,
// Theme & Appearance
theme: 'swiss',
colorScheme: 'system', // 'light' | 'dark' | 'system'
accentColor: '', // empty = theme default
// Density & Spacing
densityPreset: 'compact', // 'comfortable' | 'compact' | 'ultra-compact'
compactTopBar: true,
compactCommandBar: true,
compactMessageList: true,
compactReadingPane: true,
compactFolderPane: true,
narrowDateColumn: true,
compressComposeToolbar: true,
readingPaneMaxWidth: true,
unifiedHeader: false,
// Hide Elements
hideCopilot: true,
hideSuggestedReplies: true,
hidePromoBanners: true,
hideFocusedOtherTabs: true,
hideSidebarAppIcons: false,
hideGroupsSection: false,
hideMyDayButtons: false,
hideSenderAvatars: false,
hideFeatureDiscovery: true,
hideVivaInsights: true,
hideUnreadOtherBanner: true,
hideActivityFeed: true,
// Readability
unreadDistinction: true,
previewOwnLine: false,
normalizeFontWeight: true,
darkModeEmailFix: true,
messageListFontSize: 'medium', // 'small' | 'medium' | 'large'
// Behavior
autoCollapseRibbon: true,
rememberSidebarState: true,
suppressContactHover: true,
autoAdvanceAfterDelete: true,
autoDismissToasts: '5', // 'off' | '3' | '5' | '10' (seconds)
toastPosition: 'top-right', // 'bottom-left' | 'top-right'
stickyReplyBar: true,
autoResizeCompose: true,
throttleNotifications: false,
// Quick Actions
markAllReadButton: true,
quickFolderJump: true,
};
// Density presets define which individual toggles each preset sets
window.OutlookRelook.DENSITY_PRESETS = {
comfortable: {
compactTopBar: false,
compactCommandBar: false,
compactMessageList: false,
compactReadingPane: false,
compactFolderPane: false,
narrowDateColumn: false,
compressComposeToolbar: false,
readingPaneMaxWidth: true,
},
compact: {
compactTopBar: true,
compactCommandBar: true,
compactMessageList: true,
compactReadingPane: true,
compactFolderPane: true,
narrowDateColumn: true,
compressComposeToolbar: true,
readingPaneMaxWidth: true,
},
'ultra-compact': {
compactTopBar: true,
compactCommandBar: true,
compactMessageList: true,
compactReadingPane: true,
compactFolderPane: true,
narrowDateColumn: true,
compressComposeToolbar: true,
readingPaneMaxWidth: true,
},
};
// Load settings from chrome.storage.sync, filling in defaults for missing keys
window.OutlookRelook.loadSettings = function () {
return new Promise((resolve) => {
chrome.storage.sync.get(window.OutlookRelook.DEFAULTS, (settings) => {
resolve(settings);
});
});
};
// Save a partial settings object to chrome.storage.sync
window.OutlookRelook.saveSettings = function (partial) {
return new Promise((resolve) => {
chrome.storage.sync.set(partial, resolve);
});
};

BIN
icons/icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

BIN
icons/icon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 B

BIN
icons/icon-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

43
manifest.json Normal file
View File

@@ -0,0 +1,43 @@
{
"manifest_version": 3,
"name": "Outcut",
"version": "1.0.0",
"description": "Keyboard shortcuts for Outlook — Gmail-style multi-select, delete, archive, and more.",
"homepage_url": "https://github.com/joelbrockcoluminate/outcut",
"permissions": ["storage"],
"content_scripts": [
{
"matches": ["https://outlook.office.com/*", "https://outlook.office365.com/*", "https://outlook.cloud.microsoft/*"],
"css": ["themes/base.css"],
"js": [
"content/settings-defaults.js",
"content/selectors.js",
"content/observer.js",
"content/behavior.js",
"content/keyboard.js",
"content/injector.js",
"content/content.js"
],
"run_at": "document_idle"
}
],
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"web_accessible_resources": [
{
"resources": ["themes/*.css"],
"matches": ["https://outlook.office.com/*", "https://outlook.office365.com/*", "https://outlook.cloud.microsoft/*"]
}
]
}

250
popup/popup.css Normal file
View File

@@ -0,0 +1,250 @@
/* Outlook Relook — Settings Popup */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
width: 360px;
max-height: 540px;
overflow-y: auto;
font-family: system-ui, -apple-system, sans-serif;
font-size: 13px;
line-height: 1.4;
color: #222;
background: #fff;
padding: 0;
}
/* Header */
.or-header {
padding: 12px 16px;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
background: #fff;
z-index: 10;
}
.or-header h1 {
font-size: 15px;
font-weight: 600;
letter-spacing: -0.02em;
}
.or-header .or-version {
font-size: 11px;
color: #999;
}
/* Sections */
.or-section {
border-bottom: 1px solid #f0f0f0;
}
.or-section-header {
padding: 10px 16px 6px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #888;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
user-select: none;
}
.or-section-header::after {
content: '\25B6'; /* right-pointing triangle */
font-size: 8px;
transition: transform 0.15s;
}
.or-section.open .or-section-header::after {
transform: rotate(90deg);
}
.or-section-body {
display: none;
padding: 0 16px 10px;
}
.or-section.open .or-section-body {
display: block;
}
/* Toggle rows */
.or-toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 0;
gap: 8px;
}
.or-toggle-row label {
flex: 1;
cursor: pointer;
font-size: 12.5px;
}
/* Toggle switch */
.or-switch {
position: relative;
width: 36px;
height: 20px;
flex-shrink: 0;
}
.or-switch input {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
z-index: 2;
margin: 0;
}
.or-switch .slider {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: #ccc;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s;
}
.or-switch .slider::before {
content: '';
position: absolute;
width: 16px;
height: 16px;
left: 2px;
top: 2px;
background: #fff;
border-radius: 50%;
transition: transform 0.2s;
}
.or-switch input:checked + .slider {
background: #1976d2;
}
.or-switch input:checked + .slider::before {
transform: translateX(16px);
}
/* Select/dropdown rows */
.or-select-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 0;
gap: 8px;
}
.or-select-row label {
flex: 1;
font-size: 12.5px;
}
.or-select-row select {
padding: 3px 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 12px;
background: #fff;
color: #222;
cursor: pointer;
}
/* Color picker */
.or-color-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 0;
gap: 8px;
}
.or-color-row label {
flex: 1;
font-size: 12.5px;
}
.or-color-row input[type="color"] {
width: 32px;
height: 24px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
padding: 0;
}
/* Radio group */
.or-radio-group {
display: flex;
gap: 12px;
padding: 5px 0;
}
.or-radio-group label {
display: flex;
align-items: center;
gap: 4px;
font-size: 12.5px;
cursor: pointer;
}
/* Footer */
.or-footer {
padding: 10px 16px;
display: flex;
gap: 8px;
justify-content: flex-end;
}
.or-footer button {
padding: 5px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
font-size: 12px;
cursor: pointer;
color: #222;
}
.or-footer button:hover {
background: #f0f0f0;
}
.or-footer button.danger {
color: #d32f2f;
border-color: #d32f2f;
}
.or-footer button.danger:hover {
background: #fce4ec;
}
/* Scrollbar */
body::-webkit-scrollbar {
width: 6px;
}
body::-webkit-scrollbar-track {
background: transparent;
}
body::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}

47
popup/popup.html Normal file
View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="or-header">
<h1>Outcut</h1>
<span class="or-version" id="version"></span>
</div>
<!-- Keyboard Navigation (primary feature, always visible) -->
<div class="or-section open" data-section="keyboard">
<div class="or-section-header">Keyboard Navigation</div>
<div class="or-section-body">
<div class="or-select-row">
<label for="keyboardPreset">Shortcut style</label>
<select id="keyboardPreset" data-setting="keyboardPreset">
<option value="gmail">Gmail</option>
<option value="outlook">Outlook</option>
</select>
</div>
<div class="or-toggle-row">
<label for="keyboardMultiSelect">Enable keyboard multi-select</label>
<div class="or-switch"><input type="checkbox" id="keyboardMultiSelect" data-setting="keyboardMultiSelect"><span class="slider"></span></div>
</div>
<div id="keyboardHelpText" style="font-size:11px;color:#888;padding:4px 0 2px;line-height:1.4;">
j/k navigate, x/Space select, # delete, e archive,
Shift+i/u read/unread, v move, f flag, p pin, Esc deselect
</div>
</div>
</div>
<!-- Footer -->
<div class="or-footer">
<button id="exportBtn" title="Export settings as JSON">Export</button>
<button id="importBtn" title="Import settings from JSON">Import</button>
<button id="resetBtn" class="danger" title="Reset all settings to defaults">Reset</button>
</div>
<input type="file" id="importFile" accept=".json" style="display:none">
<script src="popup.js"></script>
</body>
</html>

196
popup/popup.js Normal file
View File

@@ -0,0 +1,196 @@
// Outcut — Popup Settings Logic
(function () {
'use strict';
// Defaults — must stay in sync with content/settings-defaults.js
// (popup runs in its own JS context, no access to content script globals)
var DEFAULTS = {
// Primary feature
keyboardMultiSelect: true,
keyboardPreset: 'gmail',
// Design tweaks master toggle
enableDesignTweaks: false,
// Theme & Appearance
theme: 'swiss',
colorScheme: 'system',
accentColor: '',
// Density & Spacing
densityPreset: 'compact',
compactTopBar: true,
compactCommandBar: true,
compactMessageList: true,
compactReadingPane: true,
compactFolderPane: true,
narrowDateColumn: true,
compressComposeToolbar: true,
readingPaneMaxWidth: true,
unifiedHeader: false,
// Hide Elements
hideCopilot: true,
hideSuggestedReplies: true,
hidePromoBanners: true,
hideFocusedOtherTabs: true,
hideSidebarAppIcons: false,
hideGroupsSection: false,
hideMyDayButtons: false,
hideSenderAvatars: false,
hideFeatureDiscovery: true,
hideVivaInsights: true,
hideUnreadOtherBanner: true,
hideActivityFeed: true,
// Readability
unreadDistinction: true,
previewOwnLine: false,
normalizeFontWeight: true,
darkModeEmailFix: true,
messageListFontSize: 'medium',
// Behavior
autoCollapseRibbon: true,
rememberSidebarState: true,
suppressContactHover: true,
autoAdvanceAfterDelete: true,
autoDismissToasts: '5',
toastPosition: 'top-right',
stickyReplyBar: true,
autoResizeCompose: true,
throttleNotifications: false,
// Quick Actions
markAllReadButton: true,
quickFolderJump: true,
};
// --- Keyboard help text ---
function updateKeyboardHelp(presetName) {
var helpEl = document.getElementById('keyboardHelpText');
if (!helpEl) return;
var presets = {
gmail: 'j/k navigate, x/Space select, # delete, e archive, Shift+i/u read/unread, v move, f flag, p pin, Esc deselect',
outlook: 'Arrows navigate, Space select, Del/Backspace delete, e archive, q read/unread, v move, f flag, p pin, Esc deselect'
};
helpEl.textContent = presets[presetName] || presets.gmail;
}
// --- Load settings and populate UI ---
function loadUI() {
chrome.storage.sync.get(DEFAULTS, function (settings) {
// Checkboxes
var checkboxes = document.querySelectorAll('input[type="checkbox"][data-setting]');
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = !!settings[checkboxes[i].dataset.setting];
}
// Selects
var selects = document.querySelectorAll('select[data-setting]');
for (var j = 0; j < selects.length; j++) {
selects[j].value = settings[selects[j].dataset.setting] || '';
}
// Version
var manifest = chrome.runtime.getManifest();
document.getElementById('version').textContent = 'v' + manifest.version;
// Update help text based on current preset
updateKeyboardHelp(settings.keyboardPreset || 'gmail');
});
}
// --- Save a single setting ---
function saveSetting(key, value) {
var obj = {};
obj[key] = value;
chrome.storage.sync.set(obj);
}
// --- Section accordion ---
var sectionHeaders = document.querySelectorAll('.or-section-header');
for (var s = 0; s < sectionHeaders.length; s++) {
sectionHeaders[s].addEventListener('click', function () {
this.parentElement.classList.toggle('open');
});
}
// --- Checkbox change handlers ---
var checkboxes = document.querySelectorAll('input[type="checkbox"][data-setting]');
for (var c = 0; c < checkboxes.length; c++) {
checkboxes[c].addEventListener('change', function () {
saveSetting(this.dataset.setting, this.checked);
});
}
// --- Select change handlers ---
var selects = document.querySelectorAll('select[data-setting]');
for (var sl = 0; sl < selects.length; sl++) {
selects[sl].addEventListener('change', function () {
var settingKey = this.dataset.setting;
var value = this.value;
saveSetting(settingKey, value);
if (settingKey === 'keyboardPreset') {
updateKeyboardHelp(value);
}
});
}
// --- Export settings ---
document.getElementById('exportBtn').addEventListener('click', function () {
chrome.storage.sync.get(DEFAULTS, function (settings) {
var blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'outcut-settings.json';
a.click();
URL.revokeObjectURL(url);
});
});
// --- Import settings ---
document.getElementById('importBtn').addEventListener('click', function () {
document.getElementById('importFile').click();
});
document.getElementById('importFile').addEventListener('change', function (e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function (event) {
try {
var imported = JSON.parse(event.target.result);
var cleaned = {};
var defaultKeys = Object.keys(DEFAULTS);
for (var i = 0; i < defaultKeys.length; i++) {
if (defaultKeys[i] in imported) cleaned[defaultKeys[i]] = imported[defaultKeys[i]];
}
chrome.storage.sync.set(cleaned, function () {
loadUI();
});
} catch (err) {
alert('Invalid settings file.');
}
};
reader.readAsText(file);
e.target.value = '';
});
// --- Reset to defaults ---
document.getElementById('resetBtn').addEventListener('click', function () {
if (confirm('Reset all settings to defaults?')) {
chrome.storage.sync.set(DEFAULTS, function () {
loadUI();
});
}
});
// --- Init ---
loadUI();
})();

121
selectors-test.html Normal file
View File

@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Outlook Relook — Selector Test Page</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; max-width: 800px; margin: 0 auto; }
h1 { font-size: 18px; margin-bottom: 16px; }
h2 { font-size: 14px; margin: 20px 0 8px; color: #666; }
.mock { border: 1px dashed #ccc; padding: 8px 12px; margin: 4px 0; font-size: 13px; background: #fafafa; }
.result { margin: 4px 0; font-size: 12px; font-family: monospace; }
.pass { color: #2e7d32; }
.fail { color: #c62828; }
#results { margin-top: 24px; border-top: 1px solid #eee; padding-top: 16px; }
button { padding: 8px 16px; margin-top: 12px; cursor: pointer; }
</style>
</head>
<body>
<h1>Outlook Relook — Selector Test Page</h1>
<p>Mock OWA DOM fragments. Click "Run Tests" to verify selectors match.</p>
<!-- Mock OWA elements -->
<h2>Copilot</h2>
<div class="mock" aria-label="Copilot" role="button">Copilot Button</div>
<div class="mock" aria-label="Copilot chat" role="complementary">Copilot Pane</div>
<div class="mock" aria-label="Inline writing suggestion">Copilot Compose Suggestion</div>
<h2>Suggested Replies</h2>
<div class="mock" aria-label="Suggested replies" role="group">Sounds good! | Thanks! | Got it.</div>
<h2>Promotional Banners</h2>
<div class="mock" aria-label="Try the new Outlook" role="banner">Try the new Outlook</div>
<div class="mock" aria-label="Upgrade to premium">Upgrade to premium</div>
<div class="mock" aria-label="Get the Outlook app">Get the Outlook app</div>
<h2>Focused/Other Tabs</h2>
<div class="mock" role="tablist" aria-label="Focused Inbox tabs">
<div role="tab">Focused</div>
<div role="tab">Other</div>
</div>
<h2>Sidebar App Icons</h2>
<nav class="mock" aria-label="App navigation">
<div>Mail</div><div>Calendar</div><div>People</div><div>To Do</div>
</nav>
<h2>Layout Regions</h2>
<div class="mock" role="banner">Top Bar / Banner</div>
<div class="mock" role="toolbar" aria-label="Command actions">Command Bar</div>
<div class="mock" role="listbox" aria-label="Message list">
<div role="option" aria-label="Unread: Subject line">Message Row</div>
<div role="option">Read Message Row</div>
</div>
<div class="mock" role="navigation" aria-label="Folder pane">
<div role="tree" aria-label="Folder list">
<div role="treeitem">Inbox</div>
<div role="treeitem">Sent</div>
</div>
</div>
<h2>Behavior Targets</h2>
<div class="mock" role="search"><input placeholder="Search" aria-label="Search mail"></div>
<div class="mock" aria-label="Ribbon display options" aria-expanded="true" role="button">Ribbon Toggle</div>
<div class="mock" role="alert">Toast notification: Message sent</div>
<button id="runTests">Run Tests</button>
<div id="results"></div>
<script>
document.getElementById('runTests').addEventListener('click', function () {
var results = document.getElementById('results');
// Clear results using safe DOM methods
while (results.firstChild) {
results.removeChild(results.firstChild);
}
var heading = document.createElement('h2');
heading.textContent = 'Test Results';
results.appendChild(heading);
var tests = [
{ name: 'copilot-button', selector: '[aria-label*="Copilot" i]', expectMatch: true },
{ name: 'copilot-pane', selector: '[aria-label*="Copilot" i][role="complementary"]', expectMatch: true },
{ name: 'copilot-compose', selector: '[aria-label*="writing suggestion" i]', expectMatch: true },
{ name: 'suggested-replies', selector: '[aria-label*="Suggested repl" i]', expectMatch: true },
{ name: 'promo-banners', selector: '[aria-label*="Try the new" i]', expectMatch: true },
{ name: 'focused-tabs', selector: '[role="tablist"][aria-label*="Focused" i]', expectMatch: true },
{ name: 'sidebar-apps', selector: 'nav[aria-label*="App" i]', expectMatch: true },
{ name: 'top-bar', selector: '[role="banner"]', expectMatch: true },
{ name: 'command-bar-alt', selector: '[role="toolbar"][aria-label*="action" i]', expectMatch: true },
{ name: 'message-list', selector: '[role="listbox"][aria-label*="Message list" i]', expectMatch: true },
{ name: 'folder-pane', selector: '[role="navigation"][aria-label*="Folder" i]', expectMatch: true },
{ name: 'search', selector: '[role="search"]', expectMatch: true },
{ name: 'ribbon-toggle', selector: '[aria-label*="Ribbon" i][aria-expanded]', expectMatch: true },
{ name: 'toast', selector: '[role="alert"]', expectMatch: true },
];
var pass = 0;
var fail = 0;
for (var i = 0; i < tests.length; i++) {
var test = tests[i];
var found = document.querySelectorAll(test.selector).length > 0;
var ok = found === test.expectMatch;
var div = document.createElement('div');
div.className = 'result ' + (ok ? 'pass' : 'fail');
div.textContent = (ok ? 'PASS' : 'FAIL') + ' ' + test.name + ': selector="' + test.selector + '" found=' + found + ' expected=' + test.expectMatch;
results.appendChild(div);
if (ok) pass++; else fail++;
}
var summary = document.createElement('div');
summary.className = 'result';
summary.textContent = pass + ' passed, ' + fail + ' failed out of ' + tests.length + ' tests';
summary.style.fontWeight = 'bold';
summary.style.marginTop = '12px';
results.appendChild(summary);
});
</script>
</body>
</html>

486
themes/base.css Normal file
View File

@@ -0,0 +1,486 @@
/*
* Outlook Relook — Base Theme
* Always loaded. Controls density, spacing, and element hiding.
*
* Toggle classes are set on <html> by content.js:
* data-outlook-relook-scheme="light|dark"
* data-or-compact-topbar="true"
* data-or-compact-commandbar="true"
* data-or-compact-messagelist="true"
* data-or-compact-readingpane="true"
* data-or-compact-folderpane="true"
* data-or-narrow-datecol="true"
* data-or-compact-compose="true"
* data-or-reading-maxwidth="true"
* data-or-hide-copilot="true"
* data-or-hide-suggestedreplies="true"
* data-or-hide-promos="true"
* data-or-hide-focusedtabs="true"
* data-or-hide-sidebaricons="true"
* data-or-hide-groups="true"
* data-or-hide-myday="true"
* data-or-hide-avatars="true"
* data-or-hide-discovery="true"
* data-or-hide-viva="true"
* data-or-hide-unreadother="true"
* data-or-hide-activity="true"
* data-or-unread-distinction="true"
* data-or-preview-own-line="true"
* data-or-normalize-font="true"
* data-or-darkmode-fix="true"
* data-or-fontsize="small|medium|large"
*/
/* ============================================================
DENSITY & SPACING
Selectors based on OWA's actual Fluent UI DOM structure:
- Search bar: [role="search"]
- Toolbars: [role="toolbar"], .fui-Toolbar
- Tab lists: [role="tablist"], .fui-TabList
- Ribbon buttons: .ms-Button, .ms-OverflowSet
- Ribbon groups: [role="group"]
============================================================ */
/* Compact top bar — target the search container and its ancestors */
html[data-or-compact-topbar="true"] [role="search"] {
height: 28px !important;
min-height: 28px !important;
max-height: 28px !important;
}
html[data-or-compact-topbar="true"] [role="search"] input {
height: 24px !important;
min-height: 24px !important;
padding-top: 2px !important;
padding-bottom: 2px !important;
}
/* Compact the search bar's parent containers */
html[data-or-compact-topbar="true"] [role="search"],
html[data-or-compact-topbar="true"] [role="search"] ~ *,
html[data-or-compact-topbar="true"] [role="search"] > * {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
/* Compact command bar / ribbon — target Fluent UI toolbars */
html[data-or-compact-commandbar="true"] [role="toolbar"],
html[data-or-compact-commandbar="true"] .fui-Toolbar {
padding: 2px 4px !important;
min-height: unset !important;
gap: 2px !important;
}
html[data-or-compact-commandbar="true"] [role="toolbar"] button,
html[data-or-compact-commandbar="true"] [role="toolbar"] .ms-Button,
html[data-or-compact-commandbar="true"] .fui-Toolbar button {
padding: 2px 6px !important;
min-height: 26px !important;
max-height: 26px !important;
font-size: 12px !important;
}
/* Compact the ribbon overflow/groups */
html[data-or-compact-commandbar="true"] .ms-OverflowSet {
min-height: unset !important;
}
html[data-or-compact-commandbar="true"] [role="group"] {
padding: 0 2px !important;
gap: 1px !important;
}
/* Compact message list */
html[data-or-compact-messagelist="true"] [role="listbox"] [role="option"],
html[data-or-compact-messagelist="true"] [role="list"] [role="listitem"] {
padding: 4px 8px !important;
min-height: unset !important;
}
/* Compact reading pane header */
html[data-or-compact-readingpane="true"] [role="main"] header,
html[data-or-compact-readingpane="true"] [aria-label*="Reading" i] > div:first-child {
padding: 8px 12px !important;
}
/* Compact folder pane */
html[data-or-compact-folderpane="true"] [role="tree"] [role="treeitem"] {
padding: 2px 8px !important;
min-height: 24px !important;
line-height: 24px !important;
}
/* Narrow date column */
html[data-or-narrow-datecol="true"] [role="listbox"] [role="option"] time,
html[data-or-narrow-datecol="true"] [role="list"] [role="listitem"] time {
font-size: 11px !important;
min-width: unset !important;
white-space: nowrap !important;
}
/* Compact compose toolbar */
html[data-or-compact-compose="true"] [role="toolbar"][aria-label*="Format" i] {
padding: 2px 4px !important;
min-height: unset !important;
}
html[data-or-compact-compose="true"] [role="toolbar"][aria-label*="Format" i] button {
padding: 2px 4px !important;
min-height: 24px !important;
}
/* Reading pane max-width */
html[data-or-reading-maxwidth="true"] [aria-label*="Message body" i],
html[data-or-reading-maxwidth="true"] [role="main"] [dir="ltr"],
html[data-or-reading-maxwidth="true"] [role="main"] [dir="rtl"] {
max-width: 72ch !important;
}
/* ============================================================
HIDE ELEMENTS
============================================================ */
/* Copilot */
html[data-or-hide-copilot="true"] [aria-label*="Copilot" i],
html[data-or-hide-copilot="true"] [data-app-section="Copilot"],
html[data-or-hide-copilot="true"] [title*="Copilot" i],
html[data-or-hide-copilot="true"] [aria-label*="writing suggestion" i] {
display: none !important;
}
/* Suggested replies */
html[data-or-hide-suggestedreplies="true"] [aria-label*="Suggested repl" i],
html[data-or-hide-suggestedreplies="true"] [aria-label*="Reply suggestion" i] {
display: none !important;
}
/* Promotional banners */
html[data-or-hide-promos="true"] [aria-label*="Try the new" i],
html[data-or-hide-promos="true"] [aria-label*="Upgrade" i],
html[data-or-hide-promos="true"] [aria-label*="Get the app" i],
html[data-or-hide-promos="true"] [aria-label*="premium" i],
html[data-or-hide-promos="true"] [aria-label*="Get the Outlook" i] {
display: none !important;
}
/* Focused / Other tabs — OWA uses .fui-TabList with .fui-Tab buttons */
html[data-or-hide-focusedtabs="true"] .fui-TabList {
display: none !important;
}
/* Left sidebar app icons */
html[data-or-hide-sidebaricons="true"] nav[aria-label*="App" i],
html[data-or-hide-sidebaricons="true"] [role="navigation"][aria-label*="Module" i] {
display: none !important;
}
/* Groups section */
html[data-or-hide-groups="true"] [aria-label*="Groups" i][role="tree"],
html[data-or-hide-groups="true"] [aria-label*="Groups" i][role="treeitem"] {
display: none !important;
}
/* My Day / right-side panel TOGGLE BUTTONS only (not the panel itself) */
html[data-or-hide-myday="true"] button[aria-label*="My Day" i],
html[data-or-hide-myday="true"] button[aria-label*="To Do" i] {
display: none !important;
}
/* Sender avatars */
html[data-or-hide-avatars="true"] [role="listbox"] [role="img"][aria-label*="profile" i],
html[data-or-hide-avatars="true"] [role="listbox"] img[src*="profile"] {
display: none !important;
}
/* Feature discovery / what's new */
html[data-or-hide-discovery="true"] [role="dialog"][aria-label*="new feature" i],
html[data-or-hide-discovery="true"] [role="dialog"][aria-label*="what's new" i],
html[data-or-hide-discovery="true"] [aria-label*="teaching" i] {
display: none !important;
}
/* Viva Insights */
html[data-or-hide-viva="true"] [aria-label*="Viva" i],
html[data-or-hide-viva="true"] [aria-label*="Daily Briefing" i],
html[data-or-hide-viva="true"] [aria-label*="Briefing" i] {
display: none !important;
}
/* Unread in Other banner */
html[data-or-hide-unreadother="true"] [aria-label*="unread in Other" i] {
display: none !important;
}
/* Activity feed */
html[data-or-hide-activity="true"] [aria-label*="Activity" i][role="complementary"],
html[data-or-hide-activity="true"] [aria-label*="mentioned you" i] {
display: none !important;
}
/* ============================================================
READABILITY
============================================================ */
/* Unread distinction: bold + left border */
html[data-or-unread-distinction="true"] [role="option"][aria-label*="Unread" i],
html[data-or-unread-distinction="true"] [role="listitem"][aria-label*="Unread" i] {
border-left: 3px solid var(--or-accent, #0078d4) !important;
font-weight: 600 !important;
}
/* Preview text on its own line */
html[data-or-preview-own-line="true"] [role="option"] [aria-hidden="true"],
html[data-or-preview-own-line="true"] [role="listitem"] span[title] {
display: block !important;
}
/* Normalize font weight */
html[data-or-normalize-font="true"] [role="listbox"],
html[data-or-normalize-font="true"] [role="list"] {
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
font-weight: 400 !important;
}
/* Dark mode email body fix */
html[data-outlook-relook-scheme="dark"][data-or-darkmode-fix="true"] [aria-label*="Message body" i],
html[data-outlook-relook-scheme="dark"][data-or-darkmode-fix="true"] [role="main"] iframe {
background-color: #1e1e1e !important;
color: #e0e0e0 !important;
color-scheme: dark !important;
}
/* Font size: small */
html[data-or-fontsize="small"] [role="listbox"],
html[data-or-fontsize="small"] [role="list"] {
font-size: 12px !important;
}
/* Font size: medium (default, no override needed) */
/* Font size: large */
html[data-or-fontsize="large"] [role="listbox"],
html[data-or-fontsize="large"] [role="list"] {
font-size: 15px !important;
}
/* ============================================================
OWA NATIVE SELECTION COLOR OVERRIDE
Replace Microsoft's purple/blue highlights with our accent color.
These target OWA's internal selection classes and states.
============================================================ */
html[data-outlook-relook-scheme] [role="option"][aria-selected="true"],
html[data-outlook-relook-scheme] [role="listitem"][aria-selected="true"],
html[data-outlook-relook-scheme] [role="option"].is-selected,
html[data-outlook-relook-scheme] [role="listitem"].is-selected {
background-color: var(--or-accent-subtle, rgba(0, 120, 212, 0.06)) !important;
border-left: 3px solid var(--or-accent, #0078d4) !important;
}
/* Override OWA's ::before/::after selection indicators */
html[data-outlook-relook-scheme] [role="option"][aria-selected="true"]::before,
html[data-outlook-relook-scheme] [role="listitem"][aria-selected="true"]::before {
background-color: var(--or-accent, #0078d4) !important;
}
/* Override OWA focus/hover highlight colors */
html[data-outlook-relook-scheme] [role="option"]:focus-visible,
html[data-outlook-relook-scheme] [role="listitem"]:focus-visible {
outline-color: var(--or-accent, #0078d4) !important;
}
/* Override the focused inbox tab active indicator */
html[data-outlook-relook-scheme] [role="tab"][aria-selected="true"]::after {
background-color: var(--or-accent, #0078d4) !important;
}
/* Override treeitem selection indicators */
html[data-outlook-relook-scheme] [role="treeitem"][aria-selected="true"]::before,
html[data-outlook-relook-scheme] [role="treeitem"][aria-current="true"]::before {
background-color: var(--or-accent, #0078d4) !important;
}
/* ============================================================
UNIFIED HEADER — Collapse all top bars into one compact surface
data-or-unified-header="true"
OWA header structure (outlook.cloud.microsoft, measured):
Row 1: div.Rn_96 — 48px — top bar (logo + search)
Row 2: div.root-109 — 36px — ribbon tabs (Home/View/Help)
Row 3: toolbar buttons — 40px — ribbon content
Row 4: Focused/Other + filters — starts at y=124
Total: ~124px of header before email content
Goal: compress to ~60px total
============================================================ */
/* --- Row 1: Top bar container (48px → 28px) --- */
html[data-or-unified-header="true"] [role="search"] {
height: 24px !important;
min-height: 24px !important;
max-height: 24px !important;
padding: 0 !important;
}
html[data-or-unified-header="true"] [role="search"] input {
height: 22px !important;
min-height: 22px !important;
font-size: 12px !important;
padding: 1px 8px !important;
}
/* Crush the top bar container's height and padding */
html[data-or-unified-header="true"] [role="search"],
html[data-or-unified-header="true"] [role="search"] ~ *,
html[data-or-unified-header="true"] [role="search"] > * {
padding-top: 0 !important;
padding-bottom: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
/* --- Row 2: Ribbon tabs (Home/View/Help) (36px → hide or minimize) --- */
/* The ribbon tab bar uses .ms-OverflowSet as a tablist */
html[data-or-unified-header="true"] .ms-OverflowSet[role="tablist"] {
max-height: 24px !important;
min-height: 20px !important;
padding: 0 !important;
}
html[data-or-unified-header="true"] .ms-OverflowSet[role="tablist"] [role="tab"] {
padding: 0 6px !important;
min-height: 20px !important;
max-height: 20px !important;
font-size: 11px !important;
}
/* --- Row 3: Ribbon toolbar content (40px → 26px) --- */
html[data-or-unified-header="true"] [role="toolbar"],
html[data-or-unified-header="true"] .fui-Toolbar {
max-height: 26px !important;
min-height: 24px !important;
padding: 0 4px !important;
overflow: hidden !important;
gap: 0 !important;
}
html[data-or-unified-header="true"] [role="toolbar"] button,
html[data-or-unified-header="true"] [role="toolbar"] .ms-Button,
html[data-or-unified-header="true"] .fui-Toolbar button {
padding: 1px 3px !important;
min-height: 22px !important;
max-height: 22px !important;
min-width: unset !important;
font-size: 11px !important;
}
/* Hide button text labels — icons only */
html[data-or-unified-header="true"] [role="toolbar"] .ms-Button-label,
html[data-or-unified-header="true"] [role="toolbar"] .ms-Button-textContainer,
html[data-or-unified-header="true"] .fui-Toolbar .ms-Button-label,
html[data-or-unified-header="true"] .fui-Toolbar .ms-Button-textContainer {
display: none !important;
}
/* Ribbon groups and overflow — compress */
html[data-or-unified-header="true"] .ms-OverflowSet:not([role="tablist"]) {
min-height: unset !important;
gap: 0 !important;
}
html[data-or-unified-header="true"] [role="group"] {
padding: 0 !important;
gap: 0 !important;
min-height: unset !important;
}
/* --- Row 4: Focused/Other tabs (fui-TabList) --- */
html[data-or-unified-header="true"] .fui-TabList {
max-height: 22px !important;
min-height: 20px !important;
padding: 0 !important;
gap: 0 !important;
}
html[data-or-unified-header="true"] .fui-TabList .fui-Tab {
padding: 0 8px !important;
min-height: 20px !important;
font-size: 11px !important;
}
/* --- Strip ALL vertical space from container divs in the header --- */
/* Target every ancestor div between the top of the page and content.
We use the known parent container selectors + general approach. */
html[data-or-unified-header="true"] [role="search"],
html[data-or-unified-header="true"] [role="toolbar"],
html[data-or-unified-header="true"] .fui-Toolbar,
html[data-or-unified-header="true"] .ms-OverflowSet,
html[data-or-unified-header="true"] .fui-TabList {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
/* Reduce height on the wrapping containers */
html[data-or-unified-header="true"] [role="search"] ~ *,
html[data-or-unified-header="true"] [role="toolbar"] ~ *,
html[data-or-unified-header="true"] .fui-Toolbar ~ * {
padding-top: 0 !important;
padding-bottom: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
/* ============================================================
KEYBOARD NAVIGATION
============================================================ */
/* Focus cursor — the message the keyboard is currently pointing at.
High specificity to beat theme selectors. */
html[data-outlook-relook-scheme] [role="option"].or-kb-focused,
html[data-outlook-relook-scheme] [role="listitem"].or-kb-focused,
html .or-kb-focused {
outline: 2px solid var(--or-accent, #0078d4) !important;
outline-offset: -2px;
position: relative;
z-index: 1;
}
/* Selected/checked messages — high specificity + strong visual */
html[data-outlook-relook-scheme] [role="option"].or-kb-selected,
html[data-outlook-relook-scheme] [role="listitem"].or-kb-selected,
html[data-outlook-relook-scheme="light"] [role="option"].or-kb-selected,
html[data-outlook-relook-scheme="light"] [role="listitem"].or-kb-selected,
html .or-kb-selected {
background-color: rgba(0, 120, 212, 0.12) !important;
border-left: 3px solid var(--or-accent, #0078d4) !important;
position: relative;
}
html[data-outlook-relook-scheme="dark"] [role="option"].or-kb-selected,
html[data-outlook-relook-scheme="dark"] [role="listitem"].or-kb-selected {
background-color: rgba(100, 181, 246, 0.18) !important;
border-left: 3px solid var(--or-accent, #64b5f6) !important;
}
/* Selection count badge (injected by keyboard.js) */
.or-kb-selection-count {
position: fixed;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
background: var(--or-bg-primary, #333);
color: var(--or-text-primary, #fff);
border: 1px solid var(--or-border, #555);
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
font-family: system-ui, sans-serif;
z-index: 999998;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
pointer-events: none;
transition: opacity 0.15s;
}

320
themes/brutalist.css Normal file
View File

@@ -0,0 +1,320 @@
/*
* Outlook Relook — Brutalist Theme
*
* Stark. Monospace. High contrast. Maximum information density.
* Borders and text, almost no fills. Inverted hover states.
* Inspired by terminal UIs, Craigslist, and concrete architecture.
*
* Scoped to specific elements — never uses `*` selectors.
*/
/* ============================================================
LIGHT VARIANT
============================================================ */
html[data-outlook-relook-scheme="light"] {
--or-bg-primary: #ffffff;
--or-bg-secondary: #ffffff;
--or-bg-tertiary: #ffffff;
--or-bg-hover: #000000;
--or-bg-selected: #000000;
--or-text-primary: #000000;
--or-text-secondary: #333333;
--or-text-tertiary: #666666;
--or-text-disabled: #999999;
--or-text-inverted: #ffffff;
--or-border: #000000;
--or-border-light: #cccccc;
--or-accent: var(--or-accent-override, #000000);
--or-accent-hover: var(--or-accent-override, #333333);
--or-shadow: none;
}
/* ============================================================
DARK VARIANT
============================================================ */
html[data-outlook-relook-scheme="dark"] {
--or-bg-primary: #000000;
--or-bg-secondary: #000000;
--or-bg-tertiary: #0a0a0a;
--or-bg-hover: #ffffff;
--or-bg-selected: #ffffff;
--or-text-primary: #ffffff;
--or-text-secondary: #cccccc;
--or-text-tertiary: #888888;
--or-text-disabled: #555555;
--or-text-inverted: #000000;
--or-border: #ffffff;
--or-border-light: #333333;
--or-accent: var(--or-accent-override, #ffffff);
--or-accent-hover: var(--or-accent-override, #cccccc);
--or-shadow: none;
}
/* ============================================================
TYPOGRAPHY — Monospace on text elements only (not icons)
============================================================ */
html[data-outlook-relook-scheme] body,
html[data-outlook-relook-scheme] div,
html[data-outlook-relook-scheme] span,
html[data-outlook-relook-scheme] p,
html[data-outlook-relook-scheme] h1,
html[data-outlook-relook-scheme] h2,
html[data-outlook-relook-scheme] h3,
html[data-outlook-relook-scheme] h4,
html[data-outlook-relook-scheme] h5,
html[data-outlook-relook-scheme] h6,
html[data-outlook-relook-scheme] a,
html[data-outlook-relook-scheme] li,
html[data-outlook-relook-scheme] td,
html[data-outlook-relook-scheme] th,
html[data-outlook-relook-scheme] label,
html[data-outlook-relook-scheme] input,
html[data-outlook-relook-scheme] button,
html[data-outlook-relook-scheme] select,
html[data-outlook-relook-scheme] textarea {
font-family: 'SF Mono', 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', monospace !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
letter-spacing: 0 !important;
}
html[data-outlook-relook-scheme] body {
font-size: 13px !important;
line-height: 1.5 !important;
}
/* ============================================================
BODY
============================================================ */
html[data-outlook-relook-scheme] body {
background-color: var(--or-bg-primary) !important;
color: var(--or-text-primary) !important;
}
/* ============================================================
TOP BAR — Heavy bottom border
============================================================ */
html[data-outlook-relook-scheme] [role="banner"],
html[data-outlook-relook-scheme] header {
background-color: var(--or-bg-primary) !important;
border-bottom: 2px solid var(--or-border) !important;
box-shadow: none !important;
}
/* ============================================================
NAVIGATION / FOLDER PANE — Heavy right border
============================================================ */
html[data-outlook-relook-scheme] [role="navigation"] {
background-color: var(--or-bg-primary) !important;
border-right: 2px solid var(--or-border) !important;
}
html[data-outlook-relook-scheme] [role="complementary"] {
background-color: var(--or-bg-primary) !important;
}
/* ============================================================
FOLDER TREE — Uppercase, dense, no fills
============================================================ */
html[data-outlook-relook-scheme] [role="treeitem"] {
color: var(--or-text-primary) !important;
background-color: transparent !important;
text-transform: uppercase !important;
font-size: 10px !important;
letter-spacing: 0.04em !important;
border-radius: 0 !important;
}
html[data-outlook-relook-scheme] [role="treeitem"]:hover {
background-color: var(--or-bg-hover) !important;
color: var(--or-text-inverted) !important;
}
html[data-outlook-relook-scheme] [role="treeitem"][aria-selected="true"],
html[data-outlook-relook-scheme] [role="treeitem"][aria-current="true"] {
font-weight: 900 !important;
background-color: transparent !important;
border-left: 3px solid var(--or-border) !important;
}
/* ============================================================
MESSAGE LIST
============================================================ */
html[data-outlook-relook-scheme] [role="listbox"],
html[data-outlook-relook-scheme] [role="list"] {
background-color: var(--or-bg-primary) !important;
}
/* Message rows — borders, no fills */
html[data-outlook-relook-scheme] [role="option"],
html[data-outlook-relook-scheme] [role="listitem"] {
background-color: transparent !important;
border-bottom: 1px solid var(--or-border-light) !important;
color: var(--or-text-primary) !important;
border-radius: 0 !important;
}
/* Hover: INVERT — the brutalist signature move */
html[data-outlook-relook-scheme] [role="option"]:hover,
html[data-outlook-relook-scheme] [role="listitem"]:hover {
background-color: var(--or-bg-hover) !important;
color: var(--or-text-inverted) !important;
}
/* Also invert child spans on hover */
html[data-outlook-relook-scheme] [role="option"]:hover span,
html[data-outlook-relook-scheme] [role="listitem"]:hover span,
html[data-outlook-relook-scheme] [role="option"]:hover time,
html[data-outlook-relook-scheme] [role="listitem"]:hover time {
color: var(--or-text-inverted) !important;
}
/* Selected: inverted */
html[data-outlook-relook-scheme] [role="option"][aria-selected="true"],
html[data-outlook-relook-scheme] [role="listitem"][aria-selected="true"] {
background-color: var(--or-bg-selected) !important;
color: var(--or-text-inverted) !important;
}
html[data-outlook-relook-scheme] [role="option"][aria-selected="true"] span,
html[data-outlook-relook-scheme] [role="listitem"][aria-selected="true"] span,
html[data-outlook-relook-scheme] [role="option"][aria-selected="true"] time,
html[data-outlook-relook-scheme] [role="listitem"][aria-selected="true"] time {
color: var(--or-text-inverted) !important;
}
/* ============================================================
READING PANE
============================================================ */
html[data-outlook-relook-scheme] [role="main"] {
background-color: var(--or-bg-primary) !important;
}
/* ============================================================
TOOLBAR — Understated, uppercase buttons
============================================================ */
html[data-outlook-relook-scheme] [role="toolbar"] {
background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border-light) !important;
box-shadow: none !important;
}
html[data-outlook-relook-scheme] [role="toolbar"] button,
html[data-outlook-relook-scheme] [role="toolbar"] [role="button"] {
background-color: transparent !important;
box-shadow: none !important;
background-image: none !important;
border-radius: 0 !important;
text-transform: uppercase !important;
font-size: 10px !important;
letter-spacing: 0.03em !important;
}
html[data-outlook-relook-scheme] [role="toolbar"] button:hover,
html[data-outlook-relook-scheme] [role="toolbar"] [role="button"]:hover {
background-color: var(--or-bg-hover) !important;
color: var(--or-text-inverted) !important;
}
/* ============================================================
BUTTONS — Outlined, never filled
============================================================ */
html[data-outlook-relook-scheme] button,
html[data-outlook-relook-scheme] [role="button"] {
box-shadow: none !important;
background-image: none !important;
}
html[data-outlook-relook-scheme] button:hover,
html[data-outlook-relook-scheme] [role="button"]:hover {
background-color: var(--or-bg-hover) !important;
color: var(--or-text-inverted) !important;
}
/* ============================================================
LINKS — Same as text, underlined
============================================================ */
html[data-outlook-relook-scheme] a {
color: var(--or-text-primary) !important;
text-decoration: underline !important;
}
html[data-outlook-relook-scheme] a:hover {
color: var(--or-text-inverted) !important;
background-color: var(--or-bg-hover) !important;
text-decoration: none !important;
}
/* ============================================================
SECONDARY TEXT
============================================================ */
html[data-outlook-relook-scheme] [role="option"] span,
html[data-outlook-relook-scheme] [role="listitem"] span {
color: var(--or-text-tertiary) !important;
font-size: 11px !important;
}
html[data-outlook-relook-scheme] time {
color: var(--or-text-tertiary) !important;
font-size: 10px !important;
text-transform: uppercase !important;
}
/* ============================================================
TABS
============================================================ */
html[data-outlook-relook-scheme] [role="tab"] {
border-radius: 0 !important;
box-shadow: none !important;
background-image: none !important;
text-transform: uppercase !important;
font-size: 10px !important;
letter-spacing: 0.04em !important;
}
html[data-outlook-relook-scheme] [role="tab"][aria-selected="true"] {
border-bottom: 2px solid var(--or-border) !important;
font-weight: 900 !important;
}
/* ============================================================
DIALOGS — Bordered, no fill, no rounding
============================================================ */
html[data-outlook-relook-scheme] [role="dialog"],
html[data-outlook-relook-scheme] [role="alertdialog"] {
background-color: var(--or-bg-primary) !important;
border: 2px solid var(--or-border) !important;
border-radius: 0 !important;
box-shadow: 8px 8px 0 var(--or-border) !important;
}
html[data-outlook-relook-scheme] [role="menu"] {
border-radius: 0 !important;
border: 1px solid var(--or-border) !important;
box-shadow: 4px 4px 0 var(--or-border) !important;
}
html[data-outlook-relook-scheme] [role="menuitem"] {
border-radius: 0 !important;
}
html[data-outlook-relook-scheme] [role="menuitem"]:hover {
background-color: var(--or-bg-hover) !important;
color: var(--or-text-inverted) !important;
}
/* ============================================================
SCROLLBARS — Hairline
============================================================ */
html[data-outlook-relook-scheme] ::-webkit-scrollbar {
width: 3px !important;
height: 3px !important;
}
html[data-outlook-relook-scheme] ::-webkit-scrollbar-track {
background: transparent !important;
}
html[data-outlook-relook-scheme] ::-webkit-scrollbar-thumb {
background: var(--or-text-primary) !important;
}

192
themes/material.css Normal file
View File

@@ -0,0 +1,192 @@
/*
* Outlook Relook — Material Flat Theme
* Clean, subtle elevation, comfortable density.
*/
/* ============================================================
LIGHT VARIANT
============================================================ */
html[data-outlook-relook-scheme="light"] {
--or-bg-primary: #ffffff;
--or-bg-secondary: #f5f5f5;
--or-bg-tertiary: #eeeeee;
--or-bg-hover: #e0e0e0;
--or-bg-selected: #e3f2fd;
--or-text-primary: #212121;
--or-text-secondary: #424242;
--or-text-tertiary: #757575;
--or-text-disabled: #9e9e9e;
--or-border: #e0e0e0;
--or-border-light: #eeeeee;
--or-accent: var(--or-accent-override, #1976d2);
--or-accent-hover: var(--or-accent-override, #1565c0);
--or-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
--or-shadow-elevated: 0 2px 6px rgba(0, 0, 0, 0.12);
}
/* ============================================================
DARK VARIANT
============================================================ */
html[data-outlook-relook-scheme="dark"] {
--or-bg-primary: #121212;
--or-bg-secondary: #1e1e1e;
--or-bg-tertiary: #252525;
--or-bg-hover: #333333;
--or-bg-selected: #1a3a5c;
--or-text-primary: #e0e0e0;
--or-text-secondary: #b0b0b0;
--or-text-tertiary: #808080;
--or-text-disabled: #5a5a5a;
--or-border: #333333;
--or-border-light: #2a2a2a;
--or-accent: var(--or-accent-override, #64b5f6);
--or-accent-hover: var(--or-accent-override, #90caf9);
--or-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
--or-shadow-elevated: 0 2px 6px rgba(0, 0, 0, 0.4);
}
/* ============================================================
TYPOGRAPHY
============================================================ */
html[data-outlook-relook-scheme] body,
html[data-outlook-relook-scheme] [role="main"],
html[data-outlook-relook-scheme] [role="navigation"],
html[data-outlook-relook-scheme] [role="complementary"],
html[data-outlook-relook-scheme] [role="banner"],
html[data-outlook-relook-scheme] input,
html[data-outlook-relook-scheme] button,
html[data-outlook-relook-scheme] select,
html[data-outlook-relook-scheme] textarea {
font-family: 'Roboto', system-ui, -apple-system, sans-serif !important;
letter-spacing: 0.01em !important;
}
/* ============================================================
SURFACES & BACKGROUNDS
============================================================ */
html[data-outlook-relook-scheme] body {
background-color: var(--or-bg-primary) !important;
color: var(--or-text-primary) !important;
}
html[data-outlook-relook-scheme] [role="banner"],
html[data-outlook-relook-scheme] header {
background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border) !important;
box-shadow: var(--or-shadow) !important;
}
html[data-outlook-relook-scheme] [role="navigation"],
html[data-outlook-relook-scheme] [role="complementary"] {
background-color: var(--or-bg-secondary) !important;
border-right: 1px solid var(--or-border-light) !important;
}
html[data-outlook-relook-scheme] [role="listbox"],
html[data-outlook-relook-scheme] [role="list"] {
background-color: var(--or-bg-primary) !important;
}
html[data-outlook-relook-scheme] [role="option"],
html[data-outlook-relook-scheme] [role="listitem"] {
background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border-light) !important;
color: var(--or-text-primary) !important;
}
html[data-outlook-relook-scheme] [role="option"]:hover,
html[data-outlook-relook-scheme] [role="listitem"]:hover {
background-color: var(--or-bg-hover) !important;
}
html[data-outlook-relook-scheme] [role="option"][aria-selected="true"],
html[data-outlook-relook-scheme] [role="listitem"][aria-selected="true"] {
background-color: var(--or-bg-selected) !important;
}
html[data-outlook-relook-scheme] [role="main"] {
background-color: var(--or-bg-primary) !important;
}
/* ============================================================
MATERIAL ROUNDING (4px on interactive elements only)
============================================================ */
html[data-outlook-relook-scheme] button,
html[data-outlook-relook-scheme] [role="button"],
html[data-outlook-relook-scheme] input,
html[data-outlook-relook-scheme] select,
html[data-outlook-relook-scheme] [role="tab"],
html[data-outlook-relook-scheme] [role="menuitem"] {
border-radius: 4px !important;
}
/* Cards / elevated surfaces */
html[data-outlook-relook-scheme] [role="dialog"],
html[data-outlook-relook-scheme] [role="alertdialog"] {
border-radius: 8px !important;
box-shadow: var(--or-shadow-elevated) !important;
}
/* ============================================================
BUTTONS & INTERACTIVE
============================================================ */
html[data-outlook-relook-scheme] button,
html[data-outlook-relook-scheme] [role="button"] {
box-shadow: none !important;
background-image: none !important;
transition: background-color 0.15s ease !important;
}
html[data-outlook-relook-scheme] button:hover,
html[data-outlook-relook-scheme] [role="button"]:hover {
background-color: var(--or-bg-hover) !important;
}
html[data-outlook-relook-scheme] a {
color: var(--or-accent) !important;
}
html[data-outlook-relook-scheme] a:hover {
color: var(--or-accent-hover) !important;
}
/* ============================================================
TOOLBAR
============================================================ */
html[data-outlook-relook-scheme] [role="toolbar"] {
background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border-light) !important;
box-shadow: var(--or-shadow) !important;
}
/* ============================================================
FOLDER PANE
============================================================ */
html[data-outlook-relook-scheme] [role="treeitem"] {
color: var(--or-text-secondary) !important;
border-radius: 4px !important;
margin: 1px 4px !important;
}
html[data-outlook-relook-scheme] [role="treeitem"]:hover {
background-color: var(--or-bg-hover) !important;
color: var(--or-text-primary) !important;
}
html[data-outlook-relook-scheme] [role="treeitem"][aria-selected="true"] {
background-color: var(--or-bg-selected) !important;
color: var(--or-accent) !important;
font-weight: 500 !important;
}
/* ============================================================
SECONDARY TEXT
============================================================ */
html[data-outlook-relook-scheme] [role="option"] span,
html[data-outlook-relook-scheme] [role="listitem"] span {
color: var(--or-text-secondary) !important;
}
html[data-outlook-relook-scheme] time {
color: var(--or-text-tertiary) !important;
}

298
themes/swiss.css Normal file
View File

@@ -0,0 +1,298 @@
/*
* Outlook Relook — Swiss / Helvetica Theme
*
* Inspired by the International Typographic Style.
* Helvetica Neue. Black, white, grey, and one accent color.
* Grid discipline. Content-first. Zero ornament.
*
* IMPORTANT: We scope surgical resets to specific OWA containers.
* Never use `*` selectors — they break SVG icons, checkboxes, etc.
*/
/* ============================================================
LIGHT VARIANT
============================================================ */
html[data-outlook-relook-scheme="light"] {
--or-bg-primary: #ffffff;
--or-bg-secondary: #fafafa;
--or-bg-tertiary: #f5f5f5;
--or-bg-hover: #f0f0f0;
--or-bg-selected: #f0f0f0;
--or-text-primary: #111111;
--or-text-secondary: #555555;
--or-text-tertiary: #888888;
--or-text-disabled: #bbbbbb;
--or-border: #d4d4d4;
--or-border-light: #ebebeb;
--or-accent: var(--or-accent-override, #e60000);
--or-accent-hover: var(--or-accent-override, #cc0000);
--or-accent-subtle: var(--or-accent-override, rgba(230, 0, 0, 0.06));
--or-shadow: none;
}
/* ============================================================
DARK VARIANT
============================================================ */
html[data-outlook-relook-scheme="dark"] {
--or-bg-primary: #111111;
--or-bg-secondary: #161616;
--or-bg-tertiary: #1a1a1a;
--or-bg-hover: #222222;
--or-bg-selected: #222222;
--or-text-primary: #ededed;
--or-text-secondary: #aaaaaa;
--or-text-tertiary: #666666;
--or-text-disabled: #444444;
--or-border: #333333;
--or-border-light: #232323;
--or-accent: var(--or-accent-override, #ff3b3b);
--or-accent-hover: var(--or-accent-override, #ff6b6b);
--or-accent-subtle: var(--or-accent-override, rgba(255, 59, 59, 0.1));
--or-shadow: none;
}
/* ============================================================
TYPOGRAPHY
Applied broadly but NOT with *, which breaks icon fonts/SVGs
============================================================ */
html[data-outlook-relook-scheme] body,
html[data-outlook-relook-scheme] div,
html[data-outlook-relook-scheme] span,
html[data-outlook-relook-scheme] p,
html[data-outlook-relook-scheme] h1,
html[data-outlook-relook-scheme] h2,
html[data-outlook-relook-scheme] h3,
html[data-outlook-relook-scheme] h4,
html[data-outlook-relook-scheme] h5,
html[data-outlook-relook-scheme] h6,
html[data-outlook-relook-scheme] a,
html[data-outlook-relook-scheme] li,
html[data-outlook-relook-scheme] td,
html[data-outlook-relook-scheme] th,
html[data-outlook-relook-scheme] label,
html[data-outlook-relook-scheme] input,
html[data-outlook-relook-scheme] button,
html[data-outlook-relook-scheme] select,
html[data-outlook-relook-scheme] textarea {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
}
/* Tighten letter-spacing on headings and UI text */
html[data-outlook-relook-scheme] [role="banner"],
html[data-outlook-relook-scheme] [role="toolbar"],
html[data-outlook-relook-scheme] [role="navigation"],
html[data-outlook-relook-scheme] [role="listbox"],
html[data-outlook-relook-scheme] [role="list"] {
letter-spacing: -0.01em !important;
}
/* ============================================================
BODY & MAIN SURFACES
============================================================ */
html[data-outlook-relook-scheme] body {
background-color: var(--or-bg-primary) !important;
color: var(--or-text-primary) !important;
}
/* ============================================================
TOP BAR / HEADER
Clean bottom edge, no shadow, no background noise
============================================================ */
html[data-outlook-relook-scheme] [role="banner"],
html[data-outlook-relook-scheme] header {
background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border) !important;
box-shadow: none !important;
}
/* ============================================================
NAVIGATION / FOLDER PANE
Subtle background, clean right edge
============================================================ */
html[data-outlook-relook-scheme] [role="navigation"] {
background-color: var(--or-bg-secondary) !important;
border-right: 1px solid var(--or-border) !important;
}
/* Complementary panels (right side panels) */
html[data-outlook-relook-scheme] [role="complementary"] {
background-color: var(--or-bg-secondary) !important;
}
/* ============================================================
FOLDER TREE — Clean text hierarchy
============================================================ */
html[data-outlook-relook-scheme] [role="treeitem"] {
color: var(--or-text-secondary) !important;
border-radius: 0 !important;
}
html[data-outlook-relook-scheme] [role="treeitem"]:hover {
background-color: var(--or-bg-hover) !important;
color: var(--or-text-primary) !important;
}
html[data-outlook-relook-scheme] [role="treeitem"][aria-selected="true"],
html[data-outlook-relook-scheme] [role="treeitem"][aria-current="true"] {
color: var(--or-text-primary) !important;
font-weight: 600 !important;
background-color: var(--or-bg-selected) !important;
border-left: 2px solid var(--or-accent) !important;
}
/* ============================================================
MESSAGE LIST
============================================================ */
html[data-outlook-relook-scheme] [role="listbox"],
html[data-outlook-relook-scheme] [role="list"] {
background-color: var(--or-bg-primary) !important;
}
/* Message rows */
html[data-outlook-relook-scheme] [role="option"],
html[data-outlook-relook-scheme] [role="listitem"] {
background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border-light) !important;
color: var(--or-text-primary) !important;
border-radius: 0 !important;
}
html[data-outlook-relook-scheme] [role="option"]:hover,
html[data-outlook-relook-scheme] [role="listitem"]:hover {
background-color: var(--or-bg-hover) !important;
}
/* Selected message — accent bar */
html[data-outlook-relook-scheme] [role="option"][aria-selected="true"],
html[data-outlook-relook-scheme] [role="listitem"][aria-selected="true"] {
background-color: var(--or-accent-subtle) !important;
border-left: 3px solid var(--or-accent) !important;
}
/* ============================================================
READING PANE
============================================================ */
html[data-outlook-relook-scheme] [role="main"] {
background-color: var(--or-bg-primary) !important;
}
/* ============================================================
TOOLBAR / COMMAND BAR
============================================================ */
html[data-outlook-relook-scheme] [role="toolbar"] {
background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border-light) !important;
box-shadow: none !important;
}
/* Toolbar buttons — muted, text-forward */
html[data-outlook-relook-scheme] [role="toolbar"] button,
html[data-outlook-relook-scheme] [role="toolbar"] [role="button"] {
border-radius: 2px !important;
box-shadow: none !important;
background-image: none !important;
}
html[data-outlook-relook-scheme] [role="toolbar"] button:hover,
html[data-outlook-relook-scheme] [role="toolbar"] [role="button"]:hover {
background-color: var(--or-bg-hover) !important;
}
/* ============================================================
BUTTONS — Minimal, no heavy fills
============================================================ */
html[data-outlook-relook-scheme] button,
html[data-outlook-relook-scheme] [role="button"] {
box-shadow: none !important;
background-image: none !important;
}
html[data-outlook-relook-scheme] button:hover,
html[data-outlook-relook-scheme] [role="button"]:hover {
background-color: var(--or-bg-hover) !important;
}
/* ============================================================
LINKS — Accent color
============================================================ */
html[data-outlook-relook-scheme] a {
color: var(--or-accent) !important;
}
html[data-outlook-relook-scheme] a:hover {
color: var(--or-accent-hover) !important;
}
/* ============================================================
SECONDARY TEXT — Clear typographic hierarchy
============================================================ */
html[data-outlook-relook-scheme] [role="option"] span,
html[data-outlook-relook-scheme] [role="listitem"] span {
color: var(--or-text-secondary) !important;
}
html[data-outlook-relook-scheme] time {
color: var(--or-text-tertiary) !important;
font-size: 11px !important;
}
/* ============================================================
TABS — Flat, no pill shapes
============================================================ */
html[data-outlook-relook-scheme] [role="tab"] {
border-radius: 0 !important;
box-shadow: none !important;
background-image: none !important;
}
html[data-outlook-relook-scheme] [role="tab"][aria-selected="true"] {
border-bottom: 2px solid var(--or-accent) !important;
}
/* ============================================================
DIALOGS & MENUS
============================================================ */
html[data-outlook-relook-scheme] [role="dialog"],
html[data-outlook-relook-scheme] [role="alertdialog"] {
background-color: var(--or-bg-primary) !important;
border: 1px solid var(--or-border) !important;
border-radius: 2px !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12) !important;
}
html[data-outlook-relook-scheme] [role="menu"],
html[data-outlook-relook-scheme] [role="listbox"][aria-expanded] {
border-radius: 2px !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important;
}
html[data-outlook-relook-scheme] [role="menuitem"] {
border-radius: 0 !important;
}
html[data-outlook-relook-scheme] [role="menuitem"]:hover {
background-color: var(--or-bg-hover) !important;
}
/* ============================================================
SCROLLBARS — Thin, monochrome
============================================================ */
html[data-outlook-relook-scheme] ::-webkit-scrollbar {
width: 6px !important;
height: 6px !important;
}
html[data-outlook-relook-scheme] ::-webkit-scrollbar-track {
background: transparent !important;
}
html[data-outlook-relook-scheme] ::-webkit-scrollbar-thumb {
background: var(--or-border) !important;
border-radius: 3px !important;
}
html[data-outlook-relook-scheme] ::-webkit-scrollbar-thumb:hover {
background: var(--or-text-tertiary) !important;
}