From 0c333a99f11d7d70b9af99421cd2dbf165e1e764 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 08:50:16 -0700 Subject: [PATCH 01/29] scaffold: manifest v3, stub content script and popup, placeholder icons --- content/behavior.js | 1 + content/content.js | 7 ++++++ content/injector.js | 1 + content/keyboard.js | 1 + content/observer.js | 1 + content/selectors.js | 1 + content/settings-defaults.js | 1 + icons/icon-128.png | Bin 0 -> 358 bytes icons/icon-16.png | Bin 0 -> 82 bytes icons/icon-48.png | Bin 0 -> 156 bytes manifest.json | 42 +++++++++++++++++++++++++++++++++++ popup/popup.html | 14 ++++++++++++ themes/base.css | 1 + 13 files changed, 70 insertions(+) create mode 100644 content/behavior.js create mode 100644 content/content.js create mode 100644 content/injector.js create mode 100644 content/keyboard.js create mode 100644 content/observer.js create mode 100644 content/selectors.js create mode 100644 content/settings-defaults.js create mode 100644 icons/icon-128.png create mode 100644 icons/icon-16.png create mode 100644 icons/icon-48.png create mode 100644 manifest.json create mode 100644 popup/popup.html create mode 100644 themes/base.css diff --git a/content/behavior.js b/content/behavior.js new file mode 100644 index 0000000..4eb3a4c --- /dev/null +++ b/content/behavior.js @@ -0,0 +1 @@ +// Outlook Relook — behavior.js (stub, implemented in later task) diff --git a/content/content.js b/content/content.js new file mode 100644 index 0000000..ea3f828 --- /dev/null +++ b/content/content.js @@ -0,0 +1,7 @@ +// Outlook Relook — Content Script Entry Point +// Loaded by manifest on outlook.office.com/* + +(function () { + 'use strict'; + console.log('[Outlook Relook] Content script loaded'); +})(); diff --git a/content/injector.js b/content/injector.js new file mode 100644 index 0000000..e611edf --- /dev/null +++ b/content/injector.js @@ -0,0 +1 @@ +// Outlook Relook — injector.js (stub, implemented in later task) diff --git a/content/keyboard.js b/content/keyboard.js new file mode 100644 index 0000000..213aca5 --- /dev/null +++ b/content/keyboard.js @@ -0,0 +1 @@ +// Outlook Relook — keyboard.js (stub, implemented in later task) diff --git a/content/observer.js b/content/observer.js new file mode 100644 index 0000000..6cbd191 --- /dev/null +++ b/content/observer.js @@ -0,0 +1 @@ +// Outlook Relook — observer.js (stub, implemented in later task) diff --git a/content/selectors.js b/content/selectors.js new file mode 100644 index 0000000..440b1fe --- /dev/null +++ b/content/selectors.js @@ -0,0 +1 @@ +// Outlook Relook — selectors.js (stub, implemented in later task) diff --git a/content/settings-defaults.js b/content/settings-defaults.js new file mode 100644 index 0000000..00160d2 --- /dev/null +++ b/content/settings-defaults.js @@ -0,0 +1 @@ +// Outlook Relook — settings-defaults.js (stub, implemented in later task) diff --git a/icons/icon-128.png b/icons/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..c058434b82c291ce3a6ccf19d0e73b3d88f0d034 GIT binary patch literal 358 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVAS<=aSW-L^Y)S=BZC3YfekNb z)X$&a)6CW)UQ+h`qUnBS0|vGO3=#~?2@E_9jAU~P**c6J<}g0u5lC;aU_K(LaE@UJ a3h~_tak*D?cF%tXAngxp*-nZ-xW2)P)8%lLcfk_mnXf@ors~;#nmMw2r~k)z4*}Q$iB} DG_);? literal 0 HcmV?d00001 diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..1973bd7 --- /dev/null +++ b/manifest.json @@ -0,0 +1,42 @@ +{ + "manifest_version": 3, + "name": "Outlook Relook", + "version": "0.1.0", + "description": "Minimalist reskin for Outlook Web App — clean themes, less clutter, more email.", + "permissions": ["storage", "activeTab"], + "content_scripts": [ + { + "matches": ["https://outlook.office.com/*", "https://outlook.office365.com/*"], + "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/*"] + } + ] +} diff --git a/popup/popup.html b/popup/popup.html new file mode 100644 index 0000000..7cf824a --- /dev/null +++ b/popup/popup.html @@ -0,0 +1,14 @@ + + + + + + + +

Outlook Relook

+

Settings panel coming soon.

+ + diff --git a/themes/base.css b/themes/base.css new file mode 100644 index 0000000..a689561 --- /dev/null +++ b/themes/base.css @@ -0,0 +1 @@ +/* Outlook Relook — base.css (stub, implemented in later task) */ From e1145c6f700d5a53b696b3eac708b7dbc31b5925 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 08:52:12 -0700 Subject: [PATCH 02/29] feat: settings defaults, storage layer, color scheme attribute --- content/content.js | 42 ++++++++++++- content/settings-defaults.js | 112 ++++++++++++++++++++++++++++++++++- 2 files changed, 151 insertions(+), 3 deletions(-) diff --git a/content/content.js b/content/content.js index ea3f828..6192352 100644 --- a/content/content.js +++ b/content/content.js @@ -1,7 +1,45 @@ // Outlook Relook — Content Script Entry Point -// Loaded by manifest on outlook.office.com/* (function () { 'use strict'; - console.log('[Outlook Relook] Content script loaded'); + + const OR = window.OutlookRelook; + + async function init() { + const settings = await OR.loadSettings(); + console.log('[Outlook Relook] Loaded settings:', settings); + + // Apply color scheme attribute + applyColorScheme(settings.colorScheme); + + // Listen for setting changes from popup + chrome.storage.onChanged.addListener((changes, area) => { + if (area !== 'sync') return; + console.log('[Outlook Relook] Settings changed:', changes); + + // Re-apply color scheme if it changed + if (changes.colorScheme) { + applyColorScheme(changes.colorScheme.newValue); + } + }); + } + + 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); + console.log('[Outlook Relook] Color scheme:', resolved); + } + + // Listen for system theme changes when scheme is 'system' + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', async () => { + const settings = await OR.loadSettings(); + if (settings.colorScheme === 'system') { + applyColorScheme('system'); + } + }); + + init(); })(); diff --git a/content/settings-defaults.js b/content/settings-defaults.js index 00160d2..4ff846d 100644 --- a/content/settings-defaults.js +++ b/content/settings-defaults.js @@ -1 +1,111 @@ -// Outlook Relook — settings-defaults.js (stub, implemented in later task) +// Outlook Relook — Settings Defaults +// Loaded first by manifest. Exposes window.OutlookRelook.DEFAULTS and helpers. + +window.OutlookRelook = window.OutlookRelook || {}; + +window.OutlookRelook.DEFAULTS = { + // 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, + + // Hide Elements + hideCopilot: true, + hideSuggestedReplies: true, + hidePromoBanners: true, + hideFocusedOtherTabs: true, + hideSidebarAppIcons: false, + hideGroupsSection: false, + hideMyDayButtons: true, + 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, + + // Keyboard Navigation + keyboardMultiSelect: true, + + // 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); + }); +}; From e50b11450fe7f01291b5e5da8594938ed3c1d885 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 08:55:06 -0700 Subject: [PATCH 03/29] feat: selector registry with resilient aria/data/structural selectors --- content/selectors.js | 166 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 1 deletion(-) diff --git a/content/selectors.js b/content/selectors.js index 440b1fe..f6e8e54 100644 --- a/content/selectors.js +++ b/content/selectors.js @@ -1 +1,165 @@ -// Outlook Relook — selectors.js (stub, implemented in later task) +// 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; +}; From 631e68cb639513ee8b0a06537d796d4b8313f73f Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 08:55:09 -0700 Subject: [PATCH 04/29] feat: base CSS theme with density, hiding, and readability overrides --- content/content.js | 110 ++++++++++++++++---- themes/base.css | 248 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 338 insertions(+), 20 deletions(-) diff --git a/content/content.js b/content/content.js index 6192352..6fb66ef 100644 --- a/content/content.js +++ b/content/content.js @@ -5,24 +5,38 @@ const OR = window.OutlookRelook; - async function init() { - const settings = await OR.loadSettings(); - console.log('[Outlook Relook] Loaded settings:', settings); + // Map setting keys to data attributes on + 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', + 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', + }; - // Apply color scheme attribute - applyColorScheme(settings.colorScheme); - - // Listen for setting changes from popup - chrome.storage.onChanged.addListener((changes, area) => { - if (area !== 'sync') return; - console.log('[Outlook Relook] Settings changed:', changes); - - // Re-apply color scheme if it changed - if (changes.colorScheme) { - applyColorScheme(changes.colorScheme.newValue); - } - }); - } + // Non-boolean attributes + const SETTING_TO_ATTR_VALUE = { + messageListFontSize: 'data-or-fontsize', + }; function applyColorScheme(scheme) { let resolved = scheme; @@ -30,10 +44,68 @@ resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } document.documentElement.setAttribute('data-outlook-relook-scheme', resolved); - console.log('[Outlook Relook] Color scheme:', resolved); } - // Listen for system theme changes when scheme is 'system' + function applySettingsToDOM(settings) { + // Boolean toggles -> data attributes + for (const [key, attr] of Object.entries(SETTING_TO_ATTR)) { + document.documentElement.setAttribute(attr, String(!!settings[key])); + } + + // Value-based attributes + for (const [key, attr] of Object.entries(SETTING_TO_ATTR_VALUE)) { + document.documentElement.setAttribute(attr, settings[key] || ''); + } + + // Color scheme + applyColorScheme(settings.colorScheme); + + // Accent color + if (settings.accentColor) { + document.documentElement.style.setProperty('--or-accent', settings.accentColor); + } + + console.log('[Outlook Relook] Settings applied to DOM'); + } + + function injectThemeCSS(theme) { + // Remove existing theme stylesheet + const existing = document.getElementById('outlook-relook-theme'); + if (existing) existing.remove(); + + // Inject the theme CSS + const 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('[Outlook Relook] Theme loaded: ' + theme); + } + + async function init() { + const settings = await OR.loadSettings(); + console.log('[Outlook Relook] Loaded settings:', settings); + + applySettingsToDOM(settings); + injectThemeCSS(settings.theme); + + // Listen for setting changes from popup + chrome.storage.onChanged.addListener((changes, area) => { + if (area !== 'sync') return; + + // Build updated settings object + OR.loadSettings().then((updated) => { + applySettingsToDOM(updated); + + // Swap theme CSS if theme changed + if (changes.theme) { + injectThemeCSS(changes.theme.newValue); + } + }); + }); + } + + // Listen for system theme changes window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', async () => { const settings = await OR.loadSettings(); if (settings.colorScheme === 'system') { diff --git a/themes/base.css b/themes/base.css index a689561..b7ad9cc 100644 --- a/themes/base.css +++ b/themes/base.css @@ -1 +1,247 @@ -/* Outlook Relook — base.css (stub, implemented in later task) */ +/* + * Outlook Relook — Base Theme + * Always loaded. Controls density, spacing, and element hiding. + * + * Toggle classes are set on 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 + ============================================================ */ + +/* Compact top bar */ +html[data-or-compact-topbar="true"] [role="banner"], +html[data-or-compact-topbar="true"] header { + max-height: 36px !important; + min-height: 36px !important; + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +html[data-or-compact-topbar="true"] [role="banner"] *, +html[data-or-compact-topbar="true"] header * { + font-size: 13px !important; +} + +/* Compact search bar inside top bar */ +html[data-or-compact-topbar="true"] [role="search"], +html[data-or-compact-topbar="true"] [role="search"] input { + height: 28px !important; + min-height: 28px !important; +} + +/* Compact command bar / ribbon */ +html[data-or-compact-commandbar="true"] [role="toolbar"] { + padding: 2px 4px !important; + min-height: unset !important; + gap: 2px !important; +} + +html[data-or-compact-commandbar="true"] [role="toolbar"] button { + padding: 2px 6px !important; + min-height: 28px !important; + font-size: 12px !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 */ +html[data-or-hide-focusedtabs="true"] [role="tablist"][aria-label*="Focused" i], +html[data-or-hide-focusedtabs="true"] [aria-label*="Focused Inbox" i] { + 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 buttons */ +html[data-or-hide-myday="true"] [aria-label*="My Day" i], +html[data-or-hide-myday="true"] [aria-label*="To Do" i][role="button"] { + 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; +} From 33b27f6061c130a9307931afbf9c63f69162bf1c Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 08:55:47 -0700 Subject: [PATCH 05/29] feat: Swiss/Helvetica theme with light and dark variants --- themes/swiss.css | 183 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 themes/swiss.css diff --git a/themes/swiss.css b/themes/swiss.css new file mode 100644 index 0000000..71b58ea --- /dev/null +++ b/themes/swiss.css @@ -0,0 +1,183 @@ +/* + * Outlook Relook — Swiss / Helvetica Theme + * Monochrome, tight grid, no decoration. Content density is king. + */ + +/* ============================================================ + LIGHT VARIANT + ============================================================ */ +html[data-outlook-relook-scheme="light"] { + --or-bg-primary: #ffffff; + --or-bg-secondary: #fafafa; + --or-bg-tertiary: #f0f0f0; + --or-bg-hover: #e8e8e8; + --or-bg-selected: #e0e0e0; + --or-text-primary: #000000; + --or-text-secondary: #333333; + --or-text-tertiary: #666666; + --or-text-disabled: #999999; + --or-border: #d0d0d0; + --or-border-light: #e5e5e5; + --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: #1a1a1a; + --or-bg-secondary: #222222; + --or-bg-tertiary: #2a2a2a; + --or-bg-hover: #333333; + --or-bg-selected: #3a3a3a; + --or-text-primary: #e8e8e8; + --or-text-secondary: #cccccc; + --or-text-tertiary: #999999; + --or-text-disabled: #666666; + --or-border: #3a3a3a; + --or-border-light: #2f2f2f; + --or-accent: var(--or-accent-override, #e8e8e8); + --or-accent-hover: var(--or-accent-override, #ffffff); + --or-shadow: none; +} + +/* ============================================================ + 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: 'Helvetica Neue', Helvetica, Arial, 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; +} + +/* Top bar */ +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; +} + +/* Folder pane */ +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; +} + +/* Message list */ +html[data-outlook-relook-scheme] [role="listbox"], +html[data-outlook-relook-scheme] [role="list"] { + background-color: var(--or-bg-primary) !important; +} + +/* Message list items */ +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; +} + +/* Reading pane */ +html[data-outlook-relook-scheme] [role="main"] { + background-color: var(--or-bg-primary) !important; +} + +/* ============================================================ + DECORATIVE REMOVAL + ============================================================ */ +html[data-outlook-relook-scheme] * { + border-radius: 0 !important; + text-shadow: none !important; +} + +html[data-outlook-relook-scheme] button, +html[data-outlook-relook-scheme] [role="button"], +html[data-outlook-relook-scheme] [role="tab"], +html[data-outlook-relook-scheme] [role="menuitem"] { + box-shadow: none !important; + background-image: none !important; +} + +/* ============================================================ + BUTTONS & INTERACTIVE + ============================================================ */ +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 / 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; +} + +/* ============================================================ + FOLDER PANE TREE ITEMS + ============================================================ */ +html[data-outlook-relook-scheme] [role="treeitem"] { + color: var(--or-text-secondary) !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-text-primary) !important; + font-weight: 600 !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; +} From 95eb17775402dae61eb26ee4fdc15244807aee22 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 08:56:15 -0700 Subject: [PATCH 06/29] feat: Material flat theme with light and dark variants --- themes/material.css | 192 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 themes/material.css diff --git a/themes/material.css b/themes/material.css new file mode 100644 index 0000000..6352185 --- /dev/null +++ b/themes/material.css @@ -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; +} From 7826046f7664c56ed5d85f2111a851c6397f369f Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 08:58:45 -0700 Subject: [PATCH 07/29] feat: MutationObserver for dynamic element suppression --- content/content.js | 18 +++++++ content/observer.js | 127 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 1 deletion(-) diff --git a/content/content.js b/content/content.js index 6fb66ef..66552cb 100644 --- a/content/content.js +++ b/content/content.js @@ -89,6 +89,15 @@ applySettingsToDOM(settings); injectThemeCSS(settings.theme); + // Start the MutationObserver + OR.Observer.start(settings); + + // Apply behavior patches + OR.Behavior.start(settings); + + // Start DOM injector (quick actions) + OR.Injector.start(settings); + // Listen for setting changes from popup chrome.storage.onChanged.addListener((changes, area) => { if (area !== 'sync') return; @@ -97,6 +106,15 @@ OR.loadSettings().then((updated) => { applySettingsToDOM(updated); + // Update observer with new settings + OR.Observer.updateSettings(updated); + + // Update behavior patches + OR.Behavior.updateSettings(updated); + + // Update injector + OR.Injector.updateSettings(updated); + // Swap theme CSS if theme changed if (changes.theme) { injectThemeCSS(changes.theme.newValue); diff --git a/content/observer.js b/content/observer.js index 6cbd191..608c78d 100644 --- a/content/observer.js +++ b/content/observer.js @@ -1 +1,126 @@ -// Outlook Relook — observer.js (stub, implemented in later task) +// 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('[Outlook Relook] 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('[Outlook Relook] 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('[Outlook Relook] Observer started'); + } + + function updateSettings(settings) { + currentSettings = settings; + // Re-run suppression with new settings + suppressElements(); + } + + function stop() { + if (observer) { + observer.disconnect(); + observer = null; + console.log('[Outlook Relook] Observer stopped'); + } + } + + return { start: start, updateSettings: updateSettings, stop: stop }; +})(); From 0862bbff2a9f8de6195fd34c1e0b86c4a8bb81f1 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 08:58:48 -0700 Subject: [PATCH 08/29] =?UTF-8?q?feat:=20behavior=20patches=20=E2=80=94=20?= =?UTF-8?q?auto-collapse,=20hover=20suppress,=20toast=20dismiss,=20auto-ad?= =?UTF-8?q?vance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/behavior.js | 292 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 291 insertions(+), 1 deletion(-) diff --git a/content/behavior.js b/content/behavior.js index 4eb3a4c..c6ee825 100644 --- a/content/behavior.js +++ b/content/behavior.js @@ -1 +1,291 @@ -// Outlook Relook — behavior.js (stub, implemented in later task) +// 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('[Outlook Relook] Auto-collapsed ribbon'); + } + } + }, 2000); + + cleanupFns.push(function () { clearTimeout(timer); }); + } + + // --- Remember sidebar collapsed/expanded state --- + function setupRememberSidebar() { + if (!currentSettings.rememberSidebarState) return; + + var savedState = localStorage.getItem('or-sidebar-collapsed'); + if (savedState === 'true') { + 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; + localStorage.setItem('or-sidebar-collapsed', String(!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('[Outlook Relook] 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('[Outlook Relook] 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('[Outlook Relook] 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 }; +})(); From 62157e67bccb7f6c7a395ac90bf23aeedc019d36 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 08:58:51 -0700 Subject: [PATCH 09/29] =?UTF-8?q?feat:=20DOM=20injector=20=E2=80=94=20mark?= =?UTF-8?q?-all-read=20button=20and=20folder=20jump=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/injector.js | 265 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 264 insertions(+), 1 deletion(-) diff --git a/content/injector.js b/content/injector.js index e611edf..9d8bc86 100644 --- a/content/injector.js +++ b/content/injector.js @@ -1 +1,264 @@ -// Outlook Relook — injector.js (stub, implemented in later task) +// 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('[Outlook Relook] Marked all as read'); + return; + } + } + // Close context menu if option not found + document.body.click(); + console.warn('[Outlook Relook] "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('[Outlook Relook] 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 }; +})(); From 2140b4468a0a79a386a315f1ebeb13bc883fef63 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 09:00:11 -0700 Subject: [PATCH 10/29] feat: selector test page with mock OWA DOM fragments --- selectors-test.html | 121 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 selectors-test.html diff --git a/selectors-test.html b/selectors-test.html new file mode 100644 index 0000000..258cfa4 --- /dev/null +++ b/selectors-test.html @@ -0,0 +1,121 @@ + + + + + Outlook Relook — Selector Test Page + + + +

Outlook Relook — Selector Test Page

+

Mock OWA DOM fragments. Click "Run Tests" to verify selectors match.

+ + +

Copilot

+
Copilot Button
+ +
Copilot Compose Suggestion
+ +

Suggested Replies

+
Sounds good! | Thanks! | Got it.
+ +

Promotional Banners

+ +
Upgrade to premium
+
Get the Outlook app
+ +

Focused/Other Tabs

+
+
Focused
+
Other
+
+ +

Sidebar App Icons

+ + +

Layout Regions

+ + +
+
Message Row
+
Read Message Row
+
+ + +

Behavior Targets

+ +
Ribbon Toggle
+ + + +
+ + + + From b59ee18a37713f41c2bb4a8d0bf8da63bcd21b10 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 09:00:29 -0700 Subject: [PATCH 11/29] docs: README with install, usage, and development guide --- README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..0998955 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Outlook Relook + +A Chrome extension that reskins Outlook Web App (outlook.office.com) with minimalist themes and granular control over visual clutter. + +## Features + +- **Switchable themes:** Swiss (Helvetica minimalism) and Material (clean, subtle elevation) +- **Light & Dark modes** with system preference detection +- **~40 granular toggles** for density, element hiding, readability, and behavior +- **MutationObserver** suppresses dynamically injected clutter (Copilot, banners, suggested replies) +- **Behavior patches:** auto-collapse ribbon, suppress contact hover cards, auto-advance after delete, toast management +- **Gmail-style keyboard navigation:** j/k to move, x/Space to multi-select, # delete, e archive, Shift+i/u read/unread, v move +- **Quick actions:** "Mark all as read" button, Ctrl+Shift+K folder jump +- **Settings sync** across Chrome instances via chrome.storage.sync +- **Export/Import/Reset** settings + +## Install (Development) + +1. Clone this repo +2. Open `chrome://extensions` in Chrome +3. Enable "Developer mode" (top-right toggle) +4. Click "Load unpacked" and select this directory +5. Open [outlook.office.com](https://outlook.office.com) — the extension activates automatically + +## Usage + +Click the extension icon to open the settings panel. Changes apply immediately — no page reload needed. + +### Keyboard Shortcuts + +**Message list (Gmail-style):** +- `j` / `Down Arrow` — Next message +- `k` / `Up Arrow` — Previous message +- `x` / `Space` — Toggle select +- `Shift+Down` / `Shift+Up` — Extend selection +- `#` — Delete selected +- `e` — Archive selected +- `Shift+i` — Mark read +- `Shift+u` — Mark unread +- `v` — Move to folder +- `Escape` — Deselect all +- `Enter` / `o` — Open message + +**Global:** +- `Ctrl+Shift+K` (or `Cmd+Shift+K` on Mac) — Quick folder jump + +## Development + +No build step. Edit files, reload the extension at `chrome://extensions`, refresh the OWA tab. + +- `themes/base.css` — density/spacing/hiding rules (always loaded) +- `themes/swiss.css` / `themes/material.css` — theme-specific styles +- `content/` — content scripts (observer, behavior, injector) +- `popup/` — settings panel +- `selectors-test.html` — offline selector verification + +### Selector Strategy + +OWA uses obfuscated class names. Selectors prioritize `aria-label`, `data-*`, and `role` attributes over class names. See `content/selectors.js` for the registry. + +When selectors break after an OWA update, check the console for `[Outlook Relook]` warnings, inspect the live DOM, and update `selectors.js`. + +## Adding a New Theme + +1. Create `themes/yourtheme.css` — define the same CSS custom properties as `swiss.css` +2. Add an option to the theme dropdown in `popup/popup.html` +3. That's it — the content script handles injection automatically + +## Adding a New Toggle + +1. Add the key and default to `DEFAULTS` in both `content/settings-defaults.js` and `popup/popup.js` +2. Add the toggle HTML to the appropriate section in `popup/popup.html` +3. If CSS-based: add a `data-or-*` attribute mapping in `content/content.js` and the CSS rule in `themes/base.css` +4. If observer-based: add a selector entry to `content/selectors.js` and a mapping in `content/observer.js` +5. If behavior-based: add a setup function in `content/behavior.js` From be70086e7e0763e67521fc0a6798d01c026bcfd2 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 09:00:53 -0700 Subject: [PATCH 12/29] feat: settings popup HTML and CSS with all toggle categories --- popup/popup.css | 246 ++++++++++++++++++++++++++++++++++++++++++++ popup/popup.html | 259 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 499 insertions(+), 6 deletions(-) create mode 100644 popup/popup.css diff --git a/popup/popup.css b/popup/popup.css new file mode 100644 index 0000000..73c731f --- /dev/null +++ b/popup/popup.css @@ -0,0 +1,246 @@ +/* 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 { + opacity: 0; + width: 0; + height: 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; +} diff --git a/popup/popup.html b/popup/popup.html index 7cf824a..ec8cf44 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -2,13 +2,260 @@ - + -

Outlook Relook

-

Settings panel coming soon.

+ +
+

Outlook Relook

+ +
+ + +
+
Theme & Appearance
+
+
+ + +
+
+ + + +
+
+ + +
+
+
+ + +
+
Density & Spacing
+
+
+ + +
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + +
+
Hide Elements
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + +
+
Readability
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
Behavior
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + +
+
Keyboard Navigation
+
+
+ +
+
+
+ j/k or arrows to navigate, x/Space to select, # delete, e archive, Shift+i read, Shift+u unread, v move, Esc deselect +
+
+
+ + +
+
Quick Actions
+
+
+ +
+
+
+ +
+
+
+
+ + + + + + + From 088bd45069228f0297e3f6daf414d43c8c65e9b3 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 09:01:23 -0700 Subject: [PATCH 13/29] =?UTF-8?q?feat:=20popup=20settings=20logic=20?= =?UTF-8?q?=E2=80=94=20load,=20save,=20presets,=20export/import/reset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- popup/popup.js | 243 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 popup/popup.js diff --git a/popup/popup.js b/popup/popup.js new file mode 100644 index 0000000..99a4bdd --- /dev/null +++ b/popup/popup.js @@ -0,0 +1,243 @@ +// Outlook Relook — Popup Settings Logic + +(function () { + 'use strict'; + + // We need access to DEFAULTS and DENSITY_PRESETS from settings-defaults.js, + // but popup runs in its own context. Duplicate the defaults here. + // (Content scripts and popup don't share a JS context.) + + var DEFAULTS = { + theme: 'swiss', + colorScheme: 'system', + accentColor: '', + densityPreset: 'compact', + compactTopBar: true, + compactCommandBar: true, + compactMessageList: true, + compactReadingPane: true, + compactFolderPane: true, + narrowDateColumn: true, + compressComposeToolbar: true, + readingPaneMaxWidth: true, + hideCopilot: true, + hideSuggestedReplies: true, + hidePromoBanners: true, + hideFocusedOtherTabs: true, + hideSidebarAppIcons: false, + hideGroupsSection: false, + hideMyDayButtons: true, + hideSenderAvatars: false, + hideFeatureDiscovery: true, + hideVivaInsights: true, + hideUnreadOtherBanner: true, + hideActivityFeed: true, + unreadDistinction: true, + previewOwnLine: false, + normalizeFontWeight: true, + darkModeEmailFix: true, + messageListFontSize: 'medium', + autoCollapseRibbon: true, + rememberSidebarState: true, + suppressContactHover: true, + autoAdvanceAfterDelete: true, + autoDismissToasts: '5', + toastPosition: 'top-right', + stickyReplyBar: true, + autoResizeCompose: true, + throttleNotifications: false, + keyboardMultiSelect: true, + markAllReadButton: true, + quickFolderJump: true, + }; + + var 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 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] || ''; + } + + // Radio groups + var radioGroups = document.querySelectorAll('.or-radio-group[data-setting]'); + for (var k = 0; k < radioGroups.length; k++) { + var key = radioGroups[k].dataset.setting; + var radio = radioGroups[k].querySelector('input[value="' + settings[key] + '"]'); + if (radio) radio.checked = true; + } + + // Color picker + var colorPicker = document.querySelector('input[type="color"][data-setting]'); + if (colorPicker) { + colorPicker.value = settings.accentColor || '#1976d2'; + } + + // Version + var manifest = chrome.runtime.getManifest(); + document.getElementById('version').textContent = 'v' + manifest.version; + }); + } + + // --- 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); + + // Density preset: apply to individual toggles + if (settingKey === 'densityPreset' && DENSITY_PRESETS[value]) { + var preset = DENSITY_PRESETS[value]; + chrome.storage.sync.set(preset); + // Update checkboxes in the UI + var toggleKeys = Object.keys(preset); + for (var p = 0; p < toggleKeys.length; p++) { + var checkbox = document.getElementById(toggleKeys[p]); + if (checkbox) checkbox.checked = preset[toggleKeys[p]]; + } + } + }); + } + + // --- Radio group change handlers --- + var radioGroups = document.querySelectorAll('.or-radio-group[data-setting]'); + for (var r = 0; r < radioGroups.length; r++) { + var groupKey = radioGroups[r].dataset.setting; + var radios = radioGroups[r].querySelectorAll('input[type="radio"]'); + for (var ri = 0; ri < radios.length; ri++) { + (function (gk) { + radios[ri].addEventListener('change', function () { + if (this.checked) saveSetting(gk, this.value); + }); + })(groupKey); + } + } + + // --- Color picker change handler --- + var colorPickers = document.querySelectorAll('input[type="color"][data-setting]'); + for (var cp = 0; cp < colorPickers.length; cp++) { + colorPickers[cp].addEventListener('input', function () { + saveSetting(this.dataset.setting, this.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 = 'outlook-relook-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); + // Only import known keys + 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(); +})(); From 311aa0e77198c94dda8a630201b7a4093fd426d4 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 09:03:29 -0700 Subject: [PATCH 14/29] feat: Gmail-style keyboard navigation and multi-select for message list --- content/content.js | 6 + content/keyboard.js | 464 +++++++++++++++++++++++++++++++++++++++++++- themes/base.css | 52 +++++ 3 files changed, 521 insertions(+), 1 deletion(-) diff --git a/content/content.js b/content/content.js index 66552cb..8dfcf33 100644 --- a/content/content.js +++ b/content/content.js @@ -95,6 +95,9 @@ // Apply behavior patches OR.Behavior.start(settings); + // Start keyboard navigation + OR.Keyboard.start(settings); + // Start DOM injector (quick actions) OR.Injector.start(settings); @@ -112,6 +115,9 @@ // Update behavior patches OR.Behavior.updateSettings(updated); + // Update keyboard navigation + OR.Keyboard.updateSettings(updated); + // Update injector OR.Injector.updateSettings(updated); diff --git a/content/keyboard.js b/content/keyboard.js index 213aca5..0bb342b 100644 --- a/content/keyboard.js +++ b/content/keyboard.js @@ -1 +1,463 @@ -// Outlook Relook — keyboard.js (stub, implemented in later task) +// Outlook Relook — Gmail-Style Keyboard Navigation & Multi-Select +// Adds keyboard focus cursor and multi-select to OWA's message list. +// Gated by the keyboardMultiSelect setting. + +window.OutlookRelook = window.OutlookRelook || {}; + +window.OutlookRelook.Keyboard = (function () { + 'use strict'; + + var OR = window.OutlookRelook; + var currentSettings = {}; + var cleanupFns = []; + + // State + var focusedIndex = -1; // Index of the focused message in the list + var selectedSet = new Set(); // Set of selected message DOM elements + var countBadge = null; // Selection count badge element + + // --- Helpers --- + + function getMessageItems() { + // Get all message items from the message list + var items = document.querySelectorAll( + '[role="listbox"] [role="option"], [role="list"] [role="listitem"]' + ); + return Array.from(items); + } + + function isComposeOrDialogActive() { + var active = document.activeElement; + if (!active) return false; + + // Check if focus is in a compose area, search bar, or dialog + 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; + } + + function setFocus(items, index) { + // Remove old focus + var oldFocused = document.querySelector('.or-kb-focused'); + if (oldFocused) oldFocused.classList.remove('or-kb-focused'); + + if (index < 0 || index >= items.length) return; + + focusedIndex = index; + var item = items[index]; + item.classList.add('or-kb-focused'); + + // Scroll into view if needed + item.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + + function toggleSelect(item) { + if (selectedSet.has(item)) { + selectedSet.delete(item); + item.classList.remove('or-kb-selected'); + } else { + selectedSet.add(item); + item.classList.add('or-kb-selected'); + } + updateCountBadge(); + } + + function clearSelection() { + selectedSet.forEach(function (item) { + item.classList.remove('or-kb-selected'); + }); + selectedSet.clear(); + updateCountBadge(); + } + + function getActionTargets(items) { + // If messages are selected, return those. Otherwise return the focused message. + if (selectedSet.size > 0) { + return Array.from(selectedSet); + } + if (focusedIndex >= 0 && focusedIndex < items.length) { + return [items[focusedIndex]]; + } + return []; + } + + // --- Selection count badge --- + + function createCountBadge() { + 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(); + var count = selectedSet.size; + if (count === 0) { + countBadge.style.opacity = '0'; + } else { + countBadge.textContent = count + ' selected'; + countBadge.style.opacity = '1'; + } + } + + // --- Actions --- + // Each action simulates what a user would do in OWA to perform the operation. + // We click the message to select it in OWA, then trigger the toolbar action. + + function performAction(targets, actionFn) { + if (targets.length === 0) return; + + // Process targets one at a time with a small delay between each + var i = 0; + function processNext() { + if (i >= targets.length) { + // After all targets processed, clear selection + clearSelection(); + return; + } + var target = targets[i]; + i++; + + // Click the message to make it OWA-selected + target.click(); + + // Small delay to let OWA register the selection, then perform action + setTimeout(function () { + actionFn(target); + // Delay before next target + setTimeout(processNext, 150); + }, 100); + } + processNext(); + } + + function actionDelete(targets) { + performAction(targets, function () { + // Try keyboard shortcut first (Delete key) + var deleteEvent = new KeyboardEvent('keydown', { + key: 'Delete', + code: 'Delete', + bubbles: true, + cancelable: true + }); + document.activeElement.dispatchEvent(deleteEvent); + + // Fallback: find and click the delete button in the toolbar + var deleteBtn = document.querySelector( + '[aria-label*="Delete" i][role="button"], [aria-label*="delete" i][role="menuitem"]' + ); + if (deleteBtn) deleteBtn.click(); + }); + } + + function actionArchive(targets) { + performAction(targets, function () { + var archiveBtn = document.querySelector( + '[aria-label*="Archive" i][role="button"], [aria-label*="archive" i][role="menuitem"]' + ); + if (archiveBtn) { + archiveBtn.click(); + } else { + console.warn('[Outlook Relook] Archive button not found'); + } + }); + } + + function actionMarkRead(targets) { + performAction(targets, function () { + var readBtn = document.querySelector( + '[aria-label*="Mark as read" i][role="button"], [aria-label*="Mark as read" i][role="menuitem"]' + ); + if (readBtn) { + readBtn.click(); + } else { + // Try context menu approach + triggerContextMenuAction(/mark as read/i); + } + }); + } + + function actionMarkUnread(targets) { + performAction(targets, function () { + var unreadBtn = document.querySelector( + '[aria-label*="Mark as unread" i][role="button"], [aria-label*="Mark as unread" i][role="menuitem"]' + ); + if (unreadBtn) { + unreadBtn.click(); + } else { + triggerContextMenuAction(/mark as unread/i); + } + }); + } + + function actionMove(targets) { + // For move, we just need to trigger OWA's move dialog on the current selection + if (targets.length > 0) { + // Click the first target to ensure something is selected + targets[0].click(); + + setTimeout(function () { + var moveBtn = document.querySelector( + '[aria-label*="Move to" i][role="button"], [aria-label*="Move" i][role="menuitem"]' + ); + if (moveBtn) { + moveBtn.click(); + } else { + triggerContextMenuAction(/move to/i); + } + }, 100); + } + } + + function triggerContextMenuAction(pattern) { + // Open context menu on the focused/selected element + var focused = document.querySelector('.or-kb-focused'); + if (!focused) return; + + var rect = focused.getBoundingClientRect(); + var contextEvent = new MouseEvent('contextmenu', { + bubbles: true, + cancelable: true, + clientX: rect.x + 10, + clientY: rect.y + 10, + }); + focused.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].click(); + return; + } + } + // Close context menu if not found + document.body.click(); + }, 300); + } + + // --- Key handler --- + + function handleKeydown(e) { + // Skip if keyboard mode is off or compose/dialog is active + if (!currentSettings.keyboardMultiSelect) return; + if (isComposeOrDialogActive()) return; + + var items = getMessageItems(); + if (items.length === 0) return; + + var key = e.key; + var shift = e.shiftKey; + + // Initialize focus if not set + if (focusedIndex < 0 || focusedIndex >= items.length) { + // Find the currently OWA-selected item, or start at 0 + for (var f = 0; f < items.length; f++) { + if (items[f].getAttribute('aria-selected') === 'true') { + focusedIndex = f; + break; + } + } + if (focusedIndex < 0) focusedIndex = 0; + } + + var handled = true; + var targets; + + switch (key) { + case 'j': + case 'ArrowDown': + if (shift) { + // Select current and move down + if (focusedIndex >= 0 && focusedIndex < items.length) { + if (!selectedSet.has(items[focusedIndex])) { + toggleSelect(items[focusedIndex]); + } + } + if (focusedIndex < items.length - 1) { + setFocus(items, focusedIndex + 1); + toggleSelect(items[focusedIndex]); + } + } else { + if (focusedIndex < items.length - 1) { + setFocus(items, focusedIndex + 1); + } + } + break; + + case 'k': + case 'ArrowUp': + if (shift) { + // Select current and move up + if (focusedIndex >= 0 && focusedIndex < items.length) { + if (!selectedSet.has(items[focusedIndex])) { + toggleSelect(items[focusedIndex]); + } + } + if (focusedIndex > 0) { + setFocus(items, focusedIndex - 1); + toggleSelect(items[focusedIndex]); + } + } else { + if (focusedIndex > 0) { + setFocus(items, focusedIndex - 1); + } + } + break; + + case 'x': + case ' ': + // Toggle select on focused message + e.preventDefault(); // Prevent page scroll on Space + if (focusedIndex >= 0 && focusedIndex < items.length) { + toggleSelect(items[focusedIndex]); + } + break; + + case '#': + // Delete selected/focused messages + targets = getActionTargets(items); + actionDelete(targets); + break; + + case 'e': + // Archive selected/focused messages + targets = getActionTargets(items); + actionArchive(targets); + break; + + case 'I': + // Shift+i — Mark as read + if (shift) { + targets = getActionTargets(items); + actionMarkRead(targets); + } else { + handled = false; + } + break; + + case 'U': + // Shift+u — Mark as unread + if (shift) { + targets = getActionTargets(items); + actionMarkUnread(targets); + } else { + handled = false; + } + break; + + case 'v': + // Move selected messages + targets = getActionTargets(items); + actionMove(targets); + break; + + case 'Escape': + // Deselect all + clearSelection(); + break; + + case 'Enter': + case 'o': + // Open focused message in reading pane + if (focusedIndex >= 0 && focusedIndex < items.length) { + items[focusedIndex].click(); + } + break; + + default: + handled = false; + } + + if (handled) { + e.stopPropagation(); + // Only preventDefault for keys we handle (except Enter which OWA should also process) + if (key !== 'Enter' && key !== 'o') { + e.preventDefault(); + } + } + } + + // --- Cleanup stale selections when message list re-renders --- + + function setupListObserver() { + var listObserver = new MutationObserver(function () { + // Remove stale entries from selectedSet (elements no longer in DOM) + selectedSet.forEach(function (item) { + if (!document.contains(item)) { + selectedSet.delete(item); + } + }); + updateCountBadge(); + + // Reset focusedIndex if the focused item is gone + var focusedEl = document.querySelector('.or-kb-focused'); + if (!focusedEl || !document.contains(focusedEl)) { + focusedIndex = -1; + } + }); + + // Watch the message list container for child changes + 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('[Outlook Relook] Keyboard navigation started'); + cleanupFns.push(function () { + document.removeEventListener('keydown', handleKeydown, true); + }); + } + + 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 = []; + + // Clean up DOM state + clearSelection(); + var focused = document.querySelector('.or-kb-focused'); + if (focused) focused.classList.remove('or-kb-focused'); + focusedIndex = -1; + + if (countBadge) { + countBadge.remove(); + countBadge = null; + } + } + + return { start: start, updateSettings: updateSettings, stop: stop }; +})(); diff --git a/themes/base.css b/themes/base.css index b7ad9cc..64bfc89 100644 --- a/themes/base.css +++ b/themes/base.css @@ -245,3 +245,55 @@ html[data-or-fontsize="large"] [role="listbox"], html[data-or-fontsize="large"] [role="list"] { font-size: 15px !important; } + + +/* ============================================================ + KEYBOARD NAVIGATION + ============================================================ */ + +/* Focus cursor — the message the keyboard is currently pointing at */ +.or-kb-focused { + outline: 2px solid var(--or-accent, #0078d4) !important; + outline-offset: -2px; + position: relative; + z-index: 1; +} + +/* Selected/checked messages */ +.or-kb-selected { + background-color: rgba(0, 120, 212, 0.08) !important; +} + +html[data-outlook-relook-scheme="dark"] .or-kb-selected { + background-color: rgba(100, 181, 246, 0.12) !important; +} + +/* Selection indicator — small left bar */ +.or-kb-selected::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: var(--or-accent, #0078d4); +} + +/* 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; +} From 3eec701eb4b4c8ac99edf714b501c1d7ef6cbcf3 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 09:07:15 -0700 Subject: [PATCH 15/29] fix: address code review findings - Fix accent color CSS variable: write to --or-accent-override - Fix actionDelete double-fire: toolbar button first, key fallback - Fix localStorage collision: use chrome.storage.local for sidebar - Fix updateSettings preserving keyboard selection state - Add document.body guard for count badge creation --- content/behavior.js | 27 ++++++++++++++------------- content/content.js | 2 +- content/keyboard.js | 36 +++++++++++++++++++++++------------- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/content/behavior.js b/content/behavior.js index c6ee825..ea55e27 100644 --- a/content/behavior.js +++ b/content/behavior.js @@ -32,26 +32,27 @@ window.OutlookRelook.Behavior = (function () { function setupRememberSidebar() { if (!currentSettings.rememberSidebarState) return; - var savedState = localStorage.getItem('or-sidebar-collapsed'); - if (savedState === 'true') { - 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(); + 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); }); - } + }, 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; - localStorage.setItem('or-sidebar-collapsed', String(!isVisible)); + chrome.storage.local.set({ sidebarCollapsed: !isVisible }); } }); diff --git a/content/content.js b/content/content.js index 8dfcf33..b465294 100644 --- a/content/content.js +++ b/content/content.js @@ -62,7 +62,7 @@ // Accent color if (settings.accentColor) { - document.documentElement.style.setProperty('--or-accent', settings.accentColor); + document.documentElement.style.setProperty('--or-accent-override', settings.accentColor); } console.log('[Outlook Relook] Settings applied to DOM'); diff --git a/content/keyboard.js b/content/keyboard.js index 0bb342b..04a9f2c 100644 --- a/content/keyboard.js +++ b/content/keyboard.js @@ -90,6 +90,7 @@ window.OutlookRelook.Keyboard = (function () { // --- 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'; @@ -99,6 +100,7 @@ window.OutlookRelook.Keyboard = (function () { function updateCountBadge() { if (!countBadge) countBadge = createCountBadge(); + if (!countBadge) return; var count = selectedSet.size; if (count === 0) { countBadge.style.opacity = '0'; @@ -141,20 +143,22 @@ window.OutlookRelook.Keyboard = (function () { function actionDelete(targets) { performAction(targets, function () { - // Try keyboard shortcut first (Delete key) - var deleteEvent = new KeyboardEvent('keydown', { - key: 'Delete', - code: 'Delete', - bubbles: true, - cancelable: true - }); - document.activeElement.dispatchEvent(deleteEvent); - - // Fallback: find and click the delete button in the toolbar + // Find and click the delete button in the toolbar var deleteBtn = document.querySelector( '[aria-label*="Delete" i][role="button"], [aria-label*="delete" i][role="menuitem"]' ); - if (deleteBtn) deleteBtn.click(); + if (deleteBtn) { + deleteBtn.click(); + } else { + // Fallback: try dispatching Delete key + var deleteEvent = new KeyboardEvent('keydown', { + key: 'Delete', + code: 'Delete', + bubbles: true, + cancelable: true + }); + document.activeElement.dispatchEvent(deleteEvent); + } }); } @@ -436,9 +440,15 @@ window.OutlookRelook.Keyboard = (function () { } function updateSettings(settings) { - stop(); + var wasEnabled = currentSettings.keyboardMultiSelect; + var isEnabled = settings.keyboardMultiSelect; currentSettings = settings; - start(settings); + + // Only tear down and restart if the keyboard setting itself changed + if (wasEnabled !== isEnabled) { + stop(); + start(settings); + } } function stop() { From 702bca43962e4ac9a858096369144f86c2e3e82b Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 09:16:01 -0700 Subject: [PATCH 16/29] fix: add outlook.cloud.microsoft URL pattern, fix toggle switch clickability --- manifest.json | 4 ++-- popup/popup.css | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/manifest.json b/manifest.json index 1973bd7..2034aeb 100644 --- a/manifest.json +++ b/manifest.json @@ -6,7 +6,7 @@ "permissions": ["storage", "activeTab"], "content_scripts": [ { - "matches": ["https://outlook.office.com/*", "https://outlook.office365.com/*"], + "matches": ["https://outlook.office.com/*", "https://outlook.office365.com/*", "https://outlook.cloud.microsoft/*"], "css": ["themes/base.css"], "js": [ "content/settings-defaults.js", @@ -36,7 +36,7 @@ "web_accessible_resources": [ { "resources": ["themes/*.css"], - "matches": ["https://outlook.office.com/*", "https://outlook.office365.com/*"] + "matches": ["https://outlook.office.com/*", "https://outlook.office365.com/*", "https://outlook.cloud.microsoft/*"] } ] } diff --git a/popup/popup.css b/popup/popup.css index 73c731f..4345e0d 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -104,9 +104,13 @@ body { } .or-switch input { + position: absolute; opacity: 0; - width: 0; - height: 0; + width: 100%; + height: 100%; + cursor: pointer; + z-index: 2; + margin: 0; } .or-switch .slider { From 6a6f9ba4f7159133bdfd2fbcdc243607caad7156 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 09:39:49 -0700 Subject: [PATCH 17/29] fix: keyboard multi-select uses stable IDs, improve Swiss theme, add Brutalist theme --- content/keyboard.js | 269 +++++++++++++++++++++++-------------------- popup/popup.html | 1 + themes/brutalist.css | 253 ++++++++++++++++++++++++++++++++++++++++ themes/swiss.css | 192 +++++++++++++++++++----------- 4 files changed, 517 insertions(+), 198 deletions(-) create mode 100644 themes/brutalist.css diff --git a/content/keyboard.js b/content/keyboard.js index 04a9f2c..330581a 100644 --- a/content/keyboard.js +++ b/content/keyboard.js @@ -1,6 +1,9 @@ // Outlook Relook — Gmail-Style 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 || {}; @@ -11,26 +14,42 @@ window.OutlookRelook.Keyboard = (function () { var currentSettings = {}; var cleanupFns = []; - // State - var focusedIndex = -1; // Index of the focused message in the list - var selectedSet = new Set(); // Set of selected message DOM elements - var countBadge = null; // Selection count badge element + // 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; - // --- Helpers --- + // --- 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() { - // Get all message items from the message list 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; - - // Check if focus is in a compose area, search bar, or dialog var tag = active.tagName.toLowerCase(); if (tag === 'input' || tag === 'textarea') return true; if (active.getAttribute('contenteditable') === 'true') return true; @@ -38,53 +57,90 @@ window.OutlookRelook.Keyboard = (function () { 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; } - function setFocus(items, index) { - // Remove old focus - var oldFocused = document.querySelector('.or-kb-focused'); - if (oldFocused) oldFocused.classList.remove('or-kb-focused'); + // --- 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. - if (index < 0 || index >= items.length) return; + 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'); - focusedIndex = index; - var item = items[index]; - item.classList.add('or-kb-focused'); + if (!items) items = getMessageItems(); - // Scroll into view if needed - item.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); - } - - function toggleSelect(item) { - if (selectedSet.has(item)) { - selectedSet.delete(item); - item.classList.remove('or-kb-selected'); - } else { - selectedSet.add(item); - item.classList.add('or-kb-selected'); + // 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() { - selectedSet.forEach(function (item) { - item.classList.remove('or-kb-selected'); - }); - selectedSet.clear(); + 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) { - // If messages are selected, return those. Otherwise return the focused message. - if (selectedSet.size > 0) { - return Array.from(selectedSet); + 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]); } - if (focusedIndex >= 0 && focusedIndex < items.length) { - return [items[focusedIndex]]; - } - return []; + return targets; } // --- Selection count badge --- @@ -101,7 +157,7 @@ window.OutlookRelook.Keyboard = (function () { function updateCountBadge() { if (!countBadge) countBadge = createCountBadge(); if (!countBadge) return; - var count = selectedSet.size; + var count = selectedIds.size; if (count === 0) { countBadge.style.opacity = '0'; } else { @@ -111,30 +167,20 @@ window.OutlookRelook.Keyboard = (function () { } // --- Actions --- - // Each action simulates what a user would do in OWA to perform the operation. - // We click the message to select it in OWA, then trigger the toolbar action. function performAction(targets, actionFn) { if (targets.length === 0) return; - - // Process targets one at a time with a small delay between each var i = 0; function processNext() { if (i >= targets.length) { - // After all targets processed, clear selection clearSelection(); return; } var target = targets[i]; i++; - - // Click the message to make it OWA-selected target.click(); - - // Small delay to let OWA register the selection, then perform action setTimeout(function () { actionFn(target); - // Delay before next target setTimeout(processNext, 150); }, 100); } @@ -143,19 +189,14 @@ window.OutlookRelook.Keyboard = (function () { function actionDelete(targets) { performAction(targets, function () { - // Find and click the delete button in the toolbar var deleteBtn = document.querySelector( '[aria-label*="Delete" i][role="button"], [aria-label*="delete" i][role="menuitem"]' ); if (deleteBtn) { deleteBtn.click(); } else { - // Fallback: try dispatching Delete key var deleteEvent = new KeyboardEvent('keydown', { - key: 'Delete', - code: 'Delete', - bubbles: true, - cancelable: true + key: 'Delete', code: 'Delete', bubbles: true, cancelable: true }); document.activeElement.dispatchEvent(deleteEvent); } @@ -183,7 +224,6 @@ window.OutlookRelook.Keyboard = (function () { if (readBtn) { readBtn.click(); } else { - // Try context menu approach triggerContextMenuAction(/mark as read/i); } }); @@ -203,11 +243,8 @@ window.OutlookRelook.Keyboard = (function () { } function actionMove(targets) { - // For move, we just need to trigger OWA's move dialog on the current selection if (targets.length > 0) { - // Click the first target to ensure something is selected targets[0].click(); - setTimeout(function () { var moveBtn = document.querySelector( '[aria-label*="Move to" i][role="button"], [aria-label*="Move" i][role="menuitem"]' @@ -222,19 +259,14 @@ window.OutlookRelook.Keyboard = (function () { } function triggerContextMenuAction(pattern) { - // Open context menu on the focused/selected element var focused = document.querySelector('.or-kb-focused'); if (!focused) return; - var rect = focused.getBoundingClientRect(); var contextEvent = new MouseEvent('contextmenu', { - bubbles: true, - cancelable: true, - clientX: rect.x + 10, - clientY: rect.y + 10, + bubbles: true, cancelable: true, + clientX: rect.x + 10, clientY: rect.y + 10, }); focused.dispatchEvent(contextEvent); - setTimeout(function () { var menuItems = document.querySelectorAll('[role="menuitem"]'); for (var j = 0; j < menuItems.length; j++) { @@ -243,7 +275,6 @@ window.OutlookRelook.Keyboard = (function () { return; } } - // Close context menu if not found document.body.click(); }, 300); } @@ -251,7 +282,6 @@ window.OutlookRelook.Keyboard = (function () { // --- Key handler --- function handleKeydown(e) { - // Skip if keyboard mode is off or compose/dialog is active if (!currentSettings.keyboardMultiSelect) return; if (isComposeOrDialogActive()) return; @@ -262,15 +292,21 @@ window.OutlookRelook.Keyboard = (function () { var shift = e.shiftKey; // Initialize focus if not set - if (focusedIndex < 0 || focusedIndex >= items.length) { - // Find the currently OWA-selected item, or start at 0 + var currentIdx = getFocusedIndex(items); + if (currentIdx < 0) { for (var f = 0; f < items.length; f++) { - if (items[f].getAttribute('aria-selected') === 'true') { - focusedIndex = f; + if (items[f].getAttribute('aria-selected') === 'true' || + items[f].classList.contains('is-selected')) { + currentIdx = f; + focusedId = getMessageId(items[f]); break; } } - if (focusedIndex < 0) focusedIndex = 0; + if (currentIdx < 0) { + currentIdx = 0; + focusedId = getMessageId(items[0]); + } + applyVisualState(items); } var handled = true; @@ -280,19 +316,14 @@ window.OutlookRelook.Keyboard = (function () { case 'j': case 'ArrowDown': if (shift) { - // Select current and move down - if (focusedIndex >= 0 && focusedIndex < items.length) { - if (!selectedSet.has(items[focusedIndex])) { - toggleSelect(items[focusedIndex]); - } - } - if (focusedIndex < items.length - 1) { - setFocus(items, focusedIndex + 1); - toggleSelect(items[focusedIndex]); + toggleSelect(items, currentIdx); + if (currentIdx < items.length - 1) { + setFocus(items, currentIdx + 1); + toggleSelect(items, currentIdx + 1); } } else { - if (focusedIndex < items.length - 1) { - setFocus(items, focusedIndex + 1); + if (currentIdx < items.length - 1) { + setFocus(items, currentIdx + 1); } } break; @@ -300,46 +331,35 @@ window.OutlookRelook.Keyboard = (function () { case 'k': case 'ArrowUp': if (shift) { - // Select current and move up - if (focusedIndex >= 0 && focusedIndex < items.length) { - if (!selectedSet.has(items[focusedIndex])) { - toggleSelect(items[focusedIndex]); - } - } - if (focusedIndex > 0) { - setFocus(items, focusedIndex - 1); - toggleSelect(items[focusedIndex]); + toggleSelect(items, currentIdx); + if (currentIdx > 0) { + setFocus(items, currentIdx - 1); + toggleSelect(items, currentIdx - 1); } } else { - if (focusedIndex > 0) { - setFocus(items, focusedIndex - 1); + if (currentIdx > 0) { + setFocus(items, currentIdx - 1); } } break; case 'x': case ' ': - // Toggle select on focused message - e.preventDefault(); // Prevent page scroll on Space - if (focusedIndex >= 0 && focusedIndex < items.length) { - toggleSelect(items[focusedIndex]); - } + e.preventDefault(); + toggleSelect(items, currentIdx); break; case '#': - // Delete selected/focused messages targets = getActionTargets(items); actionDelete(targets); break; case 'e': - // Archive selected/focused messages targets = getActionTargets(items); actionArchive(targets); break; case 'I': - // Shift+i — Mark as read if (shift) { targets = getActionTargets(items); actionMarkRead(targets); @@ -349,7 +369,6 @@ window.OutlookRelook.Keyboard = (function () { break; case 'U': - // Shift+u — Mark as unread if (shift) { targets = getActionTargets(items); actionMarkUnread(targets); @@ -359,21 +378,18 @@ window.OutlookRelook.Keyboard = (function () { break; case 'v': - // Move selected messages targets = getActionTargets(items); actionMove(targets); break; case 'Escape': - // Deselect all clearSelection(); break; case 'Enter': case 'o': - // Open focused message in reading pane - if (focusedIndex >= 0 && focusedIndex < items.length) { - items[focusedIndex].click(); + if (currentIdx >= 0 && currentIdx < items.length) { + items[currentIdx].click(); } break; @@ -383,33 +399,32 @@ window.OutlookRelook.Keyboard = (function () { if (handled) { e.stopPropagation(); - // Only preventDefault for keys we handle (except Enter which OWA should also process) if (key !== 'Enter' && key !== 'o') { e.preventDefault(); } } } - // --- Cleanup stale selections when message list re-renders --- + // --- Re-apply visual state when OWA re-renders the message list --- function setupListObserver() { var listObserver = new MutationObserver(function () { - // Remove stale entries from selectedSet (elements no longer in DOM) - selectedSet.forEach(function (item) { - if (!document.contains(item)) { - selectedSet.delete(item); - } - }); - updateCountBadge(); - - // Reset focusedIndex if the focused item is gone - var focusedEl = document.querySelector('.or-kb-focused'); - if (!focusedEl || !document.contains(focusedEl)) { - focusedIndex = -1; + // 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); }); - // Watch the message list container for child changes var checkInterval = setInterval(function () { var list = document.querySelector('[role="listbox"], [role="list"]'); if (list) { @@ -444,7 +459,6 @@ window.OutlookRelook.Keyboard = (function () { var isEnabled = settings.keyboardMultiSelect; currentSettings = settings; - // Only tear down and restart if the keyboard setting itself changed if (wasEnabled !== isEnabled) { stop(); start(settings); @@ -457,11 +471,10 @@ window.OutlookRelook.Keyboard = (function () { } cleanupFns = []; - // Clean up DOM state clearSelection(); var focused = document.querySelector('.or-kb-focused'); if (focused) focused.classList.remove('or-kb-focused'); - focusedIndex = -1; + focusedId = null; if (countBadge) { countBadge.remove(); diff --git a/popup/popup.html b/popup/popup.html index ec8cf44..eaf0bf5 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -20,6 +20,7 @@
diff --git a/themes/brutalist.css b/themes/brutalist.css new file mode 100644 index 0000000..86e1ee7 --- /dev/null +++ b/themes/brutalist.css @@ -0,0 +1,253 @@ +/* + * Outlook Relook — Brutalist Theme + * Stark minimalism. Monospace. Borders only, almost no fills. + * High contrast. Maximum information density. No comfort. + * Inspired by terminal UIs, tax forms, and concrete architecture. + */ + +/* ============================================================ + LIGHT VARIANT + ============================================================ */ +html[data-outlook-relook-scheme="light"] { + --or-bg-primary: #ffffff; + --or-bg-secondary: #ffffff; + --or-bg-tertiary: #ffffff; + --or-bg-hover: transparent; + --or-bg-selected: transparent; + --or-text-primary: #000000; + --or-text-secondary: #000000; + --or-text-tertiary: #555555; + --or-text-disabled: #999999; + --or-border: #000000; + --or-border-light: #000000; + --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: #000000; + --or-bg-hover: transparent; + --or-bg-selected: transparent; + --or-text-primary: #ffffff; + --or-text-secondary: #ffffff; + --or-text-tertiary: #aaaaaa; + --or-text-disabled: #555555; + --or-border: #ffffff; + --or-border-light: #ffffff; + --or-accent: var(--or-accent-override, #ffffff); + --or-accent-hover: var(--or-accent-override, #cccccc); + --or-shadow: none; +} + +/* ============================================================ + TYPOGRAPHY — Monospace, tight, utilitarian + ============================================================ */ +html[data-outlook-relook-scheme] *, +html[data-outlook-relook-scheme] *::before, +html[data-outlook-relook-scheme] *::after { + font-family: 'SF Mono', 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', monospace !important; + letter-spacing: -0.03em !important; + -webkit-font-smoothing: none !important; + font-smooth: never !important; +} + +html[data-outlook-relook-scheme] body { + font-size: 12px !important; +} + +/* ============================================================ + STRIP EVERYTHING — more aggressive than Swiss + ============================================================ */ +html[data-outlook-relook-scheme] * { + border-radius: 0 !important; + text-shadow: none !important; + box-shadow: none !important; + background-image: none !important; + transition: none !important; + animation: none !important; +} + +/* ============================================================ + SURFACES — pure white/black, no distinction between panes + ============================================================ */ +html[data-outlook-relook-scheme] body { + background-color: var(--or-bg-primary) !important; + color: var(--or-text-primary) !important; +} + +/* Top bar — just a 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; +} + +/* Folder pane — border only, no background change */ +html[data-outlook-relook-scheme] [role="navigation"], +html[data-outlook-relook-scheme] [role="complementary"] { + background-color: var(--or-bg-primary) !important; + border-right: 2px solid var(--or-border) !important; +} + +/* Message list — no background */ +html[data-outlook-relook-scheme] [role="listbox"], +html[data-outlook-relook-scheme] [role="list"] { + background-color: var(--or-bg-primary) !important; +} + +/* Message list items — borders only, 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) !important; + color: var(--or-text-primary) !important; + padding: 3px 8px !important; +} + +/* Hover: invert colors instead of fill */ +html[data-outlook-relook-scheme="light"] [role="option"]:hover, +html[data-outlook-relook-scheme="light"] [role="listitem"]:hover { + background-color: #000000 !important; + color: #ffffff !important; +} + +html[data-outlook-relook-scheme="dark"] [role="option"]:hover, +html[data-outlook-relook-scheme="dark"] [role="listitem"]:hover { + background-color: #ffffff !important; + color: #000000 !important; +} + +/* Selected: invert */ +html[data-outlook-relook-scheme="light"] [role="option"][aria-selected="true"], +html[data-outlook-relook-scheme="light"] [role="listitem"][aria-selected="true"] { + background-color: #000000 !important; + color: #ffffff !important; +} + +html[data-outlook-relook-scheme="dark"] [role="option"][aria-selected="true"], +html[data-outlook-relook-scheme="dark"] [role="listitem"][aria-selected="true"] { + background-color: #ffffff !important; + color: #000000 !important; +} + +/* Reading pane */ +html[data-outlook-relook-scheme] [role="main"] { + background-color: var(--or-bg-primary) !important; +} + +/* ============================================================ + TOOLBAR — stark, no visual weight + ============================================================ */ +html[data-outlook-relook-scheme] [role="toolbar"] { + background-color: var(--or-bg-primary) !important; + border-bottom: 1px solid var(--or-border) !important; +} + +html[data-outlook-relook-scheme] [role="toolbar"] button, +html[data-outlook-relook-scheme] [role="toolbar"] [role="button"] { + background-color: transparent !important; + border: 1px solid transparent !important; + color: var(--or-text-primary) !important; + text-transform: uppercase !important; + font-size: 10px !important; + letter-spacing: 0.05em !important; +} + +html[data-outlook-relook-scheme] [role="toolbar"] button:hover, +html[data-outlook-relook-scheme] [role="toolbar"] [role="button"]:hover { + border-color: var(--or-border) !important; + background-color: transparent !important; +} + +/* ============================================================ + BUTTONS — outlined only, never filled + ============================================================ */ +html[data-outlook-relook-scheme] button, +html[data-outlook-relook-scheme] [role="button"] { + background-color: transparent !important; + color: var(--or-text-primary) !important; + border: 1px solid transparent !important; +} + +html[data-outlook-relook-scheme] button:hover, +html[data-outlook-relook-scheme] [role="button"]:hover { + border-color: var(--or-border) !important; +} + +/* Links: same color 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 { + text-decoration: none !important; +} + +/* ============================================================ + FOLDER PANE — all caps, 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.06em !important; + border-bottom: 1px solid var(--or-border-light) !important; +} + +html[data-outlook-relook-scheme] [role="treeitem"]:hover { + text-decoration: underline !important; + background-color: transparent !important; +} + +html[data-outlook-relook-scheme] [role="treeitem"][aria-selected="true"] { + font-weight: 900 !important; + background-color: transparent !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; +} + +/* ============================================================ + DIALOGS — bordered box, no fill + ============================================================ */ +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; +} + +/* ============================================================ + SCROLLBARS — hairline + ============================================================ */ +html[data-outlook-relook-scheme] ::-webkit-scrollbar { + width: 2px !important; + height: 2px !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; +} diff --git a/themes/swiss.css b/themes/swiss.css index 71b58ea..a2fe681 100644 --- a/themes/swiss.css +++ b/themes/swiss.css @@ -1,6 +1,8 @@ /* * Outlook Relook — Swiss / Helvetica Theme - * Monochrome, tight grid, no decoration. Content density is king. + * Ruthlessly minimal. Black, white, grey, accent. Nothing else. + * Helvetica Neue everywhere. Zero decoration. Grid discipline. + * If an element doesn't serve the content, it should be invisible. */ /* ============================================================ @@ -8,18 +10,18 @@ ============================================================ */ html[data-outlook-relook-scheme="light"] { --or-bg-primary: #ffffff; - --or-bg-secondary: #fafafa; - --or-bg-tertiary: #f0f0f0; - --or-bg-hover: #e8e8e8; - --or-bg-selected: #e0e0e0; + --or-bg-secondary: #ffffff; + --or-bg-tertiary: #f7f7f7; + --or-bg-hover: #f0f0f0; + --or-bg-selected: #f0f0f0; --or-text-primary: #000000; - --or-text-secondary: #333333; - --or-text-tertiary: #666666; - --or-text-disabled: #999999; - --or-border: #d0d0d0; - --or-border-light: #e5e5e5; - --or-accent: var(--or-accent-override, #000000); - --or-accent-hover: var(--or-accent-override, #333333); + --or-text-secondary: #555555; + --or-text-tertiary: #888888; + --or-text-disabled: #bbbbbb; + --or-border: #000000; + --or-border-light: #e0e0e0; + --or-accent: var(--or-accent-override, #ff0000); + --or-accent-hover: var(--or-accent-override, #cc0000); --or-shadow: none; } @@ -27,71 +29,86 @@ html[data-outlook-relook-scheme="light"] { DARK VARIANT ============================================================ */ html[data-outlook-relook-scheme="dark"] { - --or-bg-primary: #1a1a1a; - --or-bg-secondary: #222222; - --or-bg-tertiary: #2a2a2a; - --or-bg-hover: #333333; - --or-bg-selected: #3a3a3a; - --or-text-primary: #e8e8e8; - --or-text-secondary: #cccccc; - --or-text-tertiary: #999999; - --or-text-disabled: #666666; - --or-border: #3a3a3a; - --or-border-light: #2f2f2f; - --or-accent: var(--or-accent-override, #e8e8e8); - --or-accent-hover: var(--or-accent-override, #ffffff); + --or-bg-primary: #0a0a0a; + --or-bg-secondary: #0a0a0a; + --or-bg-tertiary: #141414; + --or-bg-hover: #1a1a1a; + --or-bg-selected: #1a1a1a; + --or-text-primary: #ffffff; + --or-text-secondary: #aaaaaa; + --or-text-tertiary: #666666; + --or-text-disabled: #444444; + --or-border: #ffffff; + --or-border-light: #222222; + --or-accent: var(--or-accent-override, #ff3333); + --or-accent-hover: var(--or-accent-override, #ff6666); --or-shadow: none; } /* ============================================================ - TYPOGRAPHY + TYPOGRAPHY — Helvetica Neue, aggressively applied ============================================================ */ -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 { +html[data-outlook-relook-scheme] *, +html[data-outlook-relook-scheme] *::before, +html[data-outlook-relook-scheme] *::after { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif !important; - letter-spacing: -0.01em !important; + letter-spacing: -0.02em !important; + -webkit-font-smoothing: antialiased !important; + -moz-osx-font-smoothing: grayscale !important; } /* ============================================================ - SURFACES & BACKGROUNDS + STRIP ALL DECORATION — the core Swiss principle + ============================================================ */ +html[data-outlook-relook-scheme] * { + border-radius: 0 !important; + text-shadow: none !important; + box-shadow: none !important; + background-image: none !important; + text-decoration-color: var(--or-text-primary) !important; + outline-color: var(--or-accent) !important; +} + +/* Remove ALL gradients, transitions, and transforms from non-essential elements */ +html[data-outlook-relook-scheme] button, +html[data-outlook-relook-scheme] [role="button"], +html[data-outlook-relook-scheme] [role="tab"], +html[data-outlook-relook-scheme] [role="menuitem"], +html[data-outlook-relook-scheme] [role="option"], +html[data-outlook-relook-scheme] [role="listitem"], +html[data-outlook-relook-scheme] [role="treeitem"] { + transition: none !important; + animation: none !important; +} + +/* ============================================================ + SURFACES — flatten everything to white (or black in dark) ============================================================ */ html[data-outlook-relook-scheme] body { background-color: var(--or-bg-primary) !important; color: var(--or-text-primary) !important; } -/* Top bar */ 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; } -/* Folder pane */ html[data-outlook-relook-scheme] [role="navigation"], html[data-outlook-relook-scheme] [role="complementary"] { - background-color: var(--or-bg-secondary) !important; + background-color: var(--or-bg-primary) !important; border-right: 1px solid var(--or-border-light) !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 list items */ html[data-outlook-relook-scheme] [role="option"], html[data-outlook-relook-scheme] [role="listitem"] { - background-color: var(--or-bg-primary) !important; + background-color: transparent !important; border-bottom: 1px solid var(--or-border-light) !important; color: var(--or-text-primary) !important; } @@ -104,74 +121,81 @@ html[data-outlook-relook-scheme] [role="listitem"]:hover { 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; + border-left: 3px solid var(--or-accent) !important; } -/* Reading pane */ html[data-outlook-relook-scheme] [role="main"] { background-color: var(--or-bg-primary) !important; } /* ============================================================ - DECORATIVE REMOVAL + TOOLBAR — minimal, no background distinction ============================================================ */ -html[data-outlook-relook-scheme] * { - border-radius: 0 !important; - text-shadow: none !important; +html[data-outlook-relook-scheme] [role="toolbar"] { + background-color: var(--or-bg-primary) !important; + border-bottom: 1px solid var(--or-border-light) !important; } -html[data-outlook-relook-scheme] button, -html[data-outlook-relook-scheme] [role="button"], -html[data-outlook-relook-scheme] [role="tab"], -html[data-outlook-relook-scheme] [role="menuitem"] { - box-shadow: none !important; - background-image: none !important; +/* Toolbar buttons: text only, no background */ +html[data-outlook-relook-scheme] [role="toolbar"] button, +html[data-outlook-relook-scheme] [role="toolbar"] [role="button"] { + background-color: transparent !important; + border: none !important; + color: var(--or-text-primary) !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 & INTERACTIVE + BUTTONS — understate everything ============================================================ */ +html[data-outlook-relook-scheme] button, +html[data-outlook-relook-scheme] [role="button"] { + background-color: transparent !important; + color: var(--or-text-primary) !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, no underline by default */ html[data-outlook-relook-scheme] a { color: var(--or-accent) !important; + text-decoration: none !important; } html[data-outlook-relook-scheme] a:hover { + text-decoration: underline !important; color: var(--or-accent-hover) !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; -} - -/* ============================================================ - FOLDER PANE TREE ITEMS + FOLDER PANE — text hierarchy only, no visual chrome ============================================================ */ html[data-outlook-relook-scheme] [role="treeitem"] { color: var(--or-text-secondary) !important; + background-color: transparent !important; } html[data-outlook-relook-scheme] [role="treeitem"]:hover { - background-color: var(--or-bg-hover) !important; color: var(--or-text-primary) !important; + background-color: transparent !important; + text-decoration: underline !important; } html[data-outlook-relook-scheme] [role="treeitem"][aria-selected="true"] { - background-color: var(--or-bg-selected) !important; color: var(--or-text-primary) !important; - font-weight: 600 !important; + font-weight: 700 !important; + background-color: transparent !important; } /* ============================================================ - SECONDARY TEXT + SECONDARY TEXT — clear hierarchy ============================================================ */ html[data-outlook-relook-scheme] [role="option"] span, html[data-outlook-relook-scheme] [role="listitem"] span { @@ -180,4 +204,32 @@ html[data-outlook-relook-scheme] [role="listitem"] span { html[data-outlook-relook-scheme] time { color: var(--or-text-tertiary) !important; + font-size: 11px !important; + text-transform: uppercase !important; + letter-spacing: 0.03em !important; +} + +/* ============================================================ + DIALOGS & OVERLAYS + ============================================================ */ +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; +} + +/* ============================================================ + SCROLLBARS — thin, monochrome + ============================================================ */ +html[data-outlook-relook-scheme] ::-webkit-scrollbar { + width: 4px !important; + height: 4px !important; +} + +html[data-outlook-relook-scheme] ::-webkit-scrollbar-track { + background: transparent !important; +} + +html[data-outlook-relook-scheme] ::-webkit-scrollbar-thumb { + background: var(--or-text-tertiary) !important; } From 41f39fd5397baec7c5c63bc40f01365fb52e33d7 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 09:49:57 -0700 Subject: [PATCH 18/29] fix: rewrite Swiss and Brutalist themes with surgical selectors, no more broken icons --- themes/brutalist.css | 344 ++++++++++++++++++++++++++----------------- themes/swiss.css | 243 +++++++++++++++++++----------- 2 files changed, 358 insertions(+), 229 deletions(-) diff --git a/themes/brutalist.css b/themes/brutalist.css index 86e1ee7..7ca9b5a 100644 --- a/themes/brutalist.css +++ b/themes/brutalist.css @@ -1,8 +1,11 @@ /* * Outlook Relook — Brutalist Theme - * Stark minimalism. Monospace. Borders only, almost no fills. - * High contrast. Maximum information density. No comfort. - * Inspired by terminal UIs, tax forms, and concrete architecture. + * + * 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. */ /* ============================================================ @@ -12,14 +15,15 @@ html[data-outlook-relook-scheme="light"] { --or-bg-primary: #ffffff; --or-bg-secondary: #ffffff; --or-bg-tertiary: #ffffff; - --or-bg-hover: transparent; - --or-bg-selected: transparent; + --or-bg-hover: #000000; + --or-bg-selected: #000000; --or-text-primary: #000000; - --or-text-secondary: #000000; - --or-text-tertiary: #555555; + --or-text-secondary: #333333; + --or-text-tertiary: #666666; --or-text-disabled: #999999; + --or-text-inverted: #ffffff; --or-border: #000000; - --or-border-light: #000000; + --or-border-light: #cccccc; --or-accent: var(--or-accent-override, #000000); --or-accent-hover: var(--or-accent-override, #333333); --or-shadow: none; @@ -31,185 +35,213 @@ html[data-outlook-relook-scheme="light"] { html[data-outlook-relook-scheme="dark"] { --or-bg-primary: #000000; --or-bg-secondary: #000000; - --or-bg-tertiary: #000000; - --or-bg-hover: transparent; - --or-bg-selected: transparent; + --or-bg-tertiary: #0a0a0a; + --or-bg-hover: #ffffff; + --or-bg-selected: #ffffff; --or-text-primary: #ffffff; - --or-text-secondary: #ffffff; - --or-text-tertiary: #aaaaaa; + --or-text-secondary: #cccccc; + --or-text-tertiary: #888888; --or-text-disabled: #555555; + --or-text-inverted: #000000; --or-border: #ffffff; - --or-border-light: #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, tight, utilitarian + TYPOGRAPHY — Monospace on text elements only (not icons) ============================================================ */ -html[data-outlook-relook-scheme] *, -html[data-outlook-relook-scheme] *::before, -html[data-outlook-relook-scheme] *::after { +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; - letter-spacing: -0.03em !important; -webkit-font-smoothing: none !important; - font-smooth: never !important; + letter-spacing: -0.02em !important; } +/* Smaller base size for density */ html[data-outlook-relook-scheme] body { font-size: 12px !important; } /* ============================================================ - STRIP EVERYTHING — more aggressive than Swiss - ============================================================ */ -html[data-outlook-relook-scheme] * { - border-radius: 0 !important; - text-shadow: none !important; - box-shadow: none !important; - background-image: none !important; - transition: none !important; - animation: none !important; -} - -/* ============================================================ - SURFACES — pure white/black, no distinction between panes + BODY ============================================================ */ html[data-outlook-relook-scheme] body { background-color: var(--or-bg-primary) !important; color: var(--or-text-primary) !important; } -/* Top bar — just a bottom border */ +/* ============================================================ + 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; } -/* Folder pane — border only, no background change */ -html[data-outlook-relook-scheme] [role="navigation"], -html[data-outlook-relook-scheme] [role="complementary"] { +/* ============================================================ + 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; } -/* Message list — no background */ -html[data-outlook-relook-scheme] [role="listbox"], -html[data-outlook-relook-scheme] [role="list"] { - background-color: var(--or-bg-primary) !important; -} - -/* Message list items — borders only, 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) !important; - color: var(--or-text-primary) !important; - padding: 3px 8px !important; -} - -/* Hover: invert colors instead of fill */ -html[data-outlook-relook-scheme="light"] [role="option"]:hover, -html[data-outlook-relook-scheme="light"] [role="listitem"]:hover { - background-color: #000000 !important; - color: #ffffff !important; -} - -html[data-outlook-relook-scheme="dark"] [role="option"]:hover, -html[data-outlook-relook-scheme="dark"] [role="listitem"]:hover { - background-color: #ffffff !important; - color: #000000 !important; -} - -/* Selected: invert */ -html[data-outlook-relook-scheme="light"] [role="option"][aria-selected="true"], -html[data-outlook-relook-scheme="light"] [role="listitem"][aria-selected="true"] { - background-color: #000000 !important; - color: #ffffff !important; -} - -html[data-outlook-relook-scheme="dark"] [role="option"][aria-selected="true"], -html[data-outlook-relook-scheme="dark"] [role="listitem"][aria-selected="true"] { - background-color: #ffffff !important; - color: #000000 !important; -} - -/* Reading pane */ -html[data-outlook-relook-scheme] [role="main"] { +html[data-outlook-relook-scheme] [role="complementary"] { background-color: var(--or-bg-primary) !important; } /* ============================================================ - TOOLBAR — stark, no visual weight - ============================================================ */ -html[data-outlook-relook-scheme] [role="toolbar"] { - background-color: var(--or-bg-primary) !important; - border-bottom: 1px solid var(--or-border) !important; -} - -html[data-outlook-relook-scheme] [role="toolbar"] button, -html[data-outlook-relook-scheme] [role="toolbar"] [role="button"] { - background-color: transparent !important; - border: 1px solid transparent !important; - color: var(--or-text-primary) !important; - text-transform: uppercase !important; - font-size: 10px !important; - letter-spacing: 0.05em !important; -} - -html[data-outlook-relook-scheme] [role="toolbar"] button:hover, -html[data-outlook-relook-scheme] [role="toolbar"] [role="button"]:hover { - border-color: var(--or-border) !important; - background-color: transparent !important; -} - -/* ============================================================ - BUTTONS — outlined only, never filled - ============================================================ */ -html[data-outlook-relook-scheme] button, -html[data-outlook-relook-scheme] [role="button"] { - background-color: transparent !important; - color: var(--or-text-primary) !important; - border: 1px solid transparent !important; -} - -html[data-outlook-relook-scheme] button:hover, -html[data-outlook-relook-scheme] [role="button"]:hover { - border-color: var(--or-border) !important; -} - -/* Links: same color 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 { - text-decoration: none !important; -} - -/* ============================================================ - FOLDER PANE — all caps, no fills + 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.06em !important; - border-bottom: 1px solid var(--or-border-light) !important; + letter-spacing: 0.04em !important; + border-radius: 0 !important; } html[data-outlook-relook-scheme] [role="treeitem"]:hover { - text-decoration: underline !important; - background-color: transparent !important; + 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-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; } /* ============================================================ @@ -228,20 +260,54 @@ html[data-outlook-relook-scheme] time { } /* ============================================================ - DIALOGS — bordered box, no fill + 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 + SCROLLBARS — Hairline ============================================================ */ html[data-outlook-relook-scheme] ::-webkit-scrollbar { - width: 2px !important; - height: 2px !important; + width: 3px !important; + height: 3px !important; } html[data-outlook-relook-scheme] ::-webkit-scrollbar-track { diff --git a/themes/swiss.css b/themes/swiss.css index a2fe681..ce5c1f1 100644 --- a/themes/swiss.css +++ b/themes/swiss.css @@ -1,8 +1,12 @@ /* * Outlook Relook — Swiss / Helvetica Theme - * Ruthlessly minimal. Black, white, grey, accent. Nothing else. - * Helvetica Neue everywhere. Zero decoration. Grid discipline. - * If an element doesn't serve the content, it should be invisible. + * + * 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. */ /* ============================================================ @@ -10,18 +14,19 @@ ============================================================ */ html[data-outlook-relook-scheme="light"] { --or-bg-primary: #ffffff; - --or-bg-secondary: #ffffff; - --or-bg-tertiary: #f7f7f7; + --or-bg-secondary: #fafafa; + --or-bg-tertiary: #f5f5f5; --or-bg-hover: #f0f0f0; --or-bg-selected: #f0f0f0; - --or-text-primary: #000000; + --or-text-primary: #111111; --or-text-secondary: #555555; --or-text-tertiary: #888888; --or-text-disabled: #bbbbbb; - --or-border: #000000; - --or-border-light: #e0e0e0; - --or-accent: var(--or-accent-override, #ff0000); + --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; } @@ -29,88 +34,129 @@ html[data-outlook-relook-scheme="light"] { DARK VARIANT ============================================================ */ html[data-outlook-relook-scheme="dark"] { - --or-bg-primary: #0a0a0a; - --or-bg-secondary: #0a0a0a; - --or-bg-tertiary: #141414; - --or-bg-hover: #1a1a1a; - --or-bg-selected: #1a1a1a; - --or-text-primary: #ffffff; + --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: #ffffff; - --or-border-light: #222222; - --or-accent: var(--or-accent-override, #ff3333); - --or-accent-hover: var(--or-accent-override, #ff6666); + --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 — Helvetica Neue, aggressively applied + TYPOGRAPHY + Applied broadly but NOT with *, which breaks icon fonts/SVGs ============================================================ */ -html[data-outlook-relook-scheme] *, -html[data-outlook-relook-scheme] *::before, -html[data-outlook-relook-scheme] *::after { +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; - letter-spacing: -0.02em !important; -webkit-font-smoothing: antialiased !important; -moz-osx-font-smoothing: grayscale !important; } -/* ============================================================ - STRIP ALL DECORATION — the core Swiss principle - ============================================================ */ -html[data-outlook-relook-scheme] * { - border-radius: 0 !important; - text-shadow: none !important; - box-shadow: none !important; - background-image: none !important; - text-decoration-color: var(--or-text-primary) !important; - outline-color: var(--or-accent) !important; -} - -/* Remove ALL gradients, transitions, and transforms from non-essential elements */ -html[data-outlook-relook-scheme] button, -html[data-outlook-relook-scheme] [role="button"], -html[data-outlook-relook-scheme] [role="tab"], -html[data-outlook-relook-scheme] [role="menuitem"], -html[data-outlook-relook-scheme] [role="option"], -html[data-outlook-relook-scheme] [role="listitem"], -html[data-outlook-relook-scheme] [role="treeitem"] { - transition: none !important; - animation: none !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; } /* ============================================================ - SURFACES — flatten everything to white (or black in dark) + 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; } -html[data-outlook-relook-scheme] [role="navigation"], +/* ============================================================ + 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-primary) !important; - border-right: 1px solid var(--or-border-light) !important; + 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: transparent !important; + 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, @@ -118,30 +164,35 @@ 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-bg-selected) !important; + 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 — minimal, no background distinction + 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: text only, no background */ +/* Toolbar buttons — muted, text-forward */ html[data-outlook-relook-scheme] [role="toolbar"] button, html[data-outlook-relook-scheme] [role="toolbar"] [role="button"] { - background-color: transparent !important; - border: none !important; - color: var(--or-text-primary) !important; + border-radius: 2px !important; + box-shadow: none !important; + background-image: none !important; } html[data-outlook-relook-scheme] [role="toolbar"] button:hover, @@ -150,12 +201,12 @@ html[data-outlook-relook-scheme] [role="toolbar"] [role="button"]:hover { } /* ============================================================ - BUTTONS — understate everything + BUTTONS — Minimal, no heavy fills ============================================================ */ html[data-outlook-relook-scheme] button, html[data-outlook-relook-scheme] [role="button"] { - background-color: transparent !important; - color: var(--or-text-primary) !important; + box-shadow: none !important; + background-image: none !important; } html[data-outlook-relook-scheme] button:hover, @@ -163,39 +214,19 @@ html[data-outlook-relook-scheme] [role="button"]:hover { background-color: var(--or-bg-hover) !important; } -/* Links: accent color, no underline by default */ +/* ============================================================ + LINKS — Accent color + ============================================================ */ html[data-outlook-relook-scheme] a { color: var(--or-accent) !important; - text-decoration: none !important; } html[data-outlook-relook-scheme] a:hover { - text-decoration: underline !important; color: var(--or-accent-hover) !important; } /* ============================================================ - FOLDER PANE — text hierarchy only, no visual chrome - ============================================================ */ -html[data-outlook-relook-scheme] [role="treeitem"] { - color: var(--or-text-secondary) !important; - background-color: transparent !important; -} - -html[data-outlook-relook-scheme] [role="treeitem"]:hover { - color: var(--or-text-primary) !important; - background-color: transparent !important; - text-decoration: underline !important; -} - -html[data-outlook-relook-scheme] [role="treeitem"][aria-selected="true"] { - color: var(--or-text-primary) !important; - font-weight: 700 !important; - background-color: transparent !important; -} - -/* ============================================================ - SECONDARY TEXT — clear hierarchy + SECONDARY TEXT — Clear typographic hierarchy ============================================================ */ html[data-outlook-relook-scheme] [role="option"] span, html[data-outlook-relook-scheme] [role="listitem"] span { @@ -205,25 +236,52 @@ html[data-outlook-relook-scheme] [role="listitem"] span { html[data-outlook-relook-scheme] time { color: var(--or-text-tertiary) !important; font-size: 11px !important; - text-transform: uppercase !important; - letter-spacing: 0.03em !important; } /* ============================================================ - DIALOGS & OVERLAYS + 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 + SCROLLBARS — Thin, monochrome ============================================================ */ html[data-outlook-relook-scheme] ::-webkit-scrollbar { - width: 4px !important; - height: 4px !important; + width: 6px !important; + height: 6px !important; } html[data-outlook-relook-scheme] ::-webkit-scrollbar-track { @@ -231,5 +289,10 @@ html[data-outlook-relook-scheme] ::-webkit-scrollbar-track { } 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; } From 30437df2885b03776c3fd783e7741658361fe42c Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 10:03:08 -0700 Subject: [PATCH 19/29] fix: readable Brutalist font, override OWA purple selection colors, add unified header toggle --- content/content.js | 1 + content/settings-defaults.js | 1 + popup/popup.html | 4 ++ popup/popup.js | 1 + themes/base.css | 112 +++++++++++++++++++++++++++++++++++ themes/brutalist.css | 9 +-- 6 files changed, 124 insertions(+), 4 deletions(-) diff --git a/content/content.js b/content/content.js index b465294..2e61a58 100644 --- a/content/content.js +++ b/content/content.js @@ -15,6 +15,7 @@ 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', diff --git a/content/settings-defaults.js b/content/settings-defaults.js index 4ff846d..c93c158 100644 --- a/content/settings-defaults.js +++ b/content/settings-defaults.js @@ -19,6 +19,7 @@ window.OutlookRelook.DEFAULTS = { narrowDateColumn: true, compressComposeToolbar: true, readingPaneMaxWidth: true, + unifiedHeader: false, // Hide Elements hideCopilot: true, diff --git a/popup/popup.html b/popup/popup.html index eaf0bf5..4750a64 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -79,6 +79,10 @@
+
+ +
+
diff --git a/popup/popup.js b/popup/popup.js index 99a4bdd..f68400b 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -20,6 +20,7 @@ narrowDateColumn: true, compressComposeToolbar: true, readingPaneMaxWidth: true, + unifiedHeader: false, hideCopilot: true, hideSuggestedReplies: true, hidePromoBanners: true, diff --git a/themes/base.css b/themes/base.css index 64bfc89..3a74686 100644 --- a/themes/base.css +++ b/themes/base.css @@ -247,6 +247,118 @@ html[data-or-fontsize="large"] [role="list"] { } +/* ============================================================ + 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" + ============================================================ */ +html[data-or-unified-header="true"] [role="banner"], +html[data-or-unified-header="true"] header { + max-height: 32px !important; + min-height: 32px !important; + padding: 0 8px !important; + overflow: hidden !important; +} + +/* Collapse the ribbon/menu bar */ +html[data-or-unified-header="true"] [role="toolbar"] { + max-height: 28px !important; + min-height: 28px !important; + padding: 0 4px !important; + overflow: hidden !important; +} + +html[data-or-unified-header="true"] [role="toolbar"] button, +html[data-or-unified-header="true"] [role="toolbar"] [role="button"] { + padding: 2px 6px !important; + min-height: 24px !important; + max-height: 24px !important; + font-size: 11px !important; +} + +/* Collapse tab bars (Focused/Other) */ +html[data-or-unified-header="true"] [role="tablist"] { + max-height: 24px !important; + min-height: 24px !important; + padding: 0 !important; +} + +html[data-or-unified-header="true"] [role="tab"] { + padding: 2px 8px !important; + min-height: 22px !important; + font-size: 11px !important; +} + +/* Shrink the search bar */ +html[data-or-unified-header="true"] [role="search"], +html[data-or-unified-header="true"] [role="search"] input { + height: 24px !important; + min-height: 24px !important; + font-size: 12px !important; +} + +/* Remove any extra spacing/margins between header rows */ +html[data-or-unified-header="true"] [role="banner"] > div, +html[data-or-unified-header="true"] header > div { + padding-top: 0 !important; + padding-bottom: 0 !important; + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +/* Hide ribbon labels — keep icons only for space saving */ +html[data-or-unified-header="true"] [role="toolbar"] [role="button"] > span:not(:only-child), +html[data-or-unified-header="true"] [role="toolbar"] button > span:not(:only-child) { + font-size: 0 !important; + width: 0 !important; + overflow: hidden !important; +} + +/* Keep icons visible at normal size */ +html[data-or-unified-header="true"] [role="toolbar"] [role="img"], +html[data-or-unified-header="true"] [role="toolbar"] svg, +html[data-or-unified-header="true"] [role="toolbar"] img, +html[data-or-unified-header="true"] [role="toolbar"] i { + font-size: 14px !important; + width: auto !important; +} + + /* ============================================================ KEYBOARD NAVIGATION ============================================================ */ diff --git a/themes/brutalist.css b/themes/brutalist.css index 7ca9b5a..258e277 100644 --- a/themes/brutalist.css +++ b/themes/brutalist.css @@ -73,13 +73,14 @@ 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: none !important; - letter-spacing: -0.02em !important; + -webkit-font-smoothing: antialiased !important; + -moz-osx-font-smoothing: grayscale !important; + letter-spacing: 0 !important; } -/* Smaller base size for density */ html[data-outlook-relook-scheme] body { - font-size: 12px !important; + font-size: 13px !important; + line-height: 1.5 !important; } /* ============================================================ From 8fe0808bbdbf93bfa45b8fb1dc94fa9c9cfba7ef Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 10:19:44 -0700 Subject: [PATCH 20/29] fix: target OWA's actual Fluent UI DOM for density and unified header --- themes/base.css | 188 +++++++++++++++++++++++++++++------------------- 1 file changed, 114 insertions(+), 74 deletions(-) diff --git a/themes/base.css b/themes/base.css index 3a74686..432689f 100644 --- a/themes/base.css +++ b/themes/base.css @@ -33,42 +33,63 @@ /* ============================================================ 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 */ -html[data-or-compact-topbar="true"] [role="banner"], -html[data-or-compact-topbar="true"] header { - max-height: 36px !important; - min-height: 36px !important; +/* 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; } -html[data-or-compact-topbar="true"] [role="banner"] *, -html[data-or-compact-topbar="true"] header * { - font-size: 13px !important; -} - -/* Compact search bar inside top bar */ -html[data-or-compact-topbar="true"] [role="search"], -html[data-or-compact-topbar="true"] [role="search"] input { - height: 28px !important; - min-height: 28px !important; -} - -/* Compact command bar / ribbon */ -html[data-or-compact-commandbar="true"] [role="toolbar"] { +/* 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"] 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: 28px !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"] { @@ -286,78 +307,97 @@ html[data-outlook-relook-scheme] [role="treeitem"][aria-current="true"]::before /* ============================================================ UNIFIED HEADER — Collapse all top bars into one compact surface data-or-unified-header="true" + + OWA header structure (outlook.cloud.microsoft): + - Search bar: [role="search"] + - Ribbon: [role="toolbar"], .fui-Toolbar, .ms-OverflowSet + - Tabs: [role="tablist"], .fui-TabList + - Buttons: .ms-Button ============================================================ */ -html[data-or-unified-header="true"] [role="banner"], -html[data-or-unified-header="true"] header { - max-height: 32px !important; - min-height: 32px !important; - padding: 0 8px !important; - overflow: hidden !important; -} -/* Collapse the ribbon/menu bar */ -html[data-or-unified-header="true"] [role="toolbar"] { - max-height: 28px !important; - min-height: 28px !important; - padding: 0 4px !important; - overflow: hidden !important; -} - -html[data-or-unified-header="true"] [role="toolbar"] button, -html[data-or-unified-header="true"] [role="toolbar"] [role="button"] { - padding: 2px 6px !important; +/* Search bar — shrink to minimum */ +html[data-or-unified-header="true"] [role="search"] { + height: 24px !important; min-height: 24px !important; max-height: 24px !important; - font-size: 11px !important; -} - -/* Collapse tab bars (Focused/Other) */ -html[data-or-unified-header="true"] [role="tablist"] { - max-height: 24px !important; - min-height: 24px !important; padding: 0 !important; } -html[data-or-unified-header="true"] [role="tab"] { - padding: 2px 8px !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; +} + +/* Toolbars — compress to single row, minimal height */ +html[data-or-unified-header="true"] [role="toolbar"], +html[data-or-unified-header="true"] .fui-Toolbar { + max-height: 28px !important; + min-height: 24px !important; + padding: 0 4px !important; + overflow: hidden !important; + gap: 0 !important; +} + +/* Toolbar buttons — icons only, minimal size */ +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 4px !important; + min-height: 22px !important; + max-height: 22px !important; + min-width: unset !important; font-size: 11px !important; } -/* Shrink the search bar */ -html[data-or-unified-header="true"] [role="search"], -html[data-or-unified-header="true"] [role="search"] input { - height: 24px !important; - min-height: 24px !important; - font-size: 12px !important; +/* Hide button labels — keep 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; } -/* Remove any extra spacing/margins between header rows */ -html[data-or-unified-header="true"] [role="banner"] > div, -html[data-or-unified-header="true"] header > div { +/* Ribbon groups and overflow — compress */ +html[data-or-unified-header="true"] .ms-OverflowSet { + 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; +} + +/* Tab lists (Focused/Other) — minimal */ +html[data-or-unified-header="true"] [role="tablist"], +html[data-or-unified-header="true"] .fui-TabList { + max-height: 24px !important; + min-height: 22px !important; + padding: 0 !important; + gap: 0 !important; +} + +html[data-or-unified-header="true"] [role="tab"] { + padding: 1px 8px !important; + min-height: 20px !important; + font-size: 11px !important; +} + +/* Aggressively strip vertical padding from ALL divs in the header region. + We identify the header region as everything above the message list. + Target parent containers of search/toolbar/tablist. */ +html[data-or-unified-header="true"] [role="search"] ~ div, +html[data-or-unified-header="true"] [role="toolbar"] ~ div, +html[data-or-unified-header="true"] .fui-Toolbar ~ div { padding-top: 0 !important; padding-bottom: 0 !important; margin-top: 0 !important; margin-bottom: 0 !important; } -/* Hide ribbon labels — keep icons only for space saving */ -html[data-or-unified-header="true"] [role="toolbar"] [role="button"] > span:not(:only-child), -html[data-or-unified-header="true"] [role="toolbar"] button > span:not(:only-child) { - font-size: 0 !important; - width: 0 !important; - overflow: hidden !important; -} - -/* Keep icons visible at normal size */ -html[data-or-unified-header="true"] [role="toolbar"] [role="img"], -html[data-or-unified-header="true"] [role="toolbar"] svg, -html[data-or-unified-header="true"] [role="toolbar"] img, -html[data-or-unified-header="true"] [role="toolbar"] i { - font-size: 14px !important; - width: auto !important; -} - /* ============================================================ KEYBOARD NAVIGATION From c66265e48fb83740876981e29af72967a4a37b17 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 10:35:13 -0700 Subject: [PATCH 21/29] fix: use OWA's actual button selectors for keyboard actions, add flag/pin shortcuts --- content/keyboard.js | 153 ++++++++++++++++++++++++++------------------ 1 file changed, 92 insertions(+), 61 deletions(-) diff --git a/content/keyboard.js b/content/keyboard.js index 330581a..b3f3fa3 100644 --- a/content/keyboard.js +++ b/content/keyboard.js @@ -168,6 +168,29 @@ window.OutlookRelook.Keyboard = (function () { // --- 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; + } + function performAction(targets, actionFn) { if (targets.length === 0) return; var i = 0; @@ -178,99 +201,102 @@ window.OutlookRelook.Keyboard = (function () { } var target = targets[i]; i++; + // Click to select in OWA, then perform action target.click(); setTimeout(function () { actionFn(target); - setTimeout(processNext, 150); - }, 100); + setTimeout(processNext, 200); + }, 150); } processNext(); } function actionDelete(targets) { - performAction(targets, function () { - var deleteBtn = document.querySelector( - '[aria-label*="Delete" i][role="button"], [aria-label*="delete" i][role="menuitem"]' - ); - if (deleteBtn) { - deleteBtn.click(); + performAction(targets, function (target) { + var btn = findButton(target, 'Delete'); + if (btn) { + btn.click(); } else { - var deleteEvent = new KeyboardEvent('keydown', { - key: 'Delete', code: 'Delete', bubbles: true, cancelable: true - }); - document.activeElement.dispatchEvent(deleteEvent); + console.warn('[Outlook Relook] Delete button not found'); } }); } function actionArchive(targets) { - performAction(targets, function () { - var archiveBtn = document.querySelector( - '[aria-label*="Archive" i][role="button"], [aria-label*="archive" i][role="menuitem"]' - ); - if (archiveBtn) { - archiveBtn.click(); + performAction(targets, function (target) { + var btn = findButton(target, 'Archive'); + if (btn) { + btn.click(); } else { console.warn('[Outlook Relook] Archive button not found'); } }); } - function actionMarkRead(targets) { - performAction(targets, function () { - var readBtn = document.querySelector( - '[aria-label*="Mark as read" i][role="button"], [aria-label*="Mark as read" i][role="menuitem"]' - ); - if (readBtn) { - readBtn.click(); + function actionMarkReadUnread(targets) { + // OWA uses a single toggle button "Read / Unread" + performAction(targets, function (target) { + var btn = findButton(target, 'Read / Unread'); + if (btn) { + btn.click(); } else { - triggerContextMenuAction(/mark as read/i); - } - }); - } - - function actionMarkUnread(targets) { - performAction(targets, function () { - var unreadBtn = document.querySelector( - '[aria-label*="Mark as unread" i][role="button"], [aria-label*="Mark as unread" i][role="menuitem"]' - ); - if (unreadBtn) { - unreadBtn.click(); - } else { - triggerContextMenuAction(/mark as unread/i); + // Try context menu fallback + triggerContextMenuAction(target, /mark as read|mark as unread|read \/ unread/i); } }); } function actionMove(targets) { if (targets.length > 0) { + // Select first target, then open move dialog targets[0].click(); setTimeout(function () { - var moveBtn = document.querySelector( - '[aria-label*="Move to" i][role="button"], [aria-label*="Move" i][role="menuitem"]' - ); - if (moveBtn) { - moveBtn.click(); + var btn = findButton(targets[0], 'Move to'); + if (btn) { + btn.click(); } else { - triggerContextMenuAction(/move to/i); + triggerContextMenuAction(targets[0], /move to/i); } - }, 100); + }, 150); } } - function triggerContextMenuAction(pattern) { - var focused = document.querySelector('.or-kb-focused'); - if (!focused) return; - var rect = focused.getBoundingClientRect(); + function actionFlag(targets) { + performAction(targets, function (target) { + var btn = findButton(target, 'Flag this message') + || findButton(target, 'Flag / Unflag'); + if (btn) { + btn.click(); + } else { + console.warn('[Outlook Relook] Flag button not found'); + } + }); + } + + function actionPin(targets) { + performAction(targets, function (target) { + var btn = findButton(target, 'Pin / Unpin'); + if (btn) { + btn.click(); + } else { + console.warn('[Outlook Relook] Pin button not found'); + } + }); + } + + 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, }); - focused.dispatchEvent(contextEvent); + 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)) { + if (pattern.test(menuItems[j].textContent || menuItems[j].getAttribute('aria-label') || '')) { menuItems[j].click(); return; } @@ -360,18 +386,11 @@ window.OutlookRelook.Keyboard = (function () { break; case 'I': - if (shift) { - targets = getActionTargets(items); - actionMarkRead(targets); - } else { - handled = false; - } - break; - case 'U': + // Shift+i or Shift+u — OWA uses a single "Read / Unread" toggle if (shift) { targets = getActionTargets(items); - actionMarkUnread(targets); + actionMarkReadUnread(targets); } else { handled = false; } @@ -382,6 +401,18 @@ window.OutlookRelook.Keyboard = (function () { actionMove(targets); break; + case 'f': + // Flag/unflag + targets = getActionTargets(items); + actionFlag(targets); + break; + + case 'p': + // Pin/unpin + targets = getActionTargets(items); + actionPin(targets); + break; + case 'Escape': clearSelection(); break; From 7f0a88b131295235f3cbb1d68e905610c08cc1c0 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 10:43:47 -0700 Subject: [PATCH 22/29] fix: boost keyboard selection CSS specificity, add debug logging for visual state --- content/keyboard.js | 3 +++ themes/base.css | 36 ++++++++++++++++++------------------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/content/keyboard.js b/content/keyboard.js index b3f3fa3..9728c11 100644 --- a/content/keyboard.js +++ b/content/keyboard.js @@ -87,6 +87,9 @@ window.OutlookRelook.Keyboard = (function () { var idx = findItemById(items, id); if (idx >= 0) { items[idx].classList.add('or-kb-selected'); + console.log('[Outlook Relook] Applied or-kb-selected to', id.substring(0, 20), 'hasClass:', items[idx].classList.contains('or-kb-selected')); + } else { + console.log('[Outlook Relook] Could not find item for selected id:', id.substring(0, 20)); } }); diff --git a/themes/base.css b/themes/base.css index 432689f..053e8e6 100644 --- a/themes/base.css +++ b/themes/base.css @@ -403,32 +403,32 @@ html[data-or-unified-header="true"] .fui-Toolbar ~ div { KEYBOARD NAVIGATION ============================================================ */ -/* Focus cursor — the message the keyboard is currently pointing at */ -.or-kb-focused { +/* 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 */ -.or-kb-selected { - background-color: rgba(0, 120, 212, 0.08) !important; +/* 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"] .or-kb-selected { - background-color: rgba(100, 181, 246, 0.12) !important; -} - -/* Selection indicator — small left bar */ -.or-kb-selected::before { - content: ''; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 3px; - background: var(--or-accent, #0078d4); +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) */ From 919bdf2bf3f892cedc052eb890072ab2034449e5 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 10:49:37 -0700 Subject: [PATCH 23/29] fix: target actual OWA DOM for Focused/Other tabs and unified header containers --- themes/base.css | 89 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/themes/base.css b/themes/base.css index 053e8e6..00bc55e 100644 --- a/themes/base.css +++ b/themes/base.css @@ -164,9 +164,8 @@ html[data-or-hide-promos="true"] [aria-label*="Get the Outlook" i] { display: none !important; } -/* Focused / Other tabs */ -html[data-or-hide-focusedtabs="true"] [role="tablist"][aria-label*="Focused" i], -html[data-or-hide-focusedtabs="true"] [aria-label*="Focused Inbox" i] { +/* Focused / Other tabs — OWA uses .fui-TabList with .fui-Tab buttons */ +html[data-or-hide-focusedtabs="true"] .fui-TabList { display: none !important; } @@ -308,14 +307,17 @@ html[data-outlook-relook-scheme] [role="treeitem"][aria-current="true"]::before UNIFIED HEADER — Collapse all top bars into one compact surface data-or-unified-header="true" - OWA header structure (outlook.cloud.microsoft): - - Search bar: [role="search"] - - Ribbon: [role="toolbar"], .fui-Toolbar, .ms-OverflowSet - - Tabs: [role="tablist"], .fui-TabList - - Buttons: .ms-Button + 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 ============================================================ */ -/* Search bar — shrink to minimum */ +/* --- Row 1: Top bar container (48px → 28px) --- */ html[data-or-unified-header="true"] [role="search"] { height: 24px !important; min-height: 24px !important; @@ -330,28 +332,52 @@ html[data-or-unified-header="true"] [role="search"] input { padding: 1px 8px !important; } -/* Toolbars — compress to single row, minimal height */ +/* 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: 28px !important; + max-height: 26px !important; min-height: 24px !important; padding: 0 4px !important; overflow: hidden !important; gap: 0 !important; } -/* Toolbar buttons — icons only, minimal size */ 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 4px !important; + padding: 1px 3px !important; min-height: 22px !important; max-height: 22px !important; min-width: unset !important; font-size: 11px !important; } -/* Hide button labels — keep icons only */ +/* 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, @@ -360,7 +386,7 @@ html[data-or-unified-header="true"] .fui-Toolbar .ms-Button-textContainer { } /* Ribbon groups and overflow — compress */ -html[data-or-unified-header="true"] .ms-OverflowSet { +html[data-or-unified-header="true"] .ms-OverflowSet:not([role="tablist"]) { min-height: unset !important; gap: 0 !important; } @@ -371,27 +397,36 @@ html[data-or-unified-header="true"] [role="group"] { min-height: unset !important; } -/* Tab lists (Focused/Other) — minimal */ -html[data-or-unified-header="true"] [role="tablist"], +/* --- Row 4: Focused/Other tabs (fui-TabList) --- */ html[data-or-unified-header="true"] .fui-TabList { - max-height: 24px !important; - min-height: 22px !important; + max-height: 22px !important; + min-height: 20px !important; padding: 0 !important; gap: 0 !important; } -html[data-or-unified-header="true"] [role="tab"] { - padding: 1px 8px !important; +html[data-or-unified-header="true"] .fui-TabList .fui-Tab { + padding: 0 8px !important; min-height: 20px !important; font-size: 11px !important; } -/* Aggressively strip vertical padding from ALL divs in the header region. - We identify the header region as everything above the message list. - Target parent containers of search/toolbar/tablist. */ -html[data-or-unified-header="true"] [role="search"] ~ div, -html[data-or-unified-header="true"] [role="toolbar"] ~ div, -html[data-or-unified-header="true"] .fui-Toolbar ~ div { +/* --- 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; From 13901e44530c912506a130466826a1fa0f5deb86 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 11:25:19 -0700 Subject: [PATCH 24/29] fix: My Day toggle only hides buttons not panel, default to false --- content/settings-defaults.js | 2 +- popup/popup.js | 2 +- themes/base.css | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/content/settings-defaults.js b/content/settings-defaults.js index c93c158..5dff56e 100644 --- a/content/settings-defaults.js +++ b/content/settings-defaults.js @@ -28,7 +28,7 @@ window.OutlookRelook.DEFAULTS = { hideFocusedOtherTabs: true, hideSidebarAppIcons: false, hideGroupsSection: false, - hideMyDayButtons: true, + hideMyDayButtons: false, hideSenderAvatars: false, hideFeatureDiscovery: true, hideVivaInsights: true, diff --git a/popup/popup.js b/popup/popup.js index f68400b..a522bc3 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -27,7 +27,7 @@ hideFocusedOtherTabs: true, hideSidebarAppIcons: false, hideGroupsSection: false, - hideMyDayButtons: true, + hideMyDayButtons: false, hideSenderAvatars: false, hideFeatureDiscovery: true, hideVivaInsights: true, diff --git a/themes/base.css b/themes/base.css index 00bc55e..6408eb5 100644 --- a/themes/base.css +++ b/themes/base.css @@ -181,9 +181,9 @@ html[data-or-hide-groups="true"] [aria-label*="Groups" i][role="treeitem"] { display: none !important; } -/* My Day / right-side panel buttons */ -html[data-or-hide-myday="true"] [aria-label*="My Day" i], -html[data-or-hide-myday="true"] [aria-label*="To Do" i][role="button"] { +/* 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; } From e950a0e1c4921c40caa55664fc6b28b560967c54 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 16:21:43 -0700 Subject: [PATCH 25/29] fix: hover to reveal inline buttons instead of clicking rows, prevents fill-screen open --- content/keyboard.js | 86 +++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/content/keyboard.js b/content/keyboard.js index 9728c11..e933eb2 100644 --- a/content/keyboard.js +++ b/content/keyboard.js @@ -194,6 +194,23 @@ window.OutlookRelook.Keyboard = (function () { 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; @@ -204,12 +221,26 @@ window.OutlookRelook.Keyboard = (function () { } var target = targets[i]; i++; - // Click to select in OWA, then perform action - target.click(); + + // First, try to find the inline button without clicking the row. + // Hover to make OWA's inline action buttons appear. + triggerHover(target); + setTimeout(function () { - actionFn(target); - setTimeout(processNext, 200); - }, 150); + // 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(); } @@ -217,42 +248,33 @@ window.OutlookRelook.Keyboard = (function () { function actionDelete(targets) { performAction(targets, function (target) { var btn = findButton(target, 'Delete'); - if (btn) { - btn.click(); - } else { - console.warn('[Outlook Relook] Delete button not found'); - } + if (btn) { btn.click(); return true; } + console.warn('[Outlook Relook] Delete button not found'); + return false; }); } function actionArchive(targets) { performAction(targets, function (target) { var btn = findButton(target, 'Archive'); - if (btn) { - btn.click(); - } else { - console.warn('[Outlook Relook] Archive button not found'); - } + if (btn) { btn.click(); return true; } + console.warn('[Outlook Relook] Archive button not found'); + return false; }); } function actionMarkReadUnread(targets) { - // OWA uses a single toggle button "Read / Unread" performAction(targets, function (target) { var btn = findButton(target, 'Read / Unread'); - if (btn) { - btn.click(); - } else { - // Try context menu fallback - triggerContextMenuAction(target, /mark as read|mark as unread|read \/ unread/i); - } + 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) { - // Select first target, then open move dialog - targets[0].click(); + triggerHover(targets[0]); setTimeout(function () { var btn = findButton(targets[0], 'Move to'); if (btn) { @@ -268,22 +290,18 @@ window.OutlookRelook.Keyboard = (function () { performAction(targets, function (target) { var btn = findButton(target, 'Flag this message') || findButton(target, 'Flag / Unflag'); - if (btn) { - btn.click(); - } else { - console.warn('[Outlook Relook] Flag button not found'); - } + if (btn) { btn.click(); return true; } + console.warn('[Outlook Relook] Flag button not found'); + return false; }); } function actionPin(targets) { performAction(targets, function (target) { var btn = findButton(target, 'Pin / Unpin'); - if (btn) { - btn.click(); - } else { - console.warn('[Outlook Relook] Pin button not found'); - } + if (btn) { btn.click(); return true; } + console.warn('[Outlook Relook] Pin button not found'); + return false; }); } From 823de958916f54239d1eb4ef0e0df7bc2d33876c Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 16:23:40 -0700 Subject: [PATCH 26/29] chore: remove debug logging from keyboard selection --- content/keyboard.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/content/keyboard.js b/content/keyboard.js index e933eb2..c577290 100644 --- a/content/keyboard.js +++ b/content/keyboard.js @@ -87,9 +87,6 @@ window.OutlookRelook.Keyboard = (function () { var idx = findItemById(items, id); if (idx >= 0) { items[idx].classList.add('or-kb-selected'); - console.log('[Outlook Relook] Applied or-kb-selected to', id.substring(0, 20), 'hasClass:', items[idx].classList.contains('or-kb-selected')); - } else { - console.log('[Outlook Relook] Could not find item for selected id:', id.substring(0, 20)); } }); From 93a5888d833f49c1620d86fe6bbd92205668b330 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 17:20:14 -0700 Subject: [PATCH 27/29] refactor: keyboard nav as primary feature, design tweaks behind experimental toggle --- content/content.js | 113 +++++---- content/settings-defaults.js | 9 +- popup/popup.html | 468 ++++++++++++++++++----------------- popup/popup.js | 40 ++- 4 files changed, 352 insertions(+), 278 deletions(-) diff --git a/content/content.js b/content/content.js index 2e61a58..b307306 100644 --- a/content/content.js +++ b/content/content.js @@ -1,11 +1,14 @@ // 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 + // Map setting keys to data attributes on (design tweaks only) const SETTING_TO_ATTR = { compactTopBar: 'data-or-compact-topbar', compactCommandBar: 'data-or-compact-commandbar', @@ -34,11 +37,12 @@ darkModeEmailFix: 'data-or-darkmode-fix', }; - // Non-boolean attributes const SETTING_TO_ATTR_VALUE = { messageListFontSize: 'data-or-fontsize', }; + let designTweaksActive = false; + function applyColorScheme(scheme) { let resolved = scheme; if (scheme === 'system') { @@ -48,34 +52,41 @@ } function applySettingsToDOM(settings) { - // Boolean toggles -> data attributes for (const [key, attr] of Object.entries(SETTING_TO_ATTR)) { document.documentElement.setAttribute(attr, String(!!settings[key])); } - - // Value-based attributes for (const [key, attr] of Object.entries(SETTING_TO_ATTR_VALUE)) { document.documentElement.setAttribute(attr, settings[key] || ''); } - - // Color scheme applyColorScheme(settings.colorScheme); - - // Accent color if (settings.accentColor) { document.documentElement.style.setProperty('--or-accent-override', settings.accentColor); } - console.log('[Outlook Relook] Settings applied to DOM'); } - function injectThemeCSS(theme) { - // Remove existing theme stylesheet - const existing = document.getElementById('outlook-relook-theme'); - if (existing) existing.remove(); + 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'); - // Inject the theme CSS - const link = document.createElement('link'); + // Remove injected theme CSS + var themeLink = document.getElementById('outlook-relook-theme'); + if (themeLink) themeLink.remove(); + + console.log('[Outlook Relook] 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'); @@ -83,48 +94,60 @@ console.log('[Outlook Relook] 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('[Outlook Relook] Design tweaks enabled'); + } + + function stopDesignTweaks() { + OR.Observer.stop(); + OR.Behavior.stop(); + OR.Injector.stop(); + clearDesignFromDOM(); + designTweaksActive = false; + console.log('[Outlook Relook] Design tweaks disabled'); + } + async function init() { const settings = await OR.loadSettings(); console.log('[Outlook Relook] Loaded settings:', settings); - applySettingsToDOM(settings); - injectThemeCSS(settings.theme); - - // Start the MutationObserver - OR.Observer.start(settings); - - // Apply behavior patches - OR.Behavior.start(settings); - - // Start keyboard navigation + // Keyboard navigation — always starts (gated by its own toggle internally) OR.Keyboard.start(settings); - // Start DOM injector (quick actions) - OR.Injector.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; - // Build updated settings object OR.loadSettings().then((updated) => { - applySettingsToDOM(updated); - - // Update observer with new settings - OR.Observer.updateSettings(updated); - - // Update behavior patches - OR.Behavior.updateSettings(updated); - - // Update keyboard navigation + // Keyboard — always update OR.Keyboard.updateSettings(updated); - // Update injector - OR.Injector.updateSettings(updated); - - // Swap theme CSS if theme changed - if (changes.theme) { - injectThemeCSS(changes.theme.newValue); + // 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(); } }); }); @@ -133,7 +156,7 @@ // Listen for system theme changes window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', async () => { const settings = await OR.loadSettings(); - if (settings.colorScheme === 'system') { + if (settings.enableDesignTweaks && settings.colorScheme === 'system') { applyColorScheme('system'); } }); diff --git a/content/settings-defaults.js b/content/settings-defaults.js index 5dff56e..7393a1b 100644 --- a/content/settings-defaults.js +++ b/content/settings-defaults.js @@ -4,6 +4,12 @@ window.OutlookRelook = window.OutlookRelook || {}; window.OutlookRelook.DEFAULTS = { + // Keyboard Navigation (primary feature, always visible) + keyboardMultiSelect: true, + + // Design Tweaks (experimental, hidden by default) + enableDesignTweaks: false, + // Theme & Appearance theme: 'swiss', colorScheme: 'system', // 'light' | 'dark' | 'system' @@ -53,9 +59,6 @@ window.OutlookRelook.DEFAULTS = { autoResizeCompose: true, throttleNotifications: false, - // Keyboard Navigation - keyboardMultiSelect: true, - // Quick Actions markAllReadButton: true, quickFolderJump: true, diff --git a/popup/popup.html b/popup/popup.html index 4750a64..1e33c96 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -11,220 +11,8 @@ - -
-
Theme & Appearance
-
-
- - -
-
- - - -
-
- - -
-
-
- - -
-
Density & Spacing
-
-
- - -
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
- - -
-
Hide Elements
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
- - -
-
Readability
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- - -
-
-
- - -
-
Behavior
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- - -
-
- - -
-
- -
-
-
- -
-
-
- -
-
-
-
- - -
+ +
Keyboard Navigation
@@ -232,26 +20,258 @@
- j/k or arrows to navigate, x/Space to select, # delete, e archive, Shift+i read, Shift+u unread, v move, Esc deselect + j/k navigate, x/Space select, # delete, e archive, + Shift+i/u read/unread, v move, f flag, p pin, Esc deselect
- -
-
Quick Actions
-
+ +
+
Design Tweaks
+
- -
+ +
-
- -
+
+ Themes, density, element hiding, and behavior patches. May conflict with OWA updates.
+ +
+ + +
+
Theme & Appearance
+
+
+ + +
+
+ + + +
+
+ + +
+
+
+ + +
+
Density & Spacing
+
+
+ + +
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + +
+
Hide Elements
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + +
+
Readability
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
Behavior
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + +
+
Quick Actions
+
+
+ +
+
+
+ +
+
+
+
+ +
+