From afe51a3e6afed7bb1aed157739ab1784cb4db6c7 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 08:41:50 -0700 Subject: [PATCH] plan: implementation plan for Outlook Relook extension 13 tasks covering manifest, settings, selectors, themes, observer, behavior patches, injector, popup UI, and test page. --- .../plans/2026-04-23-outlook-relook.md | 3305 +++++++++++++++++ 1 file changed, 3305 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-23-outlook-relook.md diff --git a/docs/superpowers/plans/2026-04-23-outlook-relook.md b/docs/superpowers/plans/2026-04-23-outlook-relook.md new file mode 100644 index 0000000..26cfa4c --- /dev/null +++ b/docs/superpowers/plans/2026-04-23-outlook-relook.md @@ -0,0 +1,3305 @@ +# Outlook Relook Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a Manifest V3 Chrome extension that reskins Outlook Web App with switchable minimalist themes, granular toggle controls, and MutationObserver-based clutter removal. + +**Architecture:** Content script injects theme CSS and starts a MutationObserver to suppress unwanted OWA elements. A popup settings panel persists ~40 toggles to `chrome.storage.sync` and communicates changes to the content script in real time. Selector registry uses aria/data/structural selectors for resilience against OWA's obfuscated class names. + +**Tech Stack:** Vanilla JS (no build step), CSS custom properties, Chrome Extensions Manifest V3, `chrome.storage.sync` + +**Important:** OWA's DOM uses obfuscated class names that change frequently. CSS selectors targeting OWA elements must be determined by inspecting the live DOM at `outlook.office.com` during implementation. This plan provides the architecture and framework code exactly; OWA-specific selectors are marked with `/* INSPECT OWA */` comments and must be filled in by inspecting the live page using Chrome DevTools. Use the MCP Chrome plugin or DevTools to identify the correct `aria-label`, `data-*`, `role`, and structural selectors for each element. + +--- + +## File Structure + +``` +outlook-relook/ +├── manifest.json # Extension manifest (Manifest V3) +├── content/ +│ ├── content.js # Entry: loads settings, injects CSS, starts observer, listens for messages +│ ├── settings-defaults.js # All setting keys, defaults, and density presets +│ ├── observer.js # MutationObserver: suppress elements based on active settings +│ ├── selectors.js # Selector registry: logical name → primary + fallback selectors +│ ├── behavior.js # JS behavior patches (auto-advance, hover suppress, etc.) +│ └── injector.js # DOM injection (mark-all-read button, folder jump dialog) +├── popup/ +│ ├── popup.html # Settings panel markup +│ ├── popup.js # Settings panel logic +│ └── popup.css # Settings panel styles +├── themes/ +│ ├── base.css # Shared density/spacing/hiding overrides (always loaded) +│ ├── swiss.css # Swiss/Helvetica theme with light+dark variants +│ └── material.css # Material flat theme with light+dark variants +├── icons/ +│ ├── icon-16.png # Extension icon 16x16 +│ ├── icon-48.png # Extension icon 48x48 +│ └── icon-128.png # Extension icon 128x128 +├── selectors-test.html # Snapshot DOM fragments for offline selector testing +└── README.md # Developer notes +``` + +--- + +### Task 1: Project Scaffold & Manifest + +**Files:** +- Create: `manifest.json` +- Create: `content/content.js` (stub) +- Create: `popup/popup.html` (stub) +- Create: `icons/icon-16.png`, `icons/icon-48.png`, `icons/icon-128.png` + +- [ ] **Step 1: Create manifest.json** + +```json +{ + "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/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/*"] + } + ] +} +``` + +- [ ] **Step 2: Create placeholder icons** + +Generate simple solid-color PNG icons at 16x16, 48x48, and 128x128 using a canvas script or download placeholders. A single-color square with "OR" text is fine for dev. + +```bash +# Quick placeholder icons using ImageMagick (if available) or just create minimal PNGs +# If ImageMagick is not available, create 1x1 PNGs and scale them: +mkdir -p icons +convert -size 16x16 xc:'#1a1a1a' icons/icon-16.png 2>/dev/null || \ + python3 -c " +import struct, zlib +def png(w,h,path): + raw=b'' + for y in range(h): + raw+=b'\x00'+b'\x1a\x1a\x1a\xff'*w + c=zlib.compress(raw) + with open(path,'wb') as f: + f.write(b'\x89PNG\r\n\x1a\n') + def chunk(t,d): + f.write(struct.pack('>I',len(d))+t+d+struct.pack('>I',zlib.crc32(t+d)&0xffffffff)) + chunk(b'IHDR',struct.pack('>IIBBBBB',w,h,8,6,0,0,0)) + chunk(b'IDAT',c) + chunk(b'IEND',b'') +png(16,16,'icons/icon-16.png') +png(48,48,'icons/icon-48.png') +png(128,128,'icons/icon-128.png') +" +``` + +- [ ] **Step 3: Create stub content script** + +Create `content/content.js`: + +```js +// Outlook Relook — Content Script Entry Point +// Loaded by manifest on outlook.office.com/* + +(function () { + 'use strict'; + console.log('[Outlook Relook] Content script loaded'); +})(); +``` + +- [ ] **Step 4: Create stub popup** + +Create `popup/popup.html`: + +```html + + + + + + + +

Outlook Relook

+

Settings panel coming soon.

+ + +``` + +- [ ] **Step 5: Verify extension loads** + +1. Open `chrome://extensions`, enable Developer Mode +2. Click "Load unpacked", select the `outlook-relook/` directory +3. Verify: extension appears with name "Outlook Relook" and dark icon +4. Click the extension icon — popup shows "Settings panel coming soon." +5. Open `outlook.office.com`, open DevTools Console, verify `[Outlook Relook] Content script loaded` appears + +- [ ] **Step 6: Commit** + +```bash +git add manifest.json content/content.js popup/popup.html icons/ +git commit -m "scaffold: manifest v3, stub content script and popup, placeholder icons" +``` + +--- + +### Task 2: Settings Defaults & Storage Layer + +**Files:** +- Create: `content/settings-defaults.js` + +This file defines all setting keys, their default values, and density presets. It's loaded before all other content scripts so they can reference it. + +- [ ] **Step 1: Create settings-defaults.js** + +```js +// 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, + + // 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); + }); +}; +``` + +- [ ] **Step 2: Update content.js to load and log settings** + +Replace `content/content.js` with: + +```js +// Outlook Relook — Content Script Entry Point + +(function () { + 'use strict'; + + 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(); +})(); +``` + +- [ ] **Step 3: Verify settings load** + +1. Reload extension at `chrome://extensions` +2. Open `outlook.office.com`, open DevTools Console +3. Verify: `[Outlook Relook] Loaded settings: {...}` shows all default values +4. Verify: `[Outlook Relook] Color scheme: light` (or `dark` depending on OS setting) +5. Verify: `document.documentElement.getAttribute('data-outlook-relook-scheme')` returns `"light"` or `"dark"` + +- [ ] **Step 4: Commit** + +```bash +git add content/settings-defaults.js content/content.js +git commit -m "feat: settings defaults, storage layer, color scheme attribute" +``` + +--- + +### Task 3: Selector Registry + +**Files:** +- Create: `content/selectors.js` + +The selector registry maps logical element names to resilient CSS selectors. Each entry has a `primary` selector and `fallbacks` array. The `resolve` function tries each in order and returns the first matching element(s). + +- [ ] **Step 1: Create selectors.js** + +```js +// 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; +}; +``` + +- [ ] **Step 2: Verify selectors load** + +1. Reload extension at `chrome://extensions` +2. Open `outlook.office.com`, open DevTools Console +3. Run: `OutlookRelook.SELECTORS['copilot-button']` — should return the selector entry object +4. Run: `OutlookRelook.resolveSelector('copilot-button')` — should return an array (may be empty if Copilot isn't visible, but should not error) +5. Run: `OutlookRelook.resolveSelector('nonexistent')` — should log a warning and return `[]` + +- [ ] **Step 3: Commit** + +```bash +git add content/selectors.js +git commit -m "feat: selector registry with resilient aria/data/structural selectors" +``` + +--- + +### Task 4: Base CSS Theme + +**Files:** +- Create: `themes/base.css` + +The base CSS is always loaded via the manifest's `css` array. It applies density/spacing overrides controlled by `data-outlook-relook-*` attributes that the content script sets on ``. + +- [ ] **Step 1: Create base.css** + +```css +/* + * 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; +} +``` + +- [ ] **Step 2: Update content.js to set data attributes from settings** + +Add a `applySettingsToDOM` function in `content/content.js`. Replace the file contents: + +```js +// Outlook Relook — Content Script Entry Point + +(function () { + 'use strict'; + + const OR = window.OutlookRelook; + + // 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', + }; + + // Non-boolean attributes + const SETTING_TO_ATTR_VALUE = { + messageListFontSize: 'data-or-fontsize', + }; + + function applyColorScheme(scheme) { + let resolved = scheme; + if (scheme === 'system') { + resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + document.documentElement.setAttribute('data-outlook-relook-scheme', resolved); + } + + function applySettingsToDOM(settings) { + // 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') { + applyColorScheme('system'); + } + }); + + init(); +})(); +``` + +- [ ] **Step 3: Verify base CSS applies** + +1. Reload extension +2. Open `outlook.office.com` +3. In DevTools Elements panel, check that `` has data attributes like `data-or-compact-topbar="true"`, `data-or-hide-copilot="true"`, etc. +4. Verify visual changes: top bar should be shorter, message list rows tighter, Copilot elements hidden +5. If specific selectors don't match OWA's current DOM, note which ones need updating — this is expected and will be refined by inspecting the live DOM + +- [ ] **Step 4: Commit** + +```bash +git add themes/base.css content/content.js +git commit -m "feat: base CSS theme with density, hiding, and readability overrides" +``` + +--- + +### Task 5: Swiss Theme + +**Files:** +- Create: `themes/swiss.css` + +- [ ] **Step 1: Create swiss.css** + +```css +/* + * 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; +} +``` + +- [ ] **Step 2: Verify Swiss theme loads** + +1. Reload extension +2. Open `outlook.office.com` +3. In DevTools Console, verify: `[Outlook Relook] Theme loaded: swiss` +4. Verify: Helvetica font family applied, no rounded corners, monochrome palette +5. Toggle OS dark mode: colors should invert to dark variant + +- [ ] **Step 3: Commit** + +```bash +git add themes/swiss.css +git commit -m "feat: Swiss/Helvetica theme with light and dark variants" +``` + +--- + +### Task 6: Material Theme + +**Files:** +- Create: `themes/material.css` + +- [ ] **Step 1: Create material.css** + +```css +/* + * 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; +} +``` + +**Note:** Both `swiss.css` and `material.css` define the same CSS custom property names but with different values. Only one theme is injected at a time via `content.js`'s `injectThemeCSS()`. The `base.css` (loaded via manifest, always present) references these variables via `var(--or-accent)` etc. + +- [ ] **Step 2: Verify Material theme** + +1. Reload extension +2. In DevTools Console, run: `chrome.storage.sync.set({theme: 'material'})` +3. Verify: rounded corners on buttons (4px), subtle shadows on toolbar, blue accent color +4. Switch to dark mode: verify dark Material palette applies + +- [ ] **Step 3: Commit** + +```bash +git add themes/material.css +git commit -m "feat: Material flat theme with light and dark variants" +``` + +--- + +### Task 7: MutationObserver + +**Files:** +- Create: `content/observer.js` + +The observer watches OWA's DOM for dynamically injected elements (Copilot prompts, banners, suggested replies, etc.) and removes them based on active settings. CSS `display: none` handles statically rendered elements; the observer handles elements OWA injects after page load. + +- [ ] **Step 1: Create observer.js** + +```js +// 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 }; +})(); +``` + +- [ ] **Step 2: Wire observer into content.js** + +Add observer start/update calls to `content/content.js`. Add these lines inside the `init()` function after `injectThemeCSS(settings.theme)`: + +At the end of `init()`, after `injectThemeCSS(settings.theme)`, add: + +```js + // Start the MutationObserver + OR.Observer.start(settings); +``` + +Inside the `chrome.storage.onChanged` listener callback, after `applySettingsToDOM(updated)`, add: + +```js + // Update observer with new settings + OR.Observer.updateSettings(updated); +``` + +The full `init()` function and listener should now be: + +```js + 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); + + // Listen for setting changes from popup + chrome.storage.onChanged.addListener((changes, area) => { + if (area !== 'sync') return; + + OR.loadSettings().then((updated) => { + applySettingsToDOM(updated); + OR.Observer.updateSettings(updated); + + if (changes.theme) { + injectThemeCSS(changes.theme.newValue); + } + }); + }); + } +``` + +- [ ] **Step 3: Verify observer works** + +1. Reload extension +2. Open `outlook.office.com`, open DevTools Console +3. Verify: `[Outlook Relook] Observer started` +4. If Copilot or suggested replies are present, verify `[Outlook Relook] Suppressed: copilot-button` (or similar) appears +5. Navigate within OWA (open emails, switch folders) — observer should continue suppressing newly injected elements +6. In Console, run: `chrome.storage.sync.set({hideCopilot: false})` — Copilot elements should reappear on next OWA re-render + +- [ ] **Step 4: Commit** + +```bash +git add content/observer.js content/content.js +git commit -m "feat: MutationObserver for dynamic element suppression" +``` + +--- + +### Task 8: Behavior Patches + +**Files:** +- Create: `content/behavior.js` + +JavaScript behavior patches for UX annoyances. Each behavior is gated by its setting key. + +- [ ] **Step 1: Create behavior.js** + +```js +// 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 }; +})(); +``` + +- [ ] **Step 2: Wire behavior into content.js** + +In `content/content.js`, inside `init()`, after `OR.Observer.start(settings)`, add: + +```js + // Apply behavior patches + OR.Behavior.start(settings); +``` + +Inside the `chrome.storage.onChanged` listener, after `OR.Observer.updateSettings(updated)`, add: + +```js + // Update behavior patches + OR.Behavior.updateSettings(updated); +``` + +- [ ] **Step 3: Verify behavior patches** + +1. Reload extension +2. Open `outlook.office.com`, open DevTools Console +3. Verify: `[Outlook Relook] Behavior patches applied` +4. Test auto-collapse: ribbon should collapse ~2s after page load +5. Test toast suppression: perform an action that triggers a toast (e.g., move an email) — it should auto-dismiss after 5s +6. Test contact hover suppression: hover over a sender name — contact card should not appear (or appear more slowly) + +- [ ] **Step 4: Commit** + +```bash +git add content/behavior.js content/content.js +git commit -m "feat: behavior patches — auto-collapse, hover suppress, toast dismiss, auto-advance" +``` + +--- + +### Task 9: DOM Injector (Quick Actions) + +**Files:** +- Create: `content/injector.js` + +Injects custom UI elements: "Mark all as read" button on folder headers and a keyboard-triggered folder jump dialog. + +- [ ] **Step 1: Create injector.js** + +```js +// 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 }; +})(); +``` + +- [ ] **Step 2: Wire injector into content.js** + +In `content/content.js`, inside `init()`, after `OR.Behavior.start(settings)`, add: + +```js + // Start DOM injector (quick actions) + OR.Injector.start(settings); +``` + +Inside the `chrome.storage.onChanged` listener, after `OR.Behavior.updateSettings(updated)`, add: + +```js + // Update injector + OR.Injector.updateSettings(updated); +``` + +- [ ] **Step 3: Verify injections** + +1. Reload extension +2. Open `outlook.office.com` +3. Verify: folder headers have a small double-checkmark button next to them +4. Click the button — should trigger OWA's "Mark all as read" context menu action +5. Press `Ctrl+Shift+K` — folder jump dialog should appear +6. Type a folder name — matching folders should appear in the results list +7. Click a result — should navigate to that folder +8. Press `Escape` — dialog should close + +- [ ] **Step 4: Commit** + +```bash +git add content/injector.js content/content.js +git commit -m "feat: DOM injector — mark-all-read button and folder jump dialog" +``` + +--- + +### Task 10: Settings Popup — HTML & CSS + +**Files:** +- Modify: `popup/popup.html` (replace stub) +- Create: `popup/popup.css` + +- [ ] **Step 1: Create popup.css** + +```css +/* 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; +} +``` + +- [ ] **Step 2: Replace popup.html** + +```html + + + + + + + + +
+

Outlook Relook

+ +
+ + +
+
Theme & Appearance
+
+
+ + +
+
+ + + +
+
+ + +
+
+
+ + +
+
Density & Spacing
+
+
+ + +
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + +
+
Hide Elements
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + +
+
Readability
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
Behavior
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + +
+
Quick Actions
+
+
+ +
+
+
+ +
+
+
+
+ + + + + + + + + +``` + +- [ ] **Step 3: Verify popup renders** + +1. Reload extension +2. Click the extension icon +3. Verify: popup shows with header "Outlook Relook", "Theme & Appearance" section is open, all other sections are collapsed +4. Click section headers to expand/collapse +5. Verify: all toggle switches, dropdowns, color picker, and radio buttons render correctly +6. Verify: footer has Export, Import, Reset buttons + +- [ ] **Step 4: Commit** + +```bash +git add popup/popup.html popup/popup.css +git commit -m "feat: settings popup HTML and CSS with all toggle categories" +``` + +--- + +### Task 11: Settings Popup — JavaScript Logic + +**Files:** +- Create: `popup/popup.js` + +Handles loading settings into the UI, saving changes on toggle, density preset behavior, and export/import/reset. + +- [ ] **Step 1: Create popup.js** + +```js +// 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, + 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(); +})(); +``` + +- [ ] **Step 2: Verify popup functionality** + +1. Reload extension +2. Click extension icon — settings should load with default values +3. Toggle "Compact top bar" off — switch should turn gray +4. Close and reopen popup — the toggle should remain off (persisted) +5. Open OWA — verify the top bar is no longer compact +6. Toggle it back on — OWA top bar should re-compact without page reload +7. Change theme dropdown to "Material" — OWA should switch to Material theme +8. Change color scheme to "Dark" — OWA should switch to dark variant +9. Change density preset to "Comfortable" — all individual density toggles should uncheck +10. Click "Export" — a JSON file should download +11. Click "Reset" — all settings should return to defaults +12. Click "Import" — select the exported JSON — settings should restore + +- [ ] **Step 3: Commit** + +```bash +git add popup/popup.js +git commit -m "feat: popup settings logic — load, save, presets, export/import/reset" +``` + +--- + +### Task 12: Selectors Test Page + +**Files:** +- Create: `selectors-test.html` + +A static HTML file with mock OWA DOM fragments for offline selector verification. + +- [ ] **Step 1: Create selectors-test.html** + +```html + + + + + 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
+ + + +
+ + + + +``` + +- [ ] **Step 2: Verify test page** + +1. Open `selectors-test.html` directly in Chrome (file:// URL) +2. Click "Run Tests" +3. Verify: all tests show PASS (the mock DOM is designed to match the primary selectors) + +- [ ] **Step 3: Commit** + +```bash +git add selectors-test.html +git commit -m "feat: selector test page with mock OWA DOM fragments" +``` + +--- + +### Task 13: README & Final Integration + +**Files:** +- Create: `README.md` + +- [ ] **Step 1: Create README.md** + +```markdown +# 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 +- **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 + +- `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` +``` + +- [ ] **Step 2: Verify the full extension end-to-end** + +1. Reload extension +2. Open `outlook.office.com` +3. Verify in console: all `[Outlook Relook]` log messages appear without errors +4. Open popup, toggle settings, verify each takes effect on the OWA page +5. Switch themes between Swiss and Material — visual difference should be clear +6. Toggle light/dark — colors should switch +7. Toggle Copilot hiding off/on — Copilot elements should appear/disappear +8. Press Ctrl+Shift+K — folder jump dialog should work +9. Export settings, reset, import — round-trip should work +10. Open `selectors-test.html` — all selector tests should pass + +- [ ] **Step 3: Commit** + +```bash +git add README.md +git commit -m "docs: README with install, usage, and development guide" +``` + +--- + +## Self-Review Results + +**Spec coverage:** All spec sections are covered: +- Architecture (Tasks 1-2), Theme System (Tasks 4-6), Selector Strategy (Task 3), Settings Panel (Tasks 10-11), all toggle categories (distributed across Tasks 4, 7, 8, 9), File Structure (Task 1), Development & Testing (Task 12-13). Verified each spec requirement maps to a task. + +**Placeholder scan:** No TBDs or TODOs. All code blocks are complete. OWA-specific selectors are noted with a clear strategy (inspect live DOM) rather than left as placeholders. + +**Type consistency:** Verified across tasks: +- `window.OutlookRelook` namespace used consistently +- `OR.Observer.start/updateSettings/stop` API matches between `observer.js` (Task 7) and `content.js` (Task 7 wiring) +- `OR.Behavior.start/updateSettings/stop` API matches between `behavior.js` (Task 8) and `content.js` (Task 8 wiring) +- `OR.Injector.start/updateSettings/stop` API matches between `injector.js` (Task 9) and `content.js` (Task 9 wiring) +- `OR.loadSettings`, `OR.saveSettings`, `OR.DEFAULTS`, `OR.DENSITY_PRESETS` match between `settings-defaults.js` (Task 2) and `popup.js` (Task 11, duplicated for popup context) +- `OR.resolveSelector`, `OR.resolveSelectorString` match between `selectors.js` (Task 3) and consumers in `observer.js`, `behavior.js`, `injector.js` +- `SETTING_TO_ATTR` and `SETTING_TO_ATTR_VALUE` maps in `content.js` (Task 4) match the data attributes referenced in `base.css` (Task 4) +- `DEFAULTS` in `popup.js` (Task 11) matches `DEFAULTS` in `settings-defaults.js` (Task 2)