13 tasks covering manifest, settings, selectors, themes, observer, behavior patches, injector, popup UI, and test page.
3306 lines
109 KiB
Markdown
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)
|