# 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.) │ ├── keyboard.js # Gmail-style keyboard navigation & multi-select │ └── 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/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/*"] } ] } ``` - [ ] **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
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, // 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); }); }; ``` - [ ] **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** ```htmlMock OWA DOM fragments. Click "Run Tests" to verify selectors match.