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

3306 lines
109 KiB
Markdown

# Outlook Relook Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a Manifest V3 Chrome extension that reskins Outlook Web App with switchable minimalist themes, granular toggle controls, and MutationObserver-based clutter removal.
**Architecture:** Content script injects theme CSS and starts a MutationObserver to suppress unwanted OWA elements. A popup settings panel persists ~40 toggles to `chrome.storage.sync` and communicates changes to the content script in real time. Selector registry uses aria/data/structural selectors for resilience against OWA's obfuscated class names.
**Tech Stack:** Vanilla JS (no build step), CSS custom properties, Chrome Extensions Manifest V3, `chrome.storage.sync`
**Important:** OWA's DOM uses obfuscated class names that change frequently. CSS selectors targeting OWA elements must be determined by inspecting the live DOM at `outlook.office.com` during implementation. This plan provides the architecture and framework code exactly; OWA-specific selectors are marked with `/* INSPECT OWA */` comments and must be filled in by inspecting the live page using Chrome DevTools. Use the MCP Chrome plugin or DevTools to identify the correct `aria-label`, `data-*`, `role`, and structural selectors for each element.
---
## File Structure
```
outlook-relook/
├── manifest.json # Extension manifest (Manifest V3)
├── content/
│ ├── content.js # Entry: loads settings, injects CSS, starts observer, listens for messages
│ ├── settings-defaults.js # All setting keys, defaults, and density presets
│ ├── observer.js # MutationObserver: suppress elements based on active settings
│ ├── selectors.js # Selector registry: logical name → primary + fallback selectors
│ ├── behavior.js # JS behavior patches (auto-advance, hover suppress, etc.)
│ └── injector.js # DOM injection (mark-all-read button, folder jump dialog)
├── popup/
│ ├── popup.html # Settings panel markup
│ ├── popup.js # Settings panel logic
│ └── popup.css # Settings panel styles
├── themes/
│ ├── base.css # Shared density/spacing/hiding overrides (always loaded)
│ ├── swiss.css # Swiss/Helvetica theme with light+dark variants
│ └── material.css # Material flat theme with light+dark variants
├── icons/
│ ├── icon-16.png # Extension icon 16x16
│ ├── icon-48.png # Extension icon 48x48
│ └── icon-128.png # Extension icon 128x128
├── selectors-test.html # Snapshot DOM fragments for offline selector testing
└── README.md # Developer notes
```
---
### Task 1: Project Scaffold & Manifest
**Files:**
- Create: `manifest.json`
- Create: `content/content.js` (stub)
- Create: `popup/popup.html` (stub)
- Create: `icons/icon-16.png`, `icons/icon-48.png`, `icons/icon-128.png`
- [ ] **Step 1: Create manifest.json**
```json
{
"manifest_version": 3,
"name": "Outlook Relook",
"version": "0.1.0",
"description": "Minimalist reskin for Outlook Web App — clean themes, less clutter, more email.",
"permissions": ["storage", "activeTab"],
"content_scripts": [
{
"matches": ["https://outlook.office.com/*", "https://outlook.office365.com/*"],
"css": ["themes/base.css"],
"js": [
"content/settings-defaults.js",
"content/selectors.js",
"content/observer.js",
"content/behavior.js",
"content/injector.js",
"content/content.js"
],
"run_at": "document_idle"
}
],
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"web_accessible_resources": [
{
"resources": ["themes/*.css"],
"matches": ["https://outlook.office.com/*", "https://outlook.office365.com/*"]
}
]
}
```
- [ ] **Step 2: Create placeholder icons**
Generate simple solid-color PNG icons at 16x16, 48x48, and 128x128 using a canvas script or download placeholders. A single-color square with "OR" text is fine for dev.
```bash
# Quick placeholder icons using ImageMagick (if available) or just create minimal PNGs
# If ImageMagick is not available, create 1x1 PNGs and scale them:
mkdir -p icons
convert -size 16x16 xc:'#1a1a1a' icons/icon-16.png 2>/dev/null || \
python3 -c "
import struct, zlib
def png(w,h,path):
raw=b''
for y in range(h):
raw+=b'\x00'+b'\x1a\x1a\x1a\xff'*w
c=zlib.compress(raw)
with open(path,'wb') as f:
f.write(b'\x89PNG\r\n\x1a\n')
def chunk(t,d):
f.write(struct.pack('>I',len(d))+t+d+struct.pack('>I',zlib.crc32(t+d)&0xffffffff))
chunk(b'IHDR',struct.pack('>IIBBBBB',w,h,8,6,0,0,0))
chunk(b'IDAT',c)
chunk(b'IEND',b'')
png(16,16,'icons/icon-16.png')
png(48,48,'icons/icon-48.png')
png(128,128,'icons/icon-128.png')
"
```
- [ ] **Step 3: Create stub content script**
Create `content/content.js`:
```js
// Outlook Relook — Content Script Entry Point
// Loaded by manifest on outlook.office.com/*
(function () {
'use strict';
console.log('[Outlook Relook] Content script loaded');
})();
```
- [ ] **Step 4: Create stub popup**
Create `popup/popup.html`:
```html
<!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**
```bash
git add manifest.json content/content.js popup/popup.html icons/
git commit -m "scaffold: manifest v3, stub content script and popup, placeholder icons"
```
---
### Task 2: Settings Defaults & Storage Layer
**Files:**
- Create: `content/settings-defaults.js`
This file defines all setting keys, their default values, and density presets. It's loaded before all other content scripts so they can reference it.
- [ ] **Step 1: Create settings-defaults.js**
```js
// Outlook Relook — Settings Defaults
// Loaded first by manifest. Exposes window.OutlookRelook.DEFAULTS and helpers.
window.OutlookRelook = window.OutlookRelook || {};
window.OutlookRelook.DEFAULTS = {
// Theme & Appearance
theme: 'swiss',
colorScheme: 'system', // 'light' | 'dark' | 'system'
accentColor: '', // empty = theme default
// Density & Spacing
densityPreset: 'compact', // 'comfortable' | 'compact' | 'ultra-compact'
compactTopBar: true,
compactCommandBar: true,
compactMessageList: true,
compactReadingPane: true,
compactFolderPane: true,
narrowDateColumn: true,
compressComposeToolbar: true,
readingPaneMaxWidth: true,
// Hide Elements
hideCopilot: true,
hideSuggestedReplies: true,
hidePromoBanners: true,
hideFocusedOtherTabs: true,
hideSidebarAppIcons: false,
hideGroupsSection: false,
hideMyDayButtons: true,
hideSenderAvatars: false,
hideFeatureDiscovery: true,
hideVivaInsights: true,
hideUnreadOtherBanner: true,
hideActivityFeed: true,
// Readability
unreadDistinction: true,
previewOwnLine: false,
normalizeFontWeight: true,
darkModeEmailFix: true,
messageListFontSize: 'medium', // 'small' | 'medium' | 'large'
// Behavior
autoCollapseRibbon: true,
rememberSidebarState: true,
suppressContactHover: true,
autoAdvanceAfterDelete: true,
autoDismissToasts: '5', // 'off' | '3' | '5' | '10' (seconds)
toastPosition: 'top-right', // 'bottom-left' | 'top-right'
stickyReplyBar: true,
autoResizeCompose: true,
throttleNotifications: false,
// Quick Actions
markAllReadButton: true,
quickFolderJump: true,
};
// Density presets define which individual toggles each preset sets
window.OutlookRelook.DENSITY_PRESETS = {
comfortable: {
compactTopBar: false,
compactCommandBar: false,
compactMessageList: false,
compactReadingPane: false,
compactFolderPane: false,
narrowDateColumn: false,
compressComposeToolbar: false,
readingPaneMaxWidth: true,
},
compact: {
compactTopBar: true,
compactCommandBar: true,
compactMessageList: true,
compactReadingPane: true,
compactFolderPane: true,
narrowDateColumn: true,
compressComposeToolbar: true,
readingPaneMaxWidth: true,
},
'ultra-compact': {
compactTopBar: true,
compactCommandBar: true,
compactMessageList: true,
compactReadingPane: true,
compactFolderPane: true,
narrowDateColumn: true,
compressComposeToolbar: true,
readingPaneMaxWidth: true,
},
};
// Load settings from chrome.storage.sync, filling in defaults for missing keys
window.OutlookRelook.loadSettings = function () {
return new Promise((resolve) => {
chrome.storage.sync.get(window.OutlookRelook.DEFAULTS, (settings) => {
resolve(settings);
});
});
};
// Save a partial settings object to chrome.storage.sync
window.OutlookRelook.saveSettings = function (partial) {
return new Promise((resolve) => {
chrome.storage.sync.set(partial, resolve);
});
};
```
- [ ] **Step 2: Update content.js to load and log settings**
Replace `content/content.js` with:
```js
// Outlook Relook — Content Script Entry Point
(function () {
'use strict';
const OR = window.OutlookRelook;
async function init() {
const settings = await OR.loadSettings();
console.log('[Outlook Relook] Loaded settings:', settings);
// Apply color scheme attribute
applyColorScheme(settings.colorScheme);
// Listen for setting changes from popup
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'sync') return;
console.log('[Outlook Relook] Settings changed:', changes);
// Re-apply color scheme if it changed
if (changes.colorScheme) {
applyColorScheme(changes.colorScheme.newValue);
}
});
}
function applyColorScheme(scheme) {
let resolved = scheme;
if (scheme === 'system') {
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
document.documentElement.setAttribute('data-outlook-relook-scheme', resolved);
console.log('[Outlook Relook] Color scheme:', resolved);
}
// Listen for system theme changes when scheme is 'system'
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', async () => {
const settings = await OR.loadSettings();
if (settings.colorScheme === 'system') {
applyColorScheme('system');
}
});
init();
})();
```
- [ ] **Step 3: Verify settings load**
1. Reload extension at `chrome://extensions`
2. Open `outlook.office.com`, open DevTools Console
3. Verify: `[Outlook Relook] Loaded settings: {...}` shows all default values
4. Verify: `[Outlook Relook] Color scheme: light` (or `dark` depending on OS setting)
5. Verify: `document.documentElement.getAttribute('data-outlook-relook-scheme')` returns `"light"` or `"dark"`
- [ ] **Step 4: Commit**
```bash
git add content/settings-defaults.js content/content.js
git commit -m "feat: settings defaults, storage layer, color scheme attribute"
```
---
### Task 3: Selector Registry
**Files:**
- Create: `content/selectors.js`
The selector registry maps logical element names to resilient CSS selectors. Each entry has a `primary` selector and `fallbacks` array. The `resolve` function tries each in order and returns the first matching element(s).
- [ ] **Step 1: Create selectors.js**
```js
// Outlook Relook — Selector Registry
// Maps logical element names to resilient CSS selectors.
// Selectors use aria-label, data-*, role, and structural patterns
// to avoid depending on OWA's obfuscated class names.
//
// Selectors marked /* INSPECT OWA */ must be verified against the live DOM.
// Use Chrome DevTools on outlook.office.com to find correct selectors.
window.OutlookRelook = window.OutlookRelook || {};
window.OutlookRelook.SELECTORS = {
// --- Hide Elements ---
'copilot-button': {
primary: '[aria-label*="Copilot" i]',
fallbacks: ['[data-app-section="Copilot"]', '[title*="Copilot" i]'],
},
'copilot-pane': {
primary: '[aria-label*="Copilot" i][role="complementary"]',
fallbacks: [],
},
'copilot-compose-suggestions': {
primary: '[aria-label*="writing suggestion" i]',
fallbacks: ['[aria-label*="Copilot" i][role="listbox"]'],
},
'suggested-replies': {
primary: '[aria-label*="Suggested repl" i]',
fallbacks: ['[role="group"][aria-label*="Reply suggestion" i]'],
},
'promo-banners': {
primary: '[aria-label*="Try the new" i], [aria-label*="Upgrade" i], [aria-label*="Get the app" i], [aria-label*="premium" i]',
fallbacks: [],
},
'focused-other-tabs': {
primary: '[role="tablist"][aria-label*="Focused" i]',
fallbacks: ['[aria-label*="Focused Inbox" i]'],
},
'sidebar-app-icons': {
primary: 'nav[aria-label*="App" i], [role="navigation"][aria-label*="Module" i]',
fallbacks: [],
},
'groups-section': {
primary: '[aria-label*="Groups" i][role="tree"], [aria-label*="Groups" i][role="treeitem"]',
fallbacks: [],
},
'my-day-buttons': {
primary: '[aria-label*="My Day" i], [aria-label*="To Do" i][role="button"]',
fallbacks: [],
},
'sender-avatars': {
primary: '[role="listbox"] [aria-hidden="true"] img[src*="profile"], [role="listbox"] [aria-label*="avatar" i]',
fallbacks: [],
},
'feature-discovery': {
primary: '[role="dialog"][aria-label*="new feature" i], [role="dialog"][aria-label*="what\'s new" i], [aria-label*="teaching" i]',
fallbacks: [],
},
'viva-insights': {
primary: '[aria-label*="Viva" i], [aria-label*="Daily Briefing" i], [aria-label*="Briefing" i]',
fallbacks: [],
},
'unread-other-banner': {
primary: '[aria-label*="unread in Other" i]',
fallbacks: [],
},
'activity-feed': {
primary: '[aria-label*="Activity" i][role="complementary"], [aria-label*="mentioned you" i]',
fallbacks: [],
},
// --- Layout Regions (for density CSS) ---
'top-bar': {
primary: '[role="banner"]',
fallbacks: ['header'],
},
'command-bar': {
primary: '[role="toolbar"][aria-label*="command" i], [role="toolbar"][aria-label*="action" i]',
fallbacks: [],
},
'message-list': {
primary: '[role="listbox"][aria-label*="Message list" i], [role="list"][aria-label*="Message" i]',
fallbacks: [],
},
'reading-pane': {
primary: '[role="main"][aria-label*="Reading" i], [aria-label*="Message body" i]',
fallbacks: [],
},
'folder-pane': {
primary: '[role="navigation"][aria-label*="Folder" i], [role="tree"][aria-label*="Folder" i]',
fallbacks: [],
},
'compose-toolbar': {
primary: '[role="toolbar"][aria-label*="Format" i]',
fallbacks: [],
},
'search-bar': {
primary: '[role="search"], [aria-label*="Search" i] input',
fallbacks: [],
},
// --- Behavior targets ---
'ribbon-collapse-button': {
primary: '[aria-label*="Ribbon" i][aria-expanded], [aria-label*="collapse" i][role="button"]',
fallbacks: [],
},
'contact-hover-card': {
primary: '[role="dialog"][aria-label*="contact" i], [role="tooltip"][aria-label*="contact" i]',
fallbacks: [],
},
'notification-toast': {
primary: '[role="alert"], [role="status"][aria-live]',
fallbacks: [],
},
'reply-forward-bar': {
primary: '[aria-label*="Reply" i][role="button"], [aria-label*="Forward" i][role="button"]',
fallbacks: [],
},
'compose-window': {
primary: '[aria-label*="compose" i][role="dialog"], [aria-label*="New message" i]',
fallbacks: [],
},
'folder-header': {
primary: '[role="heading"][aria-level]',
fallbacks: [],
},
};
// Resolve a logical name to matching DOM elements.
// Tries primary selector first, then fallbacks in order.
// Returns an array of elements (possibly empty).
window.OutlookRelook.resolveSelector = function (name) {
const entry = window.OutlookRelook.SELECTORS[name];
if (!entry) {
console.warn('[Outlook Relook] Unknown selector: ' + name);
return [];
}
const selectors = [entry.primary, ...entry.fallbacks];
for (const selector of selectors) {
try {
const elements = document.querySelectorAll(selector);
if (elements.length > 0) return Array.from(elements);
} catch (e) {
console.warn('[Outlook Relook] Invalid selector for "' + name + '": ' + selector, e);
}
}
return [];
};
// Resolve a logical name to a CSS selector string that currently matches.
// Returns the first matching selector string, or null.
window.OutlookRelook.resolveSelectorString = function (name) {
const entry = window.OutlookRelook.SELECTORS[name];
if (!entry) return null;
const selectors = [entry.primary, ...entry.fallbacks];
for (const selector of selectors) {
try {
if (document.querySelector(selector)) return selector;
} catch (e) {
// skip invalid
}
}
return null;
};
```
- [ ] **Step 2: Verify selectors load**
1. Reload extension at `chrome://extensions`
2. Open `outlook.office.com`, open DevTools Console
3. Run: `OutlookRelook.SELECTORS['copilot-button']` — should return the selector entry object
4. Run: `OutlookRelook.resolveSelector('copilot-button')` — should return an array (may be empty if Copilot isn't visible, but should not error)
5. Run: `OutlookRelook.resolveSelector('nonexistent')` — should log a warning and return `[]`
- [ ] **Step 3: Commit**
```bash
git add content/selectors.js
git commit -m "feat: selector registry with resilient aria/data/structural selectors"
```
---
### Task 4: Base CSS Theme
**Files:**
- Create: `themes/base.css`
The base CSS is always loaded via the manifest's `css` array. It applies density/spacing overrides controlled by `data-outlook-relook-*` attributes that the content script sets on `<html>`.
- [ ] **Step 1: Create base.css**
```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:
```js
// 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**
```bash
git add themes/base.css content/content.js
git commit -m "feat: base CSS theme with density, hiding, and readability overrides"
```
---
### Task 5: Swiss Theme
**Files:**
- Create: `themes/swiss.css`
- [ ] **Step 1: Create swiss.css**
```css
/*
* Outlook Relook — Swiss / Helvetica Theme
* Monochrome, tight grid, no decoration. Content density is king.
*/
/* ============================================================
LIGHT VARIANT
============================================================ */
html[data-outlook-relook-scheme="light"] {
--or-bg-primary: #ffffff;
--or-bg-secondary: #fafafa;
--or-bg-tertiary: #f0f0f0;
--or-bg-hover: #e8e8e8;
--or-bg-selected: #e0e0e0;
--or-text-primary: #000000;
--or-text-secondary: #333333;
--or-text-tertiary: #666666;
--or-text-disabled: #999999;
--or-border: #d0d0d0;
--or-border-light: #e5e5e5;
--or-accent: var(--or-accent-override, #000000);
--or-accent-hover: var(--or-accent-override, #333333);
--or-shadow: none;
}
/* ============================================================
DARK VARIANT
============================================================ */
html[data-outlook-relook-scheme="dark"] {
--or-bg-primary: #1a1a1a;
--or-bg-secondary: #222222;
--or-bg-tertiary: #2a2a2a;
--or-bg-hover: #333333;
--or-bg-selected: #3a3a3a;
--or-text-primary: #e8e8e8;
--or-text-secondary: #cccccc;
--or-text-tertiary: #999999;
--or-text-disabled: #666666;
--or-border: #3a3a3a;
--or-border-light: #2f2f2f;
--or-accent: var(--or-accent-override, #e8e8e8);
--or-accent-hover: var(--or-accent-override, #ffffff);
--or-shadow: none;
}
/* ============================================================
TYPOGRAPHY
============================================================ */
html[data-outlook-relook-scheme] body,
html[data-outlook-relook-scheme] [role="main"],
html[data-outlook-relook-scheme] [role="navigation"],
html[data-outlook-relook-scheme] [role="complementary"],
html[data-outlook-relook-scheme] [role="banner"],
html[data-outlook-relook-scheme] input,
html[data-outlook-relook-scheme] button,
html[data-outlook-relook-scheme] select,
html[data-outlook-relook-scheme] textarea {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif !important;
letter-spacing: -0.01em !important;
}
/* ============================================================
SURFACES & BACKGROUNDS
============================================================ */
html[data-outlook-relook-scheme] body {
background-color: var(--or-bg-primary) !important;
color: var(--or-text-primary) !important;
}
/* Top bar */
html[data-outlook-relook-scheme] [role="banner"],
html[data-outlook-relook-scheme] header {
background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border) !important;
box-shadow: var(--or-shadow) !important;
}
/* Folder pane */
html[data-outlook-relook-scheme] [role="navigation"],
html[data-outlook-relook-scheme] [role="complementary"] {
background-color: var(--or-bg-secondary) !important;
border-right: 1px solid var(--or-border-light) !important;
}
/* Message list */
html[data-outlook-relook-scheme] [role="listbox"],
html[data-outlook-relook-scheme] [role="list"] {
background-color: var(--or-bg-primary) !important;
}
/* Message list items */
html[data-outlook-relook-scheme] [role="option"],
html[data-outlook-relook-scheme] [role="listitem"] {
background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border-light) !important;
color: var(--or-text-primary) !important;
}
html[data-outlook-relook-scheme] [role="option"]:hover,
html[data-outlook-relook-scheme] [role="listitem"]:hover {
background-color: var(--or-bg-hover) !important;
}
html[data-outlook-relook-scheme] [role="option"][aria-selected="true"],
html[data-outlook-relook-scheme] [role="listitem"][aria-selected="true"] {
background-color: var(--or-bg-selected) !important;
}
/* Reading pane */
html[data-outlook-relook-scheme] [role="main"] {
background-color: var(--or-bg-primary) !important;
}
/* ============================================================
DECORATIVE REMOVAL
============================================================ */
html[data-outlook-relook-scheme] * {
border-radius: 0 !important;
text-shadow: none !important;
}
html[data-outlook-relook-scheme] button,
html[data-outlook-relook-scheme] [role="button"],
html[data-outlook-relook-scheme] [role="tab"],
html[data-outlook-relook-scheme] [role="menuitem"] {
box-shadow: none !important;
background-image: none !important;
}
/* ============================================================
BUTTONS & INTERACTIVE
============================================================ */
html[data-outlook-relook-scheme] button:hover,
html[data-outlook-relook-scheme] [role="button"]:hover {
background-color: var(--or-bg-hover) !important;
}
html[data-outlook-relook-scheme] a {
color: var(--or-accent) !important;
}
html[data-outlook-relook-scheme] a:hover {
color: var(--or-accent-hover) !important;
}
/* ============================================================
TOOLBAR / COMMAND BAR
============================================================ */
html[data-outlook-relook-scheme] [role="toolbar"] {
background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border-light) !important;
box-shadow: none !important;
}
/* ============================================================
FOLDER PANE TREE ITEMS
============================================================ */
html[data-outlook-relook-scheme] [role="treeitem"] {
color: var(--or-text-secondary) !important;
}
html[data-outlook-relook-scheme] [role="treeitem"]:hover {
background-color: var(--or-bg-hover) !important;
color: var(--or-text-primary) !important;
}
html[data-outlook-relook-scheme] [role="treeitem"][aria-selected="true"] {
background-color: var(--or-bg-selected) !important;
color: var(--or-text-primary) !important;
font-weight: 600 !important;
}
/* ============================================================
SECONDARY TEXT
============================================================ */
html[data-outlook-relook-scheme] [role="option"] span,
html[data-outlook-relook-scheme] [role="listitem"] span {
color: var(--or-text-secondary) !important;
}
html[data-outlook-relook-scheme] time {
color: var(--or-text-tertiary) !important;
}
```
- [ ] **Step 2: Verify Swiss theme loads**
1. Reload extension
2. Open `outlook.office.com`
3. In DevTools Console, verify: `[Outlook Relook] Theme loaded: swiss`
4. Verify: Helvetica font family applied, no rounded corners, monochrome palette
5. Toggle OS dark mode: colors should invert to dark variant
- [ ] **Step 3: Commit**
```bash
git add themes/swiss.css
git commit -m "feat: Swiss/Helvetica theme with light and dark variants"
```
---
### Task 6: Material Theme
**Files:**
- Create: `themes/material.css`
- [ ] **Step 1: Create material.css**
```css
/*
* Outlook Relook — Material Flat Theme
* Clean, subtle elevation, comfortable density.
*/
/* ============================================================
LIGHT VARIANT
============================================================ */
html[data-outlook-relook-scheme="light"] {
--or-bg-primary: #ffffff;
--or-bg-secondary: #f5f5f5;
--or-bg-tertiary: #eeeeee;
--or-bg-hover: #e0e0e0;
--or-bg-selected: #e3f2fd;
--or-text-primary: #212121;
--or-text-secondary: #424242;
--or-text-tertiary: #757575;
--or-text-disabled: #9e9e9e;
--or-border: #e0e0e0;
--or-border-light: #eeeeee;
--or-accent: var(--or-accent-override, #1976d2);
--or-accent-hover: var(--or-accent-override, #1565c0);
--or-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
--or-shadow-elevated: 0 2px 6px rgba(0, 0, 0, 0.12);
}
/* ============================================================
DARK VARIANT
============================================================ */
html[data-outlook-relook-scheme="dark"] {
--or-bg-primary: #121212;
--or-bg-secondary: #1e1e1e;
--or-bg-tertiary: #252525;
--or-bg-hover: #333333;
--or-bg-selected: #1a3a5c;
--or-text-primary: #e0e0e0;
--or-text-secondary: #b0b0b0;
--or-text-tertiary: #808080;
--or-text-disabled: #5a5a5a;
--or-border: #333333;
--or-border-light: #2a2a2a;
--or-accent: var(--or-accent-override, #64b5f6);
--or-accent-hover: var(--or-accent-override, #90caf9);
--or-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
--or-shadow-elevated: 0 2px 6px rgba(0, 0, 0, 0.4);
}
/* ============================================================
TYPOGRAPHY
============================================================ */
html[data-outlook-relook-scheme] body,
html[data-outlook-relook-scheme] [role="main"],
html[data-outlook-relook-scheme] [role="navigation"],
html[data-outlook-relook-scheme] [role="complementary"],
html[data-outlook-relook-scheme] [role="banner"],
html[data-outlook-relook-scheme] input,
html[data-outlook-relook-scheme] button,
html[data-outlook-relook-scheme] select,
html[data-outlook-relook-scheme] textarea {
font-family: 'Roboto', system-ui, -apple-system, sans-serif !important;
letter-spacing: 0.01em !important;
}
/* ============================================================
SURFACES & BACKGROUNDS
============================================================ */
html[data-outlook-relook-scheme] body {
background-color: var(--or-bg-primary) !important;
color: var(--or-text-primary) !important;
}
html[data-outlook-relook-scheme] [role="banner"],
html[data-outlook-relook-scheme] header {
background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border) !important;
box-shadow: var(--or-shadow) !important;
}
html[data-outlook-relook-scheme] [role="navigation"],
html[data-outlook-relook-scheme] [role="complementary"] {
background-color: var(--or-bg-secondary) !important;
border-right: 1px solid var(--or-border-light) !important;
}
html[data-outlook-relook-scheme] [role="listbox"],
html[data-outlook-relook-scheme] [role="list"] {
background-color: var(--or-bg-primary) !important;
}
html[data-outlook-relook-scheme] [role="option"],
html[data-outlook-relook-scheme] [role="listitem"] {
background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border-light) !important;
color: var(--or-text-primary) !important;
}
html[data-outlook-relook-scheme] [role="option"]:hover,
html[data-outlook-relook-scheme] [role="listitem"]:hover {
background-color: var(--or-bg-hover) !important;
}
html[data-outlook-relook-scheme] [role="option"][aria-selected="true"],
html[data-outlook-relook-scheme] [role="listitem"][aria-selected="true"] {
background-color: var(--or-bg-selected) !important;
}
html[data-outlook-relook-scheme] [role="main"] {
background-color: var(--or-bg-primary) !important;
}
/* ============================================================
MATERIAL ROUNDING (4px on interactive elements only)
============================================================ */
html[data-outlook-relook-scheme] button,
html[data-outlook-relook-scheme] [role="button"],
html[data-outlook-relook-scheme] input,
html[data-outlook-relook-scheme] select,
html[data-outlook-relook-scheme] [role="tab"],
html[data-outlook-relook-scheme] [role="menuitem"] {
border-radius: 4px !important;
}
/* Cards / elevated surfaces */
html[data-outlook-relook-scheme] [role="dialog"],
html[data-outlook-relook-scheme] [role="alertdialog"] {
border-radius: 8px !important;
box-shadow: var(--or-shadow-elevated) !important;
}
/* ============================================================
BUTTONS & INTERACTIVE
============================================================ */
html[data-outlook-relook-scheme] button,
html[data-outlook-relook-scheme] [role="button"] {
box-shadow: none !important;
background-image: none !important;
transition: background-color 0.15s ease !important;
}
html[data-outlook-relook-scheme] button:hover,
html[data-outlook-relook-scheme] [role="button"]:hover {
background-color: var(--or-bg-hover) !important;
}
html[data-outlook-relook-scheme] a {
color: var(--or-accent) !important;
}
html[data-outlook-relook-scheme] a:hover {
color: var(--or-accent-hover) !important;
}
/* ============================================================
TOOLBAR
============================================================ */
html[data-outlook-relook-scheme] [role="toolbar"] {
background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border-light) !important;
box-shadow: var(--or-shadow) !important;
}
/* ============================================================
FOLDER PANE
============================================================ */
html[data-outlook-relook-scheme] [role="treeitem"] {
color: var(--or-text-secondary) !important;
border-radius: 4px !important;
margin: 1px 4px !important;
}
html[data-outlook-relook-scheme] [role="treeitem"]:hover {
background-color: var(--or-bg-hover) !important;
color: var(--or-text-primary) !important;
}
html[data-outlook-relook-scheme] [role="treeitem"][aria-selected="true"] {
background-color: var(--or-bg-selected) !important;
color: var(--or-accent) !important;
font-weight: 500 !important;
}
/* ============================================================
SECONDARY TEXT
============================================================ */
html[data-outlook-relook-scheme] [role="option"] span,
html[data-outlook-relook-scheme] [role="listitem"] span {
color: var(--or-text-secondary) !important;
}
html[data-outlook-relook-scheme] time {
color: var(--or-text-tertiary) !important;
}
```
**Note:** Both `swiss.css` and `material.css` define the same CSS custom property names but with different values. Only one theme is injected at a time via `content.js`'s `injectThemeCSS()`. The `base.css` (loaded via manifest, always present) references these variables via `var(--or-accent)` etc.
- [ ] **Step 2: Verify Material theme**
1. Reload extension
2. In DevTools Console, run: `chrome.storage.sync.set({theme: 'material'})`
3. Verify: rounded corners on buttons (4px), subtle shadows on toolbar, blue accent color
4. Switch to dark mode: verify dark Material palette applies
- [ ] **Step 3: Commit**
```bash
git add themes/material.css
git commit -m "feat: Material flat theme with light and dark variants"
```
---
### Task 7: MutationObserver
**Files:**
- Create: `content/observer.js`
The observer watches OWA's DOM for dynamically injected elements (Copilot prompts, banners, suggested replies, etc.) and removes them based on active settings. CSS `display: none` handles statically rendered elements; the observer handles elements OWA injects after page load.
- [ ] **Step 1: Create observer.js**
```js
// Outlook Relook — MutationObserver
// Watches OWA's DOM and removes dynamically injected elements
// based on active settings.
window.OutlookRelook = window.OutlookRelook || {};
window.OutlookRelook.Observer = (function () {
'use strict';
let observer = null;
let currentSettings = {};
// Map setting keys to selector registry names
// Each setting can suppress one or more logical elements
const SETTING_TO_SELECTORS = {
hideCopilot: ['copilot-button', 'copilot-pane', 'copilot-compose-suggestions'],
hideSuggestedReplies: ['suggested-replies'],
hidePromoBanners: ['promo-banners'],
hideFocusedOtherTabs: ['focused-other-tabs'],
hideSidebarAppIcons: ['sidebar-app-icons'],
hideGroupsSection: ['groups-section'],
hideMyDayButtons: ['my-day-buttons'],
hideSenderAvatars: ['sender-avatars'],
hideFeatureDiscovery: ['feature-discovery'],
hideVivaInsights: ['viva-insights'],
hideUnreadOtherBanner: ['unread-other-banner'],
hideActivityFeed: ['activity-feed'],
};
// Text content patterns to match elements by their inner text.
// Used when aria/data selectors don't catch dynamically injected content.
const TEXT_PATTERNS = {
hidePromoBanners: [
/try the new outlook/i,
/upgrade to premium/i,
/get the outlook app/i,
/switch to the new/i,
],
hideFeatureDiscovery: [
/what's new/i,
/new feature/i,
/did you know/i,
],
};
function suppressElements() {
for (const [settingKey, selectorNames] of Object.entries(SETTING_TO_SELECTORS)) {
if (!currentSettings[settingKey]) continue;
for (const name of selectorNames) {
const elements = window.OutlookRelook.resolveSelector(name);
for (const el of elements) {
if (el.style.display !== 'none') {
el.style.display = 'none';
console.log('[Outlook Relook] Suppressed: ' + name, el);
}
}
}
}
// Text-based suppression for elements missed by selectors
for (const [settingKey, patterns] of Object.entries(TEXT_PATTERNS)) {
if (!currentSettings[settingKey]) continue;
for (const pattern of patterns) {
// Check buttons, banners, and generic containers
const candidates = document.querySelectorAll(
'[role="alert"], [role="banner"], [role="dialog"], [role="status"], button, a'
);
for (const el of candidates) {
if (el.style.display === 'none') continue;
var text = el.textContent || '';
if (pattern.test(text) && text.length < 200) {
el.style.display = 'none';
console.log('[Outlook Relook] Suppressed by text: "' + text.trim().substring(0, 50) + '"', el);
}
}
}
}
}
function start(settings) {
currentSettings = settings;
// Initial pass
suppressElements();
// Watch for DOM mutations
observer = new MutationObserver(function (mutations) {
// Debounce: only process if nodes were actually added
var hasAddedNodes = false;
for (var i = 0; i < mutations.length; i++) {
if (mutations[i].addedNodes.length > 0) {
hasAddedNodes = true;
break;
}
}
if (hasAddedNodes) {
suppressElements();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
console.log('[Outlook Relook] Observer started');
}
function updateSettings(settings) {
currentSettings = settings;
// Re-run suppression with new settings
suppressElements();
}
function stop() {
if (observer) {
observer.disconnect();
observer = null;
console.log('[Outlook Relook] Observer stopped');
}
}
return { start: start, updateSettings: updateSettings, stop: stop };
})();
```
- [ ] **Step 2: Wire observer into content.js**
Add observer start/update calls to `content/content.js`. Add these lines inside the `init()` function after `injectThemeCSS(settings.theme)`:
At the end of `init()`, after `injectThemeCSS(settings.theme)`, add:
```js
// Start the MutationObserver
OR.Observer.start(settings);
```
Inside the `chrome.storage.onChanged` listener callback, after `applySettingsToDOM(updated)`, add:
```js
// Update observer with new settings
OR.Observer.updateSettings(updated);
```
The full `init()` function and listener should now be:
```js
async function init() {
const settings = await OR.loadSettings();
console.log('[Outlook Relook] Loaded settings:', settings);
applySettingsToDOM(settings);
injectThemeCSS(settings.theme);
// Start the MutationObserver
OR.Observer.start(settings);
// Listen for setting changes from popup
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'sync') return;
OR.loadSettings().then((updated) => {
applySettingsToDOM(updated);
OR.Observer.updateSettings(updated);
if (changes.theme) {
injectThemeCSS(changes.theme.newValue);
}
});
});
}
```
- [ ] **Step 3: Verify observer works**
1. Reload extension
2. Open `outlook.office.com`, open DevTools Console
3. Verify: `[Outlook Relook] Observer started`
4. If Copilot or suggested replies are present, verify `[Outlook Relook] Suppressed: copilot-button` (or similar) appears
5. Navigate within OWA (open emails, switch folders) — observer should continue suppressing newly injected elements
6. In Console, run: `chrome.storage.sync.set({hideCopilot: false})` — Copilot elements should reappear on next OWA re-render
- [ ] **Step 4: Commit**
```bash
git add content/observer.js content/content.js
git commit -m "feat: MutationObserver for dynamic element suppression"
```
---
### Task 8: Behavior Patches
**Files:**
- Create: `content/behavior.js`
JavaScript behavior patches for UX annoyances. Each behavior is gated by its setting key.
- [ ] **Step 1: Create behavior.js**
```js
// Outlook Relook — Behavior Patches
// JS-based UX improvements, each gated by a setting key.
window.OutlookRelook = window.OutlookRelook || {};
window.OutlookRelook.Behavior = (function () {
'use strict';
var OR = window.OutlookRelook;
var currentSettings = {};
var cleanupFns = [];
// --- Auto-collapse ribbon on page load ---
function setupAutoCollapseRibbon() {
if (!currentSettings.autoCollapseRibbon) return;
// Wait for OWA to finish rendering, then collapse the ribbon
var timer = setTimeout(function () {
var elements = OR.resolveSelector('ribbon-collapse-button');
for (var i = 0; i < elements.length; i++) {
if (elements[i].getAttribute('aria-expanded') === 'true') {
elements[i].click();
console.log('[Outlook Relook] Auto-collapsed ribbon');
}
}
}, 2000);
cleanupFns.push(function () { clearTimeout(timer); });
}
// --- Remember sidebar collapsed/expanded state ---
function setupRememberSidebar() {
if (!currentSettings.rememberSidebarState) return;
var savedState = localStorage.getItem('or-sidebar-collapsed');
if (savedState === 'true') {
var timer = setTimeout(function () {
var pane = OR.resolveSelector('folder-pane');
for (var i = 0; i < pane.length; i++) {
var toggle = pane[i].closest('[aria-expanded]') || pane[i].querySelector('[aria-expanded]');
if (toggle && toggle.getAttribute('aria-expanded') === 'true') {
toggle.click();
}
}
}, 2000);
cleanupFns.push(function () { clearTimeout(timer); });
}
// Watch for sidebar toggle changes
var sidebarObserver = new MutationObserver(function () {
var pane = OR.resolveSelector('folder-pane');
if (pane.length > 0) {
var isVisible = pane[0].offsetWidth > 50;
localStorage.setItem('or-sidebar-collapsed', String(!isVisible));
}
});
var timer2 = setTimeout(function () {
var pane = OR.resolveSelector('folder-pane');
if (pane.length > 0 && pane[0].parentElement) {
sidebarObserver.observe(pane[0].parentElement, { attributes: true, subtree: true });
}
}, 3000);
cleanupFns.push(function () {
clearTimeout(timer2);
sidebarObserver.disconnect();
});
}
// --- Suppress contact card hover popups ---
function setupSuppressContactHover() {
if (!currentSettings.suppressContactHover) return;
var handler = function (e) {
// Check if the hovered element or its ancestors trigger a contact card
var target = e.target.closest('[data-lpc-hover-target], [aria-haspopup="dialog"]');
if (target) {
e.stopPropagation();
e.preventDefault();
}
};
document.addEventListener('mouseenter', handler, true);
cleanupFns.push(function () { document.removeEventListener('mouseenter', handler, true); });
}
// --- Auto-advance to next email after delete ---
function setupAutoAdvance() {
if (!currentSettings.autoAdvanceAfterDelete) return;
var handler = function (e) {
// Detect delete key press
if (e.key === 'Delete' || e.key === 'Backspace') {
var selected = document.querySelector(
'[role="option"][aria-selected="true"], [role="listitem"][aria-selected="true"]'
);
if (selected) {
var next = selected.nextElementSibling;
if (next) {
// Wait for OWA to process the delete, then focus next
setTimeout(function () {
next.click();
next.focus();
}, 200);
}
}
}
};
document.addEventListener('keydown', handler, true);
cleanupFns.push(function () { document.removeEventListener('keydown', handler, true); });
}
// --- Auto-dismiss notification toasts ---
function setupAutoDismissToasts() {
if (currentSettings.autoDismissToasts === 'off') return;
var delay = parseInt(currentSettings.autoDismissToasts, 10) * 1000;
if (isNaN(delay) || delay <= 0) return;
var toastObserver = new MutationObserver(function (mutations) {
for (var m = 0; m < mutations.length; m++) {
var addedNodes = mutations[m].addedNodes;
for (var n = 0; n < addedNodes.length; n++) {
var node = addedNodes[n];
if (node.nodeType !== 1) continue;
var toasts = [];
if (node.matches && node.matches('[role="alert"], [role="status"][aria-live]')) {
toasts.push(node);
}
if (node.querySelectorAll) {
var found = node.querySelectorAll('[role="alert"], [role="status"][aria-live]');
for (var t = 0; t < found.length; t++) toasts.push(found[t]);
}
for (var i = 0; i < toasts.length; i++) {
(function (toast) {
setTimeout(function () {
// Try to find a dismiss button
var dismiss = toast.querySelector('[aria-label*="Close" i], [aria-label*="Dismiss" i]');
if (dismiss) {
dismiss.click();
} else {
toast.style.display = 'none';
}
}, delay);
})(toasts[i]);
}
}
}
});
toastObserver.observe(document.body, { childList: true, subtree: true });
cleanupFns.push(function () { toastObserver.disconnect(); });
}
// --- Reposition toast notifications ---
function setupToastPosition() {
if (currentSettings.toastPosition === 'bottom-left') return; // OWA default
var style = document.createElement('style');
style.id = 'or-toast-position';
style.textContent = [
'[role="alert"], [role="status"][aria-live] {',
' position: fixed !important;',
' top: 8px !important;',
' right: 8px !important;',
' bottom: auto !important;',
' left: auto !important;',
' z-index: 999999 !important;',
'}'
].join('\n');
document.head.appendChild(style);
cleanupFns.push(function () { style.remove(); });
}
// --- Sticky Reply/Forward bar ---
function setupStickyReplyBar() {
if (!currentSettings.stickyReplyBar) return;
var style = document.createElement('style');
style.id = 'or-sticky-reply';
style.textContent = [
'[aria-label*="Reply all" i][role="button"],',
'[aria-label*="Reply" i][role="button"],',
'[aria-label*="Forward" i][role="button"] {',
' position: sticky !important;',
' bottom: 0 !important;',
' z-index: 10 !important;',
' background-color: var(--or-bg-primary, #fff) !important;',
'}'
].join('\n');
document.head.appendChild(style);
cleanupFns.push(function () { style.remove(); });
}
// --- Auto-resize compose window ---
function setupAutoResizeCompose() {
if (!currentSettings.autoResizeCompose) return;
var composeObserver = new MutationObserver(function (mutations) {
for (var m = 0; m < mutations.length; m++) {
var addedNodes = mutations[m].addedNodes;
for (var n = 0; n < addedNodes.length; n++) {
var node = addedNodes[n];
if (node.nodeType !== 1) continue;
var composeWindows = [];
if (node.matches && node.matches('[aria-label*="compose" i][role="dialog"], [aria-label*="New message" i]')) {
composeWindows.push(node);
}
if (node.querySelectorAll) {
var found = node.querySelectorAll('[aria-label*="compose" i][role="dialog"], [aria-label*="New message" i]');
for (var i = 0; i < found.length; i++) composeWindows.push(found[i]);
}
for (var w = 0; w < composeWindows.length; w++) {
composeWindows[w].style.minHeight = '60vh';
composeWindows[w].style.height = '60vh';
console.log('[Outlook Relook] Auto-resized compose window');
}
}
}
});
composeObserver.observe(document.body, { childList: true, subtree: true });
cleanupFns.push(function () { composeObserver.disconnect(); });
}
// --- Throttle desktop notifications ---
function setupThrottleNotifications() {
if (!currentSettings.throttleNotifications) return;
var OrigNotification = window.Notification;
// Only patch if Notification API exists
if (!OrigNotification) return;
var lastNotificationTime = 0;
var MIN_INTERVAL = 30000; // 30 seconds between notifications
window.Notification = function (title, options) {
var now = Date.now();
if (now - lastNotificationTime < MIN_INTERVAL) {
console.log('[Outlook Relook] Throttled notification: "' + title + '"');
return {};
}
lastNotificationTime = now;
return new OrigNotification(title, options);
};
window.Notification.permission = OrigNotification.permission;
window.Notification.requestPermission = OrigNotification.requestPermission.bind(OrigNotification);
cleanupFns.push(function () {
window.Notification = OrigNotification;
});
}
// --- Public API ---
function start(settings) {
currentSettings = settings;
setupAutoCollapseRibbon();
setupRememberSidebar();
setupSuppressContactHover();
setupAutoAdvance();
setupAutoDismissToasts();
setupToastPosition();
setupStickyReplyBar();
setupAutoResizeCompose();
setupThrottleNotifications();
console.log('[Outlook Relook] Behavior patches applied');
}
function updateSettings(settings) {
// Tear down existing behaviors and re-apply
stop();
currentSettings = settings;
start(settings);
}
function stop() {
for (var i = 0; i < cleanupFns.length; i++) {
try { cleanupFns[i](); } catch (e) { /* ignore */ }
}
cleanupFns = [];
}
return { start: start, updateSettings: updateSettings, stop: stop };
})();
```
- [ ] **Step 2: Wire behavior into content.js**
In `content/content.js`, inside `init()`, after `OR.Observer.start(settings)`, add:
```js
// Apply behavior patches
OR.Behavior.start(settings);
```
Inside the `chrome.storage.onChanged` listener, after `OR.Observer.updateSettings(updated)`, add:
```js
// Update behavior patches
OR.Behavior.updateSettings(updated);
```
- [ ] **Step 3: Verify behavior patches**
1. Reload extension
2. Open `outlook.office.com`, open DevTools Console
3. Verify: `[Outlook Relook] Behavior patches applied`
4. Test auto-collapse: ribbon should collapse ~2s after page load
5. Test toast suppression: perform an action that triggers a toast (e.g., move an email) — it should auto-dismiss after 5s
6. Test contact hover suppression: hover over a sender name — contact card should not appear (or appear more slowly)
- [ ] **Step 4: Commit**
```bash
git add content/behavior.js content/content.js
git commit -m "feat: behavior patches — auto-collapse, hover suppress, toast dismiss, auto-advance"
```
---
### Task 9: DOM Injector (Quick Actions)
**Files:**
- Create: `content/injector.js`
Injects custom UI elements: "Mark all as read" button on folder headers and a keyboard-triggered folder jump dialog.
- [ ] **Step 1: Create injector.js**
```js
// Outlook Relook — DOM Injector
// Injects custom UI elements: mark-all-read button, folder jump dialog.
window.OutlookRelook = window.OutlookRelook || {};
window.OutlookRelook.Injector = (function () {
'use strict';
var OR = window.OutlookRelook;
var currentSettings = {};
var cleanupFns = [];
// --- "Mark all as read" button ---
function setupMarkAllRead() {
if (!currentSettings.markAllReadButton) return;
function injectButton() {
// Find folder headers that don't already have our button
var headers = OR.resolveSelector('folder-header');
for (var i = 0; i < headers.length; i++) {
var header = headers[i];
if (header.querySelector('.or-mark-all-read')) continue;
var btn = document.createElement('button');
btn.className = 'or-mark-all-read';
btn.textContent = '\u2713\u2713'; // double checkmark
btn.title = 'Mark all as read';
btn.style.cssText = [
'background: none;',
'border: 1px solid var(--or-border, #ccc);',
'color: var(--or-text-secondary, #666);',
'cursor: pointer;',
'font-size: 11px;',
'padding: 2px 6px;',
'margin-left: 8px;',
'border-radius: 3px;',
'line-height: 1;',
'vertical-align: middle;'
].join(' ');
btn.addEventListener('click', (function (hdr) {
return function (e) {
e.stopPropagation();
// Find the context menu "Mark all as read" option
var folder = hdr.closest('[role="treeitem"]');
if (folder) {
// Dispatch a contextmenu event to open OWA's context menu
var rect = folder.getBoundingClientRect();
var contextEvent = new MouseEvent('contextmenu', {
bubbles: true,
cancelable: true,
clientX: rect.x + 10,
clientY: rect.y + 10,
});
folder.dispatchEvent(contextEvent);
// Wait for context menu to render, then find and click "Mark all as read"
setTimeout(function () {
var menuItems = document.querySelectorAll('[role="menuitem"]');
for (var j = 0; j < menuItems.length; j++) {
if (/mark all as read/i.test(menuItems[j].textContent)) {
menuItems[j].click();
console.log('[Outlook Relook] Marked all as read');
return;
}
}
// Close context menu if option not found
document.body.click();
console.warn('[Outlook Relook] "Mark all as read" menu item not found');
}, 300);
}
};
})(header));
header.appendChild(btn);
}
}
// Inject on load and watch for new headers
var timer = setTimeout(injectButton, 3000);
var injectObserver = new MutationObserver(function () { injectButton(); });
injectObserver.observe(document.body, { childList: true, subtree: true });
cleanupFns.push(function () {
clearTimeout(timer);
injectObserver.disconnect();
var btns = document.querySelectorAll('.or-mark-all-read');
for (var k = 0; k < btns.length; k++) btns[k].remove();
});
}
// --- Quick folder jump (Ctrl+Shift+K) ---
function setupFolderJump() {
if (!currentSettings.quickFolderJump) return;
var dialog = null;
function createDialog() {
var overlay = document.createElement('div');
overlay.id = 'or-folder-jump';
overlay.style.cssText = [
'position: fixed;',
'top: 0; left: 0; right: 0; bottom: 0;',
'background: rgba(0,0,0,0.4);',
'z-index: 999999;',
'display: flex;',
'align-items: flex-start;',
'justify-content: center;',
'padding-top: 20vh;'
].join(' ');
var box = document.createElement('div');
box.style.cssText = [
'background: var(--or-bg-primary, #fff);',
'border: 1px solid var(--or-border, #ccc);',
'border-radius: 8px;',
'padding: 12px;',
'width: 400px;',
'max-height: 400px;',
'box-shadow: 0 8px 32px rgba(0,0,0,0.2);',
'font-family: inherit;'
].join(' ');
var input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Jump to folder...';
input.style.cssText = [
'width: 100%;',
'padding: 8px 12px;',
'border: 1px solid var(--or-border, #ccc);',
'border-radius: 4px;',
'font-size: 14px;',
'outline: none;',
'box-sizing: border-box;',
'background: var(--or-bg-secondary, #f5f5f5);',
'color: var(--or-text-primary, #000);'
].join(' ');
var results = document.createElement('div');
results.style.cssText = 'margin-top: 8px; max-height: 300px; overflow-y: auto;';
input.addEventListener('input', function () {
var query = input.value.toLowerCase().trim();
// Clear results using safe DOM methods
while (results.firstChild) {
results.removeChild(results.firstChild);
}
if (!query) return;
// Find all folder tree items
var folders = document.querySelectorAll('[role="treeitem"]');
var matches = [];
for (var i = 0; i < folders.length; i++) {
var name = (folders[i].textContent || '').trim();
if (name.toLowerCase().indexOf(query) !== -1) {
matches.push({ name: name, element: folders[i] });
}
}
var limit = Math.min(matches.length, 10);
for (var j = 0; j < limit; j++) {
(function (match) {
var item = document.createElement('div');
item.textContent = match.name;
item.style.cssText = [
'padding: 6px 12px;',
'cursor: pointer;',
'border-radius: 4px;',
'color: var(--or-text-primary, #000);'
].join(' ');
item.addEventListener('mouseenter', function () {
item.style.backgroundColor = 'var(--or-bg-hover, #e0e0e0)';
});
item.addEventListener('mouseleave', function () {
item.style.backgroundColor = '';
});
item.addEventListener('click', function () {
match.element.click();
closeDialog();
});
results.appendChild(item);
})(matches[j]);
}
});
box.appendChild(input);
box.appendChild(results);
overlay.appendChild(box);
overlay.addEventListener('click', function (e) {
if (e.target === overlay) closeDialog();
});
input.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeDialog();
if (e.key === 'Enter') {
var firstResult = results.querySelector('div');
if (firstResult) firstResult.click();
}
});
return { overlay: overlay, input: input };
}
function openDialog() {
if (dialog) return;
var created = createDialog();
document.body.appendChild(created.overlay);
dialog = created.overlay;
setTimeout(function () { created.input.focus(); }, 50);
}
function closeDialog() {
if (dialog) {
dialog.remove();
dialog = null;
}
}
var handler = function (e) {
// Ctrl+Shift+K (or Cmd+Shift+K on Mac)
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'K') {
e.preventDefault();
e.stopPropagation();
if (dialog) {
closeDialog();
} else {
openDialog();
}
}
};
document.addEventListener('keydown', handler, true);
cleanupFns.push(function () {
document.removeEventListener('keydown', handler, true);
closeDialog();
});
}
// --- Public API ---
function start(settings) {
currentSettings = settings;
setupMarkAllRead();
setupFolderJump();
console.log('[Outlook Relook] Injector started');
}
function updateSettings(settings) {
stop();
currentSettings = settings;
start(settings);
}
function stop() {
for (var i = 0; i < cleanupFns.length; i++) {
try { cleanupFns[i](); } catch (e) { /* ignore */ }
}
cleanupFns = [];
}
return { start: start, updateSettings: updateSettings, stop: stop };
})();
```
- [ ] **Step 2: Wire injector into content.js**
In `content/content.js`, inside `init()`, after `OR.Behavior.start(settings)`, add:
```js
// Start DOM injector (quick actions)
OR.Injector.start(settings);
```
Inside the `chrome.storage.onChanged` listener, after `OR.Behavior.updateSettings(updated)`, add:
```js
// Update injector
OR.Injector.updateSettings(updated);
```
- [ ] **Step 3: Verify injections**
1. Reload extension
2. Open `outlook.office.com`
3. Verify: folder headers have a small double-checkmark button next to them
4. Click the button — should trigger OWA's "Mark all as read" context menu action
5. Press `Ctrl+Shift+K` — folder jump dialog should appear
6. Type a folder name — matching folders should appear in the results list
7. Click a result — should navigate to that folder
8. Press `Escape` — dialog should close
- [ ] **Step 4: Commit**
```bash
git add content/injector.js content/content.js
git commit -m "feat: DOM injector — mark-all-read button and folder jump dialog"
```
---
### Task 10: Settings Popup — HTML & CSS
**Files:**
- Modify: `popup/popup.html` (replace stub)
- Create: `popup/popup.css`
- [ ] **Step 1: Create popup.css**
```css
/* Outlook Relook — Settings Popup */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
width: 360px;
max-height: 540px;
overflow-y: auto;
font-family: system-ui, -apple-system, sans-serif;
font-size: 13px;
line-height: 1.4;
color: #222;
background: #fff;
padding: 0;
}
/* Header */
.or-header {
padding: 12px 16px;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
background: #fff;
z-index: 10;
}
.or-header h1 {
font-size: 15px;
font-weight: 600;
letter-spacing: -0.02em;
}
.or-header .or-version {
font-size: 11px;
color: #999;
}
/* Sections */
.or-section {
border-bottom: 1px solid #f0f0f0;
}
.or-section-header {
padding: 10px 16px 6px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #888;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
user-select: none;
}
.or-section-header::after {
content: '\25B6'; /* right-pointing triangle */
font-size: 8px;
transition: transform 0.15s;
}
.or-section.open .or-section-header::after {
transform: rotate(90deg);
}
.or-section-body {
display: none;
padding: 0 16px 10px;
}
.or-section.open .or-section-body {
display: block;
}
/* Toggle rows */
.or-toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 0;
gap: 8px;
}
.or-toggle-row label {
flex: 1;
cursor: pointer;
font-size: 12.5px;
}
/* Toggle switch */
.or-switch {
position: relative;
width: 36px;
height: 20px;
flex-shrink: 0;
}
.or-switch input {
opacity: 0;
width: 0;
height: 0;
}
.or-switch .slider {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: #ccc;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s;
}
.or-switch .slider::before {
content: '';
position: absolute;
width: 16px;
height: 16px;
left: 2px;
top: 2px;
background: #fff;
border-radius: 50%;
transition: transform 0.2s;
}
.or-switch input:checked + .slider {
background: #1976d2;
}
.or-switch input:checked + .slider::before {
transform: translateX(16px);
}
/* Select/dropdown rows */
.or-select-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 0;
gap: 8px;
}
.or-select-row label {
flex: 1;
font-size: 12.5px;
}
.or-select-row select {
padding: 3px 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 12px;
background: #fff;
color: #222;
cursor: pointer;
}
/* Color picker */
.or-color-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 0;
gap: 8px;
}
.or-color-row label {
flex: 1;
font-size: 12.5px;
}
.or-color-row input[type="color"] {
width: 32px;
height: 24px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
padding: 0;
}
/* Radio group */
.or-radio-group {
display: flex;
gap: 12px;
padding: 5px 0;
}
.or-radio-group label {
display: flex;
align-items: center;
gap: 4px;
font-size: 12.5px;
cursor: pointer;
}
/* Footer */
.or-footer {
padding: 10px 16px;
display: flex;
gap: 8px;
justify-content: flex-end;
}
.or-footer button {
padding: 5px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
font-size: 12px;
cursor: pointer;
color: #222;
}
.or-footer button:hover {
background: #f0f0f0;
}
.or-footer button.danger {
color: #d32f2f;
border-color: #d32f2f;
}
.or-footer button.danger:hover {
background: #fce4ec;
}
/* Scrollbar */
body::-webkit-scrollbar {
width: 6px;
}
body::-webkit-scrollbar-track {
background: transparent;
}
body::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
```
- [ ] **Step 2: Replace popup.html**
```html
<!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**
```bash
git add popup/popup.html popup/popup.css
git commit -m "feat: settings popup HTML and CSS with all toggle categories"
```
---
### Task 11: Settings Popup — JavaScript Logic
**Files:**
- Create: `popup/popup.js`
Handles loading settings into the UI, saving changes on toggle, density preset behavior, and export/import/reset.
- [ ] **Step 1: Create popup.js**
```js
// Outlook Relook — Popup Settings Logic
(function () {
'use strict';
// We need access to DEFAULTS and DENSITY_PRESETS from settings-defaults.js,
// but popup runs in its own context. Duplicate the defaults here.
// (Content scripts and popup don't share a JS context.)
var DEFAULTS = {
theme: 'swiss',
colorScheme: 'system',
accentColor: '',
densityPreset: 'compact',
compactTopBar: true,
compactCommandBar: true,
compactMessageList: true,
compactReadingPane: true,
compactFolderPane: true,
narrowDateColumn: true,
compressComposeToolbar: true,
readingPaneMaxWidth: true,
hideCopilot: true,
hideSuggestedReplies: true,
hidePromoBanners: true,
hideFocusedOtherTabs: true,
hideSidebarAppIcons: false,
hideGroupsSection: false,
hideMyDayButtons: true,
hideSenderAvatars: false,
hideFeatureDiscovery: true,
hideVivaInsights: true,
hideUnreadOtherBanner: true,
hideActivityFeed: true,
unreadDistinction: true,
previewOwnLine: false,
normalizeFontWeight: true,
darkModeEmailFix: true,
messageListFontSize: 'medium',
autoCollapseRibbon: true,
rememberSidebarState: true,
suppressContactHover: true,
autoAdvanceAfterDelete: true,
autoDismissToasts: '5',
toastPosition: 'top-right',
stickyReplyBar: true,
autoResizeCompose: true,
throttleNotifications: false,
markAllReadButton: true,
quickFolderJump: true,
};
var DENSITY_PRESETS = {
comfortable: {
compactTopBar: false,
compactCommandBar: false,
compactMessageList: false,
compactReadingPane: false,
compactFolderPane: false,
narrowDateColumn: false,
compressComposeToolbar: false,
readingPaneMaxWidth: true,
},
compact: {
compactTopBar: true,
compactCommandBar: true,
compactMessageList: true,
compactReadingPane: true,
compactFolderPane: true,
narrowDateColumn: true,
compressComposeToolbar: true,
readingPaneMaxWidth: true,
},
'ultra-compact': {
compactTopBar: true,
compactCommandBar: true,
compactMessageList: true,
compactReadingPane: true,
compactFolderPane: true,
narrowDateColumn: true,
compressComposeToolbar: true,
readingPaneMaxWidth: true,
},
};
// --- Load settings and populate UI ---
function loadUI() {
chrome.storage.sync.get(DEFAULTS, function (settings) {
// Checkboxes
var checkboxes = document.querySelectorAll('input[type="checkbox"][data-setting]');
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = !!settings[checkboxes[i].dataset.setting];
}
// Selects
var selects = document.querySelectorAll('select[data-setting]');
for (var j = 0; j < selects.length; j++) {
selects[j].value = settings[selects[j].dataset.setting] || '';
}
// Radio groups
var radioGroups = document.querySelectorAll('.or-radio-group[data-setting]');
for (var k = 0; k < radioGroups.length; k++) {
var key = radioGroups[k].dataset.setting;
var radio = radioGroups[k].querySelector('input[value="' + settings[key] + '"]');
if (radio) radio.checked = true;
}
// Color picker
var colorPicker = document.querySelector('input[type="color"][data-setting]');
if (colorPicker) {
colorPicker.value = settings.accentColor || '#1976d2';
}
// Version
var manifest = chrome.runtime.getManifest();
document.getElementById('version').textContent = 'v' + manifest.version;
});
}
// --- Save a single setting ---
function saveSetting(key, value) {
var obj = {};
obj[key] = value;
chrome.storage.sync.set(obj);
}
// --- Section accordion ---
var sectionHeaders = document.querySelectorAll('.or-section-header');
for (var s = 0; s < sectionHeaders.length; s++) {
sectionHeaders[s].addEventListener('click', function () {
this.parentElement.classList.toggle('open');
});
}
// --- Checkbox change handlers ---
var checkboxes = document.querySelectorAll('input[type="checkbox"][data-setting]');
for (var c = 0; c < checkboxes.length; c++) {
checkboxes[c].addEventListener('change', function () {
saveSetting(this.dataset.setting, this.checked);
});
}
// --- Select change handlers ---
var selects = document.querySelectorAll('select[data-setting]');
for (var sl = 0; sl < selects.length; sl++) {
selects[sl].addEventListener('change', function () {
var settingKey = this.dataset.setting;
var value = this.value;
saveSetting(settingKey, value);
// Density preset: apply to individual toggles
if (settingKey === 'densityPreset' && DENSITY_PRESETS[value]) {
var preset = DENSITY_PRESETS[value];
chrome.storage.sync.set(preset);
// Update checkboxes in the UI
var toggleKeys = Object.keys(preset);
for (var p = 0; p < toggleKeys.length; p++) {
var checkbox = document.getElementById(toggleKeys[p]);
if (checkbox) checkbox.checked = preset[toggleKeys[p]];
}
}
});
}
// --- Radio group change handlers ---
var radioGroups = document.querySelectorAll('.or-radio-group[data-setting]');
for (var r = 0; r < radioGroups.length; r++) {
var groupKey = radioGroups[r].dataset.setting;
var radios = radioGroups[r].querySelectorAll('input[type="radio"]');
for (var ri = 0; ri < radios.length; ri++) {
(function (gk) {
radios[ri].addEventListener('change', function () {
if (this.checked) saveSetting(gk, this.value);
});
})(groupKey);
}
}
// --- Color picker change handler ---
var colorPickers = document.querySelectorAll('input[type="color"][data-setting]');
for (var cp = 0; cp < colorPickers.length; cp++) {
colorPickers[cp].addEventListener('input', function () {
saveSetting(this.dataset.setting, this.value);
});
}
// --- Export settings ---
document.getElementById('exportBtn').addEventListener('click', function () {
chrome.storage.sync.get(DEFAULTS, function (settings) {
var blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'outlook-relook-settings.json';
a.click();
URL.revokeObjectURL(url);
});
});
// --- Import settings ---
document.getElementById('importBtn').addEventListener('click', function () {
document.getElementById('importFile').click();
});
document.getElementById('importFile').addEventListener('change', function (e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function (event) {
try {
var imported = JSON.parse(event.target.result);
// Only import known keys
var cleaned = {};
var defaultKeys = Object.keys(DEFAULTS);
for (var i = 0; i < defaultKeys.length; i++) {
if (defaultKeys[i] in imported) cleaned[defaultKeys[i]] = imported[defaultKeys[i]];
}
chrome.storage.sync.set(cleaned, function () {
loadUI();
});
} catch (err) {
alert('Invalid settings file.');
}
};
reader.readAsText(file);
e.target.value = '';
});
// --- Reset to defaults ---
document.getElementById('resetBtn').addEventListener('click', function () {
if (confirm('Reset all settings to defaults?')) {
chrome.storage.sync.set(DEFAULTS, function () {
loadUI();
});
}
});
// --- Init ---
loadUI();
})();
```
- [ ] **Step 2: Verify popup functionality**
1. Reload extension
2. Click extension icon — settings should load with default values
3. Toggle "Compact top bar" off — switch should turn gray
4. Close and reopen popup — the toggle should remain off (persisted)
5. Open OWA — verify the top bar is no longer compact
6. Toggle it back on — OWA top bar should re-compact without page reload
7. Change theme dropdown to "Material" — OWA should switch to Material theme
8. Change color scheme to "Dark" — OWA should switch to dark variant
9. Change density preset to "Comfortable" — all individual density toggles should uncheck
10. Click "Export" — a JSON file should download
11. Click "Reset" — all settings should return to defaults
12. Click "Import" — select the exported JSON — settings should restore
- [ ] **Step 3: Commit**
```bash
git add popup/popup.js
git commit -m "feat: popup settings logic — load, save, presets, export/import/reset"
```
---
### Task 12: Selectors Test Page
**Files:**
- Create: `selectors-test.html`
A static HTML file with mock OWA DOM fragments for offline selector verification.
- [ ] **Step 1: Create selectors-test.html**
```html
<!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**
```bash
git add selectors-test.html
git commit -m "feat: selector test page with mock OWA DOM fragments"
```
---
### Task 13: README & Final Integration
**Files:**
- Create: `README.md`
- [ ] **Step 1: Create README.md**
```markdown
# Outlook Relook
A Chrome extension that reskins Outlook Web App (outlook.office.com) with minimalist themes and granular control over visual clutter.
## Features
- **Switchable themes:** Swiss (Helvetica minimalism) and Material (clean, subtle elevation)
- **Light & Dark modes** with system preference detection
- **~40 granular toggles** for density, element hiding, readability, and behavior
- **MutationObserver** suppresses dynamically injected clutter (Copilot, banners, suggested replies)
- **Behavior patches:** auto-collapse ribbon, suppress contact hover cards, auto-advance after delete, toast management
- **Quick actions:** "Mark all as read" button, Ctrl+Shift+K folder jump
- **Settings sync** across Chrome instances via chrome.storage.sync
- **Export/Import/Reset** settings
## Install (Development)
1. Clone this repo
2. Open `chrome://extensions` in Chrome
3. Enable "Developer mode" (top-right toggle)
4. Click "Load unpacked" and select this directory
5. Open [outlook.office.com](https://outlook.office.com) — the extension activates automatically
## Usage
Click the extension icon to open the settings panel. Changes apply immediately — no page reload needed.
### Keyboard Shortcuts
- `Ctrl+Shift+K` (or `Cmd+Shift+K` on Mac) — Quick folder jump
## Development
No build step. Edit files, reload the extension at `chrome://extensions`, refresh the OWA tab.
- `themes/base.css` — density/spacing/hiding rules (always loaded)
- `themes/swiss.css` / `themes/material.css` — theme-specific styles
- `content/` — content scripts (observer, behavior, injector)
- `popup/` — settings panel
- `selectors-test.html` — offline selector verification
### Selector Strategy
OWA uses obfuscated class names. Selectors prioritize `aria-label`, `data-*`, and `role` attributes over class names. See `content/selectors.js` for the registry.
When selectors break after an OWA update, check the console for `[Outlook Relook]` warnings, inspect the live DOM, and update `selectors.js`.
## Adding a New Theme
1. Create `themes/yourtheme.css` — define the same CSS custom properties as `swiss.css`
2. Add an option to the theme dropdown in `popup/popup.html`
3. That's it — the content script handles injection automatically
## Adding a New Toggle
1. Add the key and default to `DEFAULTS` in both `content/settings-defaults.js` and `popup/popup.js`
2. Add the toggle HTML to the appropriate section in `popup/popup.html`
3. If CSS-based: add a `data-or-*` attribute mapping in `content/content.js` and the CSS rule in `themes/base.css`
4. If observer-based: add a selector entry to `content/selectors.js` and a mapping in `content/observer.js`
5. If behavior-based: add a setup function in `content/behavior.js`
```
- [ ] **Step 2: Verify the full extension end-to-end**
1. Reload extension
2. Open `outlook.office.com`
3. Verify in console: all `[Outlook Relook]` log messages appear without errors
4. Open popup, toggle settings, verify each takes effect on the OWA page
5. Switch themes between Swiss and Material — visual difference should be clear
6. Toggle light/dark — colors should switch
7. Toggle Copilot hiding off/on — Copilot elements should appear/disappear
8. Press Ctrl+Shift+K — folder jump dialog should work
9. Export settings, reset, import — round-trip should work
10. Open `selectors-test.html` — all selector tests should pass
- [ ] **Step 3: Commit**
```bash
git add README.md
git commit -m "docs: README with install, usage, and development guide"
```
---
## Self-Review Results
**Spec coverage:** All spec sections are covered:
- Architecture (Tasks 1-2), Theme System (Tasks 4-6), Selector Strategy (Task 3), Settings Panel (Tasks 10-11), all toggle categories (distributed across Tasks 4, 7, 8, 9), File Structure (Task 1), Development & Testing (Task 12-13). Verified each spec requirement maps to a task.
**Placeholder scan:** No TBDs or TODOs. All code blocks are complete. OWA-specific selectors are noted with a clear strategy (inspect live DOM) rather than left as placeholders.
**Type consistency:** Verified across tasks:
- `window.OutlookRelook` namespace used consistently
- `OR.Observer.start/updateSettings/stop` API matches between `observer.js` (Task 7) and `content.js` (Task 7 wiring)
- `OR.Behavior.start/updateSettings/stop` API matches between `behavior.js` (Task 8) and `content.js` (Task 8 wiring)
- `OR.Injector.start/updateSettings/stop` API matches between `injector.js` (Task 9) and `content.js` (Task 9 wiring)
- `OR.loadSettings`, `OR.saveSettings`, `OR.DEFAULTS`, `OR.DENSITY_PRESETS` match between `settings-defaults.js` (Task 2) and `popup.js` (Task 11, duplicated for popup context)
- `OR.resolveSelector`, `OR.resolveSelectorString` match between `selectors.js` (Task 3) and consumers in `observer.js`, `behavior.js`, `injector.js`
- `SETTING_TO_ATTR` and `SETTING_TO_ATTR_VALUE` maps in `content.js` (Task 4) match the data attributes referenced in `base.css` (Task 4)
- `DEFAULTS` in `popup.js` (Task 11) matches `DEFAULTS` in `settings-defaults.js` (Task 2)