Files
Outcut/docs/superpowers/plans/2026-04-23-outlook-relook.md
Joel Brock afe51a3e6a plan: implementation plan for Outlook Relook extension
13 tasks covering manifest, settings, selectors, themes, observer,
behavior patches, injector, popup UI, and test page.
2026-04-23 08:41:50 -07:00

109 KiB

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

{
  "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.

# 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:

// 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:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <style>
    body { width: 320px; padding: 16px; font-family: system-ui, sans-serif; font-size: 13px; }
    h1 { font-size: 16px; margin: 0 0 8px; }
  </style>
</head>
<body>
  <h1>Outlook Relook</h1>
  <p>Settings panel coming soon.</p>
</body>
</html>
  • 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
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
// 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:

// 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
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
// 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
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 <html>.

  • Step 1: Create base.css
/*
 * Outlook Relook — Base Theme
 * Always loaded. Controls density, spacing, and element hiding.
 *
 * Toggle classes are set on <html> by content.js:
 *   data-outlook-relook-scheme="light|dark"
 *   data-or-compact-topbar="true"
 *   data-or-compact-commandbar="true"
 *   data-or-compact-messagelist="true"
 *   data-or-compact-readingpane="true"
 *   data-or-compact-folderpane="true"
 *   data-or-narrow-datecol="true"
 *   data-or-compact-compose="true"
 *   data-or-reading-maxwidth="true"
 *   data-or-hide-copilot="true"
 *   data-or-hide-suggestedreplies="true"
 *   data-or-hide-promos="true"
 *   data-or-hide-focusedtabs="true"
 *   data-or-hide-sidebaricons="true"
 *   data-or-hide-groups="true"
 *   data-or-hide-myday="true"
 *   data-or-hide-avatars="true"
 *   data-or-hide-discovery="true"
 *   data-or-hide-viva="true"
 *   data-or-hide-unreadother="true"
 *   data-or-hide-activity="true"
 *   data-or-unread-distinction="true"
 *   data-or-preview-own-line="true"
 *   data-or-normalize-font="true"
 *   data-or-darkmode-fix="true"
 *   data-or-fontsize="small|medium|large"
 */

/* ============================================================
   DENSITY & SPACING
   ============================================================ */

/* 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:

// Outlook Relook — Content Script Entry Point

(function () {
  'use strict';

  const OR = window.OutlookRelook;

  // Map setting keys to data attributes on <html>
  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 <html> 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
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

/*
 * 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
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

/*
 * 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
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
// 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:

    // Start the MutationObserver
    OR.Observer.start(settings);

Inside the chrome.storage.onChanged listener callback, after applySettingsToDOM(updated), add:

        // Update observer with new settings
        OR.Observer.updateSettings(updated);

The full init() function and listener should now be:

  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
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
// 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:

    // Apply behavior patches
    OR.Behavior.start(settings);

Inside the chrome.storage.onChanged listener, after OR.Observer.updateSettings(updated), add:

        // 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
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
// 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:

    // Start DOM injector (quick actions)
    OR.Injector.start(settings);

Inside the chrome.storage.onChanged listener, after OR.Behavior.updateSettings(updated), add:

        // 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
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

/* 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
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="popup.css">
</head>
<body>

  <div class="or-header">
    <h1>Outlook Relook</h1>
    <span class="or-version" id="version"></span>
  </div>

  <!-- Theme & Appearance -->
  <div class="or-section open" data-section="theme">
    <div class="or-section-header">Theme & Appearance</div>
    <div class="or-section-body">
      <div class="or-select-row">
        <label for="theme">Theme</label>
        <select id="theme" data-setting="theme">
          <option value="swiss">Swiss</option>
          <option value="material">Material</option>
        </select>
      </div>
      <div class="or-radio-group" data-setting="colorScheme">
        <label><input type="radio" name="colorScheme" value="light"> Light</label>
        <label><input type="radio" name="colorScheme" value="dark"> Dark</label>
        <label><input type="radio" name="colorScheme" value="system"> System</label>
      </div>
      <div class="or-color-row">
        <label for="accentColor">Accent color</label>
        <input type="color" id="accentColor" data-setting="accentColor" value="#1976d2">
      </div>
    </div>
  </div>

  <!-- Density & Spacing -->
  <div class="or-section" data-section="density">
    <div class="or-section-header">Density & Spacing</div>
    <div class="or-section-body">
      <div class="or-select-row">
        <label for="densityPreset">Density preset</label>
        <select id="densityPreset" data-setting="densityPreset">
          <option value="comfortable">Comfortable</option>
          <option value="compact">Compact</option>
          <option value="ultra-compact">Ultra-compact</option>
        </select>
      </div>
      <div class="or-toggle-row">
        <label for="compactTopBar">Compact top bar</label>
        <div class="or-switch"><input type="checkbox" id="compactTopBar" data-setting="compactTopBar"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="compactCommandBar">Compact command bar</label>
        <div class="or-switch"><input type="checkbox" id="compactCommandBar" data-setting="compactCommandBar"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="compactMessageList">Compact message list</label>
        <div class="or-switch"><input type="checkbox" id="compactMessageList" data-setting="compactMessageList"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="compactReadingPane">Compact reading pane</label>
        <div class="or-switch"><input type="checkbox" id="compactReadingPane" data-setting="compactReadingPane"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="compactFolderPane">Compact folder pane</label>
        <div class="or-switch"><input type="checkbox" id="compactFolderPane" data-setting="compactFolderPane"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="narrowDateColumn">Narrow date column</label>
        <div class="or-switch"><input type="checkbox" id="narrowDateColumn" data-setting="narrowDateColumn"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="compressComposeToolbar">Compress compose toolbar</label>
        <div class="or-switch"><input type="checkbox" id="compressComposeToolbar" data-setting="compressComposeToolbar"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="readingPaneMaxWidth">Reading pane max-width</label>
        <div class="or-switch"><input type="checkbox" id="readingPaneMaxWidth" data-setting="readingPaneMaxWidth"><span class="slider"></span></div>
      </div>
    </div>
  </div>

  <!-- Hide Elements -->
  <div class="or-section" data-section="hide">
    <div class="or-section-header">Hide Elements</div>
    <div class="or-section-body">
      <div class="or-toggle-row">
        <label for="hideCopilot">Copilot (button, pane, suggestions)</label>
        <div class="or-switch"><input type="checkbox" id="hideCopilot" data-setting="hideCopilot"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="hideSuggestedReplies">Suggested replies</label>
        <div class="or-switch"><input type="checkbox" id="hideSuggestedReplies" data-setting="hideSuggestedReplies"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="hidePromoBanners">Promotional banners</label>
        <div class="or-switch"><input type="checkbox" id="hidePromoBanners" data-setting="hidePromoBanners"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="hideFocusedOtherTabs">Focused / Other tabs</label>
        <div class="or-switch"><input type="checkbox" id="hideFocusedOtherTabs" data-setting="hideFocusedOtherTabs"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="hideSidebarAppIcons">Sidebar app icons</label>
        <div class="or-switch"><input type="checkbox" id="hideSidebarAppIcons" data-setting="hideSidebarAppIcons"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="hideGroupsSection">Groups section</label>
        <div class="or-switch"><input type="checkbox" id="hideGroupsSection" data-setting="hideGroupsSection"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="hideMyDayButtons">My Day / panel buttons</label>
        <div class="or-switch"><input type="checkbox" id="hideMyDayButtons" data-setting="hideMyDayButtons"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="hideSenderAvatars">Sender avatars</label>
        <div class="or-switch"><input type="checkbox" id="hideSenderAvatars" data-setting="hideSenderAvatars"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="hideFeatureDiscovery">Feature discovery tooltips</label>
        <div class="or-switch"><input type="checkbox" id="hideFeatureDiscovery" data-setting="hideFeatureDiscovery"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="hideVivaInsights">Viva Insights / Briefing</label>
        <div class="or-switch"><input type="checkbox" id="hideVivaInsights" data-setting="hideVivaInsights"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="hideUnreadOtherBanner">Unread in Other banner</label>
        <div class="or-switch"><input type="checkbox" id="hideUnreadOtherBanner" data-setting="hideUnreadOtherBanner"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="hideActivityFeed">Activity feed banners</label>
        <div class="or-switch"><input type="checkbox" id="hideActivityFeed" data-setting="hideActivityFeed"><span class="slider"></span></div>
      </div>
    </div>
  </div>

  <!-- Readability -->
  <div class="or-section" data-section="readability">
    <div class="or-section-header">Readability</div>
    <div class="or-section-body">
      <div class="or-toggle-row">
        <label for="unreadDistinction">Unread distinction (bold + border)</label>
        <div class="or-switch"><input type="checkbox" id="unreadDistinction" data-setting="unreadDistinction"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="previewOwnLine">Preview text on own line</label>
        <div class="or-switch"><input type="checkbox" id="previewOwnLine" data-setting="previewOwnLine"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="normalizeFontWeight">Normalize font weight</label>
        <div class="or-switch"><input type="checkbox" id="normalizeFontWeight" data-setting="normalizeFontWeight"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="darkModeEmailFix">Dark mode email body fix</label>
        <div class="or-switch"><input type="checkbox" id="darkModeEmailFix" data-setting="darkModeEmailFix"><span class="slider"></span></div>
      </div>
      <div class="or-select-row">
        <label for="messageListFontSize">Message list font size</label>
        <select id="messageListFontSize" data-setting="messageListFontSize">
          <option value="small">Small</option>
          <option value="medium">Medium</option>
          <option value="large">Large</option>
        </select>
      </div>
    </div>
  </div>

  <!-- Behavior -->
  <div class="or-section" data-section="behavior">
    <div class="or-section-header">Behavior</div>
    <div class="or-section-body">
      <div class="or-toggle-row">
        <label for="autoCollapseRibbon">Auto-collapse ribbon</label>
        <div class="or-switch"><input type="checkbox" id="autoCollapseRibbon" data-setting="autoCollapseRibbon"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="rememberSidebarState">Remember sidebar state</label>
        <div class="or-switch"><input type="checkbox" id="rememberSidebarState" data-setting="rememberSidebarState"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="suppressContactHover">Suppress contact hover cards</label>
        <div class="or-switch"><input type="checkbox" id="suppressContactHover" data-setting="suppressContactHover"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="autoAdvanceAfterDelete">Auto-advance after delete</label>
        <div class="or-switch"><input type="checkbox" id="autoAdvanceAfterDelete" data-setting="autoAdvanceAfterDelete"><span class="slider"></span></div>
      </div>
      <div class="or-select-row">
        <label for="autoDismissToasts">Auto-dismiss toasts</label>
        <select id="autoDismissToasts" data-setting="autoDismissToasts">
          <option value="off">Off</option>
          <option value="3">3 seconds</option>
          <option value="5">5 seconds</option>
          <option value="10">10 seconds</option>
        </select>
      </div>
      <div class="or-select-row">
        <label for="toastPosition">Toast position</label>
        <select id="toastPosition" data-setting="toastPosition">
          <option value="bottom-left">Bottom-left</option>
          <option value="top-right">Top-right</option>
        </select>
      </div>
      <div class="or-toggle-row">
        <label for="stickyReplyBar">Sticky Reply/Forward bar</label>
        <div class="or-switch"><input type="checkbox" id="stickyReplyBar" data-setting="stickyReplyBar"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="autoResizeCompose">Auto-resize compose window</label>
        <div class="or-switch"><input type="checkbox" id="autoResizeCompose" data-setting="autoResizeCompose"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="throttleNotifications">Throttle desktop notifications</label>
        <div class="or-switch"><input type="checkbox" id="throttleNotifications" data-setting="throttleNotifications"><span class="slider"></span></div>
      </div>
    </div>
  </div>

  <!-- Quick Actions -->
  <div class="or-section" data-section="actions">
    <div class="or-section-header">Quick Actions</div>
    <div class="or-section-body">
      <div class="or-toggle-row">
        <label for="markAllReadButton">"Mark all as read" button</label>
        <div class="or-switch"><input type="checkbox" id="markAllReadButton" data-setting="markAllReadButton"><span class="slider"></span></div>
      </div>
      <div class="or-toggle-row">
        <label for="quickFolderJump">Quick folder jump (Ctrl+Shift+K)</label>
        <div class="or-switch"><input type="checkbox" id="quickFolderJump" data-setting="quickFolderJump"><span class="slider"></span></div>
      </div>
    </div>
  </div>

  <!-- Footer -->
  <div class="or-footer">
    <button id="exportBtn" title="Export settings as JSON">Export</button>
    <button id="importBtn" title="Import settings from JSON">Import</button>
    <button id="resetBtn" class="danger" title="Reset all settings to defaults">Reset</button>
  </div>

  <input type="file" id="importFile" accept=".json" style="display:none">

  <script src="popup.js"></script>
</body>
</html>
  • 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
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
// 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
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
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Outlook Relook — Selector Test Page</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 20px; max-width: 800px; margin: 0 auto; }
    h1 { font-size: 18px; margin-bottom: 16px; }
    h2 { font-size: 14px; margin: 20px 0 8px; color: #666; }
    .mock { border: 1px dashed #ccc; padding: 8px 12px; margin: 4px 0; font-size: 13px; background: #fafafa; }
    .result { margin: 4px 0; font-size: 12px; font-family: monospace; }
    .pass { color: #2e7d32; }
    .fail { color: #c62828; }
    #results { margin-top: 24px; border-top: 1px solid #eee; padding-top: 16px; }
    button { padding: 8px 16px; margin-top: 12px; cursor: pointer; }
  </style>
</head>
<body>
  <h1>Outlook Relook — Selector Test Page</h1>
  <p>Mock OWA DOM fragments. Click "Run Tests" to verify selectors match.</p>

  <!-- Mock OWA elements -->
  <h2>Copilot</h2>
  <div class="mock" aria-label="Copilot" role="button">Copilot Button</div>
  <div class="mock" aria-label="Copilot chat" role="complementary">Copilot Pane</div>
  <div class="mock" aria-label="Inline writing suggestion">Copilot Compose Suggestion</div>

  <h2>Suggested Replies</h2>
  <div class="mock" aria-label="Suggested replies" role="group">Sounds good! | Thanks! | Got it.</div>

  <h2>Promotional Banners</h2>
  <div class="mock" aria-label="Try the new Outlook" role="banner">Try the new Outlook</div>
  <div class="mock" aria-label="Upgrade to premium">Upgrade to premium</div>
  <div class="mock" aria-label="Get the Outlook app">Get the Outlook app</div>

  <h2>Focused/Other Tabs</h2>
  <div class="mock" role="tablist" aria-label="Focused Inbox tabs">
    <div role="tab">Focused</div>
    <div role="tab">Other</div>
  </div>

  <h2>Sidebar App Icons</h2>
  <nav class="mock" aria-label="App navigation">
    <div>Mail</div><div>Calendar</div><div>People</div><div>To Do</div>
  </nav>

  <h2>Layout Regions</h2>
  <div class="mock" role="banner">Top Bar / Banner</div>
  <div class="mock" role="toolbar" aria-label="Command actions">Command Bar</div>
  <div class="mock" role="listbox" aria-label="Message list">
    <div role="option" aria-label="Unread: Subject line">Message Row</div>
    <div role="option">Read Message Row</div>
  </div>
  <div class="mock" role="navigation" aria-label="Folder pane">
    <div role="tree" aria-label="Folder list">
      <div role="treeitem">Inbox</div>
      <div role="treeitem">Sent</div>
    </div>
  </div>

  <h2>Behavior Targets</h2>
  <div class="mock" role="search"><input placeholder="Search" aria-label="Search mail"></div>
  <div class="mock" aria-label="Ribbon display options" aria-expanded="true" role="button">Ribbon Toggle</div>
  <div class="mock" role="alert">Toast notification: Message sent</div>

  <button id="runTests">Run Tests</button>
  <div id="results"></div>

  <script>
    document.getElementById('runTests').addEventListener('click', function () {
      var results = document.getElementById('results');
      // Clear results using safe DOM methods
      while (results.firstChild) {
        results.removeChild(results.firstChild);
      }

      var heading = document.createElement('h2');
      heading.textContent = 'Test Results';
      results.appendChild(heading);

      var tests = [
        { name: 'copilot-button', selector: '[aria-label*="Copilot" i]', expectMatch: true },
        { name: 'copilot-pane', selector: '[aria-label*="Copilot" i][role="complementary"]', expectMatch: true },
        { name: 'copilot-compose', selector: '[aria-label*="writing suggestion" i]', expectMatch: true },
        { name: 'suggested-replies', selector: '[aria-label*="Suggested repl" i]', expectMatch: true },
        { name: 'promo-banners', selector: '[aria-label*="Try the new" i]', expectMatch: true },
        { name: 'focused-tabs', selector: '[role="tablist"][aria-label*="Focused" i]', expectMatch: true },
        { name: 'sidebar-apps', selector: 'nav[aria-label*="App" i]', expectMatch: true },
        { name: 'top-bar', selector: '[role="banner"]', expectMatch: true },
        { name: 'command-bar-alt', selector: '[role="toolbar"][aria-label*="action" i]', expectMatch: true },
        { name: 'message-list', selector: '[role="listbox"][aria-label*="Message list" i]', expectMatch: true },
        { name: 'folder-pane', selector: '[role="navigation"][aria-label*="Folder" i]', expectMatch: true },
        { name: 'search', selector: '[role="search"]', expectMatch: true },
        { name: 'ribbon-toggle', selector: '[aria-label*="Ribbon" i][aria-expanded]', expectMatch: true },
        { name: 'toast', selector: '[role="alert"]', expectMatch: true },
      ];

      var pass = 0;
      var fail = 0;

      for (var i = 0; i < tests.length; i++) {
        var test = tests[i];
        var found = document.querySelectorAll(test.selector).length > 0;
        var ok = found === test.expectMatch;
        var div = document.createElement('div');
        div.className = 'result ' + (ok ? 'pass' : 'fail');
        div.textContent = (ok ? 'PASS' : 'FAIL') + ' ' + test.name + ': selector="' + test.selector + '" found=' + found + ' expected=' + test.expectMatch;
        results.appendChild(div);
        if (ok) pass++; else fail++;
      }

      var summary = document.createElement('div');
      summary.className = 'result';
      summary.textContent = pass + ' passed, ' + fail + ' failed out of ' + tests.length + ' tests';
      summary.style.fontWeight = 'bold';
      summary.style.marginTop = '12px';
      results.appendChild(summary);
    });
  </script>
</body>
</html>
  • 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
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

# 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
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)