fix: keyboard multi-select uses stable IDs, improve Swiss theme, add Brutalist theme

This commit is contained in:
Joel Brock
2026-04-23 09:39:49 -07:00
parent 702bca4396
commit 6a6f9ba4f7
4 changed files with 517 additions and 198 deletions

View File

@@ -1,6 +1,9 @@
// Outlook Relook — Gmail-Style Keyboard Navigation & Multi-Select // Outlook Relook — Gmail-Style Keyboard Navigation & Multi-Select
// Adds keyboard focus cursor and multi-select to OWA's message list. // Adds keyboard focus cursor and multi-select to OWA's message list.
// Gated by the keyboardMultiSelect setting. // Gated by the keyboardMultiSelect setting.
//
// Tracks selections by stable message ID (data attribute or aria-label)
// rather than DOM element reference, since OWA frequently re-renders rows.
window.OutlookRelook = window.OutlookRelook || {}; window.OutlookRelook = window.OutlookRelook || {};
@@ -11,26 +14,42 @@ window.OutlookRelook.Keyboard = (function () {
var currentSettings = {}; var currentSettings = {};
var cleanupFns = []; var cleanupFns = [];
// State // State — track by ID strings, not DOM references
var focusedIndex = -1; // Index of the focused message in the list var focusedId = null; // ID of the focused message
var selectedSet = new Set(); // Set of selected message DOM elements var selectedIds = new Set(); // Set of selected message IDs
var countBadge = null; // Selection count badge element var countBadge = null;
// --- Helpers --- // --- Message ID extraction ---
// OWA messages have various attributes we can use as stable IDs.
// Try data-convid, data-itemid, id, or fall back to aria-label.
function getMessageId(el) {
return el.getAttribute('data-convid')
|| el.getAttribute('data-itemid')
|| el.getAttribute('data-tid')
|| el.getAttribute('id')
|| el.getAttribute('aria-label')
|| null;
}
function getMessageItems() { function getMessageItems() {
// Get all message items from the message list
var items = document.querySelectorAll( var items = document.querySelectorAll(
'[role="listbox"] [role="option"], [role="list"] [role="listitem"]' '[role="listbox"] [role="option"], [role="list"] [role="listitem"]'
); );
return Array.from(items); return Array.from(items);
} }
function findItemById(items, id) {
if (!id) return -1;
for (var i = 0; i < items.length; i++) {
if (getMessageId(items[i]) === id) return i;
}
return -1;
}
function isComposeOrDialogActive() { function isComposeOrDialogActive() {
var active = document.activeElement; var active = document.activeElement;
if (!active) return false; if (!active) return false;
// Check if focus is in a compose area, search bar, or dialog
var tag = active.tagName.toLowerCase(); var tag = active.tagName.toLowerCase();
if (tag === 'input' || tag === 'textarea') return true; if (tag === 'input' || tag === 'textarea') return true;
if (active.getAttribute('contenteditable') === 'true') return true; if (active.getAttribute('contenteditable') === 'true') return true;
@@ -38,53 +57,90 @@ window.OutlookRelook.Keyboard = (function () {
if (active.closest('[role="search"]')) return true; if (active.closest('[role="search"]')) return true;
if (active.closest('[aria-label*="compose" i]')) return true; if (active.closest('[aria-label*="compose" i]')) return true;
if (active.closest('[aria-label*="New message" i]')) return true; if (active.closest('[aria-label*="New message" i]')) return true;
return false; return false;
} }
function setFocus(items, index) { // --- Visual state application ---
// Remove old focus // Re-applies focus and selection classes to current DOM elements
var oldFocused = document.querySelector('.or-kb-focused'); // based on the ID-based state. Called after every key action and
if (oldFocused) oldFocused.classList.remove('or-kb-focused'); // by the MutationObserver when OWA re-renders.
if (index < 0 || index >= items.length) return; function applyVisualState(items) {
// Clear all visual markers first
var oldFocused = document.querySelectorAll('.or-kb-focused');
for (var f = 0; f < oldFocused.length; f++) oldFocused[f].classList.remove('or-kb-focused');
var oldSelected = document.querySelectorAll('.or-kb-selected');
for (var s = 0; s < oldSelected.length; s++) oldSelected[s].classList.remove('or-kb-selected');
focusedIndex = index; if (!items) items = getMessageItems();
var item = items[index];
item.classList.add('or-kb-focused');
// Scroll into view if needed // Apply focus
item.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); if (focusedId) {
} var focusIdx = findItemById(items, focusedId);
if (focusIdx >= 0) {
function toggleSelect(item) { items[focusIdx].classList.add('or-kb-focused');
if (selectedSet.has(item)) { }
selectedSet.delete(item);
item.classList.remove('or-kb-selected');
} else {
selectedSet.add(item);
item.classList.add('or-kb-selected');
} }
// Apply selections
selectedIds.forEach(function (id) {
var idx = findItemById(items, id);
if (idx >= 0) {
items[idx].classList.add('or-kb-selected');
}
});
updateCountBadge(); updateCountBadge();
} }
// --- Focus management ---
function getFocusedIndex(items) {
if (!focusedId) return -1;
return findItemById(items, focusedId);
}
function setFocus(items, index) {
if (index < 0 || index >= items.length) return;
focusedId = getMessageId(items[index]);
applyVisualState(items);
items[index].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
// --- Selection management ---
function toggleSelect(items, index) {
if (index < 0 || index >= items.length) return;
var id = getMessageId(items[index]);
if (!id) return;
if (selectedIds.has(id)) {
selectedIds.delete(id);
} else {
selectedIds.add(id);
}
applyVisualState(items);
}
function clearSelection() { function clearSelection() {
selectedSet.forEach(function (item) { selectedIds.clear();
item.classList.remove('or-kb-selected'); var oldSelected = document.querySelectorAll('.or-kb-selected');
}); for (var i = 0; i < oldSelected.length; i++) oldSelected[i].classList.remove('or-kb-selected');
selectedSet.clear();
updateCountBadge(); updateCountBadge();
} }
function getActionTargets(items) { function getActionTargets(items) {
// If messages are selected, return those. Otherwise return the focused message. var targets = [];
if (selectedSet.size > 0) { if (selectedIds.size > 0) {
return Array.from(selectedSet); selectedIds.forEach(function (id) {
var idx = findItemById(items, id);
if (idx >= 0) targets.push(items[idx]);
});
} else {
var fi = getFocusedIndex(items);
if (fi >= 0) targets.push(items[fi]);
} }
if (focusedIndex >= 0 && focusedIndex < items.length) { return targets;
return [items[focusedIndex]];
}
return [];
} }
// --- Selection count badge --- // --- Selection count badge ---
@@ -101,7 +157,7 @@ window.OutlookRelook.Keyboard = (function () {
function updateCountBadge() { function updateCountBadge() {
if (!countBadge) countBadge = createCountBadge(); if (!countBadge) countBadge = createCountBadge();
if (!countBadge) return; if (!countBadge) return;
var count = selectedSet.size; var count = selectedIds.size;
if (count === 0) { if (count === 0) {
countBadge.style.opacity = '0'; countBadge.style.opacity = '0';
} else { } else {
@@ -111,30 +167,20 @@ window.OutlookRelook.Keyboard = (function () {
} }
// --- Actions --- // --- Actions ---
// Each action simulates what a user would do in OWA to perform the operation.
// We click the message to select it in OWA, then trigger the toolbar action.
function performAction(targets, actionFn) { function performAction(targets, actionFn) {
if (targets.length === 0) return; if (targets.length === 0) return;
// Process targets one at a time with a small delay between each
var i = 0; var i = 0;
function processNext() { function processNext() {
if (i >= targets.length) { if (i >= targets.length) {
// After all targets processed, clear selection
clearSelection(); clearSelection();
return; return;
} }
var target = targets[i]; var target = targets[i];
i++; i++;
// Click the message to make it OWA-selected
target.click(); target.click();
// Small delay to let OWA register the selection, then perform action
setTimeout(function () { setTimeout(function () {
actionFn(target); actionFn(target);
// Delay before next target
setTimeout(processNext, 150); setTimeout(processNext, 150);
}, 100); }, 100);
} }
@@ -143,19 +189,14 @@ window.OutlookRelook.Keyboard = (function () {
function actionDelete(targets) { function actionDelete(targets) {
performAction(targets, function () { performAction(targets, function () {
// Find and click the delete button in the toolbar
var deleteBtn = document.querySelector( var deleteBtn = document.querySelector(
'[aria-label*="Delete" i][role="button"], [aria-label*="delete" i][role="menuitem"]' '[aria-label*="Delete" i][role="button"], [aria-label*="delete" i][role="menuitem"]'
); );
if (deleteBtn) { if (deleteBtn) {
deleteBtn.click(); deleteBtn.click();
} else { } else {
// Fallback: try dispatching Delete key
var deleteEvent = new KeyboardEvent('keydown', { var deleteEvent = new KeyboardEvent('keydown', {
key: 'Delete', key: 'Delete', code: 'Delete', bubbles: true, cancelable: true
code: 'Delete',
bubbles: true,
cancelable: true
}); });
document.activeElement.dispatchEvent(deleteEvent); document.activeElement.dispatchEvent(deleteEvent);
} }
@@ -183,7 +224,6 @@ window.OutlookRelook.Keyboard = (function () {
if (readBtn) { if (readBtn) {
readBtn.click(); readBtn.click();
} else { } else {
// Try context menu approach
triggerContextMenuAction(/mark as read/i); triggerContextMenuAction(/mark as read/i);
} }
}); });
@@ -203,11 +243,8 @@ window.OutlookRelook.Keyboard = (function () {
} }
function actionMove(targets) { function actionMove(targets) {
// For move, we just need to trigger OWA's move dialog on the current selection
if (targets.length > 0) { if (targets.length > 0) {
// Click the first target to ensure something is selected
targets[0].click(); targets[0].click();
setTimeout(function () { setTimeout(function () {
var moveBtn = document.querySelector( var moveBtn = document.querySelector(
'[aria-label*="Move to" i][role="button"], [aria-label*="Move" i][role="menuitem"]' '[aria-label*="Move to" i][role="button"], [aria-label*="Move" i][role="menuitem"]'
@@ -222,19 +259,14 @@ window.OutlookRelook.Keyboard = (function () {
} }
function triggerContextMenuAction(pattern) { function triggerContextMenuAction(pattern) {
// Open context menu on the focused/selected element
var focused = document.querySelector('.or-kb-focused'); var focused = document.querySelector('.or-kb-focused');
if (!focused) return; if (!focused) return;
var rect = focused.getBoundingClientRect(); var rect = focused.getBoundingClientRect();
var contextEvent = new MouseEvent('contextmenu', { var contextEvent = new MouseEvent('contextmenu', {
bubbles: true, bubbles: true, cancelable: true,
cancelable: true, clientX: rect.x + 10, clientY: rect.y + 10,
clientX: rect.x + 10,
clientY: rect.y + 10,
}); });
focused.dispatchEvent(contextEvent); focused.dispatchEvent(contextEvent);
setTimeout(function () { setTimeout(function () {
var menuItems = document.querySelectorAll('[role="menuitem"]'); var menuItems = document.querySelectorAll('[role="menuitem"]');
for (var j = 0; j < menuItems.length; j++) { for (var j = 0; j < menuItems.length; j++) {
@@ -243,7 +275,6 @@ window.OutlookRelook.Keyboard = (function () {
return; return;
} }
} }
// Close context menu if not found
document.body.click(); document.body.click();
}, 300); }, 300);
} }
@@ -251,7 +282,6 @@ window.OutlookRelook.Keyboard = (function () {
// --- Key handler --- // --- Key handler ---
function handleKeydown(e) { function handleKeydown(e) {
// Skip if keyboard mode is off or compose/dialog is active
if (!currentSettings.keyboardMultiSelect) return; if (!currentSettings.keyboardMultiSelect) return;
if (isComposeOrDialogActive()) return; if (isComposeOrDialogActive()) return;
@@ -262,15 +292,21 @@ window.OutlookRelook.Keyboard = (function () {
var shift = e.shiftKey; var shift = e.shiftKey;
// Initialize focus if not set // Initialize focus if not set
if (focusedIndex < 0 || focusedIndex >= items.length) { var currentIdx = getFocusedIndex(items);
// Find the currently OWA-selected item, or start at 0 if (currentIdx < 0) {
for (var f = 0; f < items.length; f++) { for (var f = 0; f < items.length; f++) {
if (items[f].getAttribute('aria-selected') === 'true') { if (items[f].getAttribute('aria-selected') === 'true' ||
focusedIndex = f; items[f].classList.contains('is-selected')) {
currentIdx = f;
focusedId = getMessageId(items[f]);
break; break;
} }
} }
if (focusedIndex < 0) focusedIndex = 0; if (currentIdx < 0) {
currentIdx = 0;
focusedId = getMessageId(items[0]);
}
applyVisualState(items);
} }
var handled = true; var handled = true;
@@ -280,19 +316,14 @@ window.OutlookRelook.Keyboard = (function () {
case 'j': case 'j':
case 'ArrowDown': case 'ArrowDown':
if (shift) { if (shift) {
// Select current and move down toggleSelect(items, currentIdx);
if (focusedIndex >= 0 && focusedIndex < items.length) { if (currentIdx < items.length - 1) {
if (!selectedSet.has(items[focusedIndex])) { setFocus(items, currentIdx + 1);
toggleSelect(items[focusedIndex]); toggleSelect(items, currentIdx + 1);
}
}
if (focusedIndex < items.length - 1) {
setFocus(items, focusedIndex + 1);
toggleSelect(items[focusedIndex]);
} }
} else { } else {
if (focusedIndex < items.length - 1) { if (currentIdx < items.length - 1) {
setFocus(items, focusedIndex + 1); setFocus(items, currentIdx + 1);
} }
} }
break; break;
@@ -300,46 +331,35 @@ window.OutlookRelook.Keyboard = (function () {
case 'k': case 'k':
case 'ArrowUp': case 'ArrowUp':
if (shift) { if (shift) {
// Select current and move up toggleSelect(items, currentIdx);
if (focusedIndex >= 0 && focusedIndex < items.length) { if (currentIdx > 0) {
if (!selectedSet.has(items[focusedIndex])) { setFocus(items, currentIdx - 1);
toggleSelect(items[focusedIndex]); toggleSelect(items, currentIdx - 1);
}
}
if (focusedIndex > 0) {
setFocus(items, focusedIndex - 1);
toggleSelect(items[focusedIndex]);
} }
} else { } else {
if (focusedIndex > 0) { if (currentIdx > 0) {
setFocus(items, focusedIndex - 1); setFocus(items, currentIdx - 1);
} }
} }
break; break;
case 'x': case 'x':
case ' ': case ' ':
// Toggle select on focused message e.preventDefault();
e.preventDefault(); // Prevent page scroll on Space toggleSelect(items, currentIdx);
if (focusedIndex >= 0 && focusedIndex < items.length) {
toggleSelect(items[focusedIndex]);
}
break; break;
case '#': case '#':
// Delete selected/focused messages
targets = getActionTargets(items); targets = getActionTargets(items);
actionDelete(targets); actionDelete(targets);
break; break;
case 'e': case 'e':
// Archive selected/focused messages
targets = getActionTargets(items); targets = getActionTargets(items);
actionArchive(targets); actionArchive(targets);
break; break;
case 'I': case 'I':
// Shift+i — Mark as read
if (shift) { if (shift) {
targets = getActionTargets(items); targets = getActionTargets(items);
actionMarkRead(targets); actionMarkRead(targets);
@@ -349,7 +369,6 @@ window.OutlookRelook.Keyboard = (function () {
break; break;
case 'U': case 'U':
// Shift+u — Mark as unread
if (shift) { if (shift) {
targets = getActionTargets(items); targets = getActionTargets(items);
actionMarkUnread(targets); actionMarkUnread(targets);
@@ -359,21 +378,18 @@ window.OutlookRelook.Keyboard = (function () {
break; break;
case 'v': case 'v':
// Move selected messages
targets = getActionTargets(items); targets = getActionTargets(items);
actionMove(targets); actionMove(targets);
break; break;
case 'Escape': case 'Escape':
// Deselect all
clearSelection(); clearSelection();
break; break;
case 'Enter': case 'Enter':
case 'o': case 'o':
// Open focused message in reading pane if (currentIdx >= 0 && currentIdx < items.length) {
if (focusedIndex >= 0 && focusedIndex < items.length) { items[currentIdx].click();
items[focusedIndex].click();
} }
break; break;
@@ -383,33 +399,32 @@ window.OutlookRelook.Keyboard = (function () {
if (handled) { if (handled) {
e.stopPropagation(); e.stopPropagation();
// Only preventDefault for keys we handle (except Enter which OWA should also process)
if (key !== 'Enter' && key !== 'o') { if (key !== 'Enter' && key !== 'o') {
e.preventDefault(); e.preventDefault();
} }
} }
} }
// --- Cleanup stale selections when message list re-renders --- // --- Re-apply visual state when OWA re-renders the message list ---
function setupListObserver() { function setupListObserver() {
var listObserver = new MutationObserver(function () { var listObserver = new MutationObserver(function () {
// Remove stale entries from selectedSet (elements no longer in DOM) // Prune IDs that no longer exist in DOM
selectedSet.forEach(function (item) { var items = getMessageItems();
if (!document.contains(item)) { var currentIds = new Set();
selectedSet.delete(item); for (var i = 0; i < items.length; i++) {
} var id = getMessageId(items[i]);
}); if (id) currentIds.add(id);
updateCountBadge();
// Reset focusedIndex if the focused item is gone
var focusedEl = document.querySelector('.or-kb-focused');
if (!focusedEl || !document.contains(focusedEl)) {
focusedIndex = -1;
} }
selectedIds.forEach(function (id) {
if (!currentIds.has(id)) selectedIds.delete(id);
});
if (focusedId && !currentIds.has(focusedId)) focusedId = null;
// Re-apply classes to new DOM elements
applyVisualState(items);
}); });
// Watch the message list container for child changes
var checkInterval = setInterval(function () { var checkInterval = setInterval(function () {
var list = document.querySelector('[role="listbox"], [role="list"]'); var list = document.querySelector('[role="listbox"], [role="list"]');
if (list) { if (list) {
@@ -444,7 +459,6 @@ window.OutlookRelook.Keyboard = (function () {
var isEnabled = settings.keyboardMultiSelect; var isEnabled = settings.keyboardMultiSelect;
currentSettings = settings; currentSettings = settings;
// Only tear down and restart if the keyboard setting itself changed
if (wasEnabled !== isEnabled) { if (wasEnabled !== isEnabled) {
stop(); stop();
start(settings); start(settings);
@@ -457,11 +471,10 @@ window.OutlookRelook.Keyboard = (function () {
} }
cleanupFns = []; cleanupFns = [];
// Clean up DOM state
clearSelection(); clearSelection();
var focused = document.querySelector('.or-kb-focused'); var focused = document.querySelector('.or-kb-focused');
if (focused) focused.classList.remove('or-kb-focused'); if (focused) focused.classList.remove('or-kb-focused');
focusedIndex = -1; focusedId = null;
if (countBadge) { if (countBadge) {
countBadge.remove(); countBadge.remove();

View File

@@ -20,6 +20,7 @@
<select id="theme" data-setting="theme"> <select id="theme" data-setting="theme">
<option value="swiss">Swiss</option> <option value="swiss">Swiss</option>
<option value="material">Material</option> <option value="material">Material</option>
<option value="brutalist">Brutalist</option>
</select> </select>
</div> </div>
<div class="or-radio-group" data-setting="colorScheme"> <div class="or-radio-group" data-setting="colorScheme">

253
themes/brutalist.css Normal file
View File

@@ -0,0 +1,253 @@
/*
* Outlook Relook — Brutalist Theme
* Stark minimalism. Monospace. Borders only, almost no fills.
* High contrast. Maximum information density. No comfort.
* Inspired by terminal UIs, tax forms, and concrete architecture.
*/
/* ============================================================
LIGHT VARIANT
============================================================ */
html[data-outlook-relook-scheme="light"] {
--or-bg-primary: #ffffff;
--or-bg-secondary: #ffffff;
--or-bg-tertiary: #ffffff;
--or-bg-hover: transparent;
--or-bg-selected: transparent;
--or-text-primary: #000000;
--or-text-secondary: #000000;
--or-text-tertiary: #555555;
--or-text-disabled: #999999;
--or-border: #000000;
--or-border-light: #000000;
--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: #000000;
--or-bg-secondary: #000000;
--or-bg-tertiary: #000000;
--or-bg-hover: transparent;
--or-bg-selected: transparent;
--or-text-primary: #ffffff;
--or-text-secondary: #ffffff;
--or-text-tertiary: #aaaaaa;
--or-text-disabled: #555555;
--or-border: #ffffff;
--or-border-light: #ffffff;
--or-accent: var(--or-accent-override, #ffffff);
--or-accent-hover: var(--or-accent-override, #cccccc);
--or-shadow: none;
}
/* ============================================================
TYPOGRAPHY — Monospace, tight, utilitarian
============================================================ */
html[data-outlook-relook-scheme] *,
html[data-outlook-relook-scheme] *::before,
html[data-outlook-relook-scheme] *::after {
font-family: 'SF Mono', 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', monospace !important;
letter-spacing: -0.03em !important;
-webkit-font-smoothing: none !important;
font-smooth: never !important;
}
html[data-outlook-relook-scheme] body {
font-size: 12px !important;
}
/* ============================================================
STRIP EVERYTHING — more aggressive than Swiss
============================================================ */
html[data-outlook-relook-scheme] * {
border-radius: 0 !important;
text-shadow: none !important;
box-shadow: none !important;
background-image: none !important;
transition: none !important;
animation: none !important;
}
/* ============================================================
SURFACES — pure white/black, no distinction between panes
============================================================ */
html[data-outlook-relook-scheme] body {
background-color: var(--or-bg-primary) !important;
color: var(--or-text-primary) !important;
}
/* Top bar — just a bottom border */
html[data-outlook-relook-scheme] [role="banner"],
html[data-outlook-relook-scheme] header {
background-color: var(--or-bg-primary) !important;
border-bottom: 2px solid var(--or-border) !important;
}
/* Folder pane — border only, no background change */
html[data-outlook-relook-scheme] [role="navigation"],
html[data-outlook-relook-scheme] [role="complementary"] {
background-color: var(--or-bg-primary) !important;
border-right: 2px solid var(--or-border) !important;
}
/* Message list — no background */
html[data-outlook-relook-scheme] [role="listbox"],
html[data-outlook-relook-scheme] [role="list"] {
background-color: var(--or-bg-primary) !important;
}
/* Message list items — borders only, no fills */
html[data-outlook-relook-scheme] [role="option"],
html[data-outlook-relook-scheme] [role="listitem"] {
background-color: transparent !important;
border-bottom: 1px solid var(--or-border) !important;
color: var(--or-text-primary) !important;
padding: 3px 8px !important;
}
/* Hover: invert colors instead of fill */
html[data-outlook-relook-scheme="light"] [role="option"]:hover,
html[data-outlook-relook-scheme="light"] [role="listitem"]:hover {
background-color: #000000 !important;
color: #ffffff !important;
}
html[data-outlook-relook-scheme="dark"] [role="option"]:hover,
html[data-outlook-relook-scheme="dark"] [role="listitem"]:hover {
background-color: #ffffff !important;
color: #000000 !important;
}
/* Selected: invert */
html[data-outlook-relook-scheme="light"] [role="option"][aria-selected="true"],
html[data-outlook-relook-scheme="light"] [role="listitem"][aria-selected="true"] {
background-color: #000000 !important;
color: #ffffff !important;
}
html[data-outlook-relook-scheme="dark"] [role="option"][aria-selected="true"],
html[data-outlook-relook-scheme="dark"] [role="listitem"][aria-selected="true"] {
background-color: #ffffff !important;
color: #000000 !important;
}
/* Reading pane */
html[data-outlook-relook-scheme] [role="main"] {
background-color: var(--or-bg-primary) !important;
}
/* ============================================================
TOOLBAR — stark, no visual weight
============================================================ */
html[data-outlook-relook-scheme] [role="toolbar"] {
background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border) !important;
}
html[data-outlook-relook-scheme] [role="toolbar"] button,
html[data-outlook-relook-scheme] [role="toolbar"] [role="button"] {
background-color: transparent !important;
border: 1px solid transparent !important;
color: var(--or-text-primary) !important;
text-transform: uppercase !important;
font-size: 10px !important;
letter-spacing: 0.05em !important;
}
html[data-outlook-relook-scheme] [role="toolbar"] button:hover,
html[data-outlook-relook-scheme] [role="toolbar"] [role="button"]:hover {
border-color: var(--or-border) !important;
background-color: transparent !important;
}
/* ============================================================
BUTTONS — outlined only, never filled
============================================================ */
html[data-outlook-relook-scheme] button,
html[data-outlook-relook-scheme] [role="button"] {
background-color: transparent !important;
color: var(--or-text-primary) !important;
border: 1px solid transparent !important;
}
html[data-outlook-relook-scheme] button:hover,
html[data-outlook-relook-scheme] [role="button"]:hover {
border-color: var(--or-border) !important;
}
/* Links: same color as text, underlined */
html[data-outlook-relook-scheme] a {
color: var(--or-text-primary) !important;
text-decoration: underline !important;
}
html[data-outlook-relook-scheme] a:hover {
text-decoration: none !important;
}
/* ============================================================
FOLDER PANE — all caps, no fills
============================================================ */
html[data-outlook-relook-scheme] [role="treeitem"] {
color: var(--or-text-primary) !important;
background-color: transparent !important;
text-transform: uppercase !important;
font-size: 10px !important;
letter-spacing: 0.06em !important;
border-bottom: 1px solid var(--or-border-light) !important;
}
html[data-outlook-relook-scheme] [role="treeitem"]:hover {
text-decoration: underline !important;
background-color: transparent !important;
}
html[data-outlook-relook-scheme] [role="treeitem"][aria-selected="true"] {
font-weight: 900 !important;
background-color: transparent !important;
}
/* ============================================================
SECONDARY TEXT
============================================================ */
html[data-outlook-relook-scheme] [role="option"] span,
html[data-outlook-relook-scheme] [role="listitem"] span {
color: var(--or-text-tertiary) !important;
font-size: 11px !important;
}
html[data-outlook-relook-scheme] time {
color: var(--or-text-tertiary) !important;
font-size: 10px !important;
text-transform: uppercase !important;
}
/* ============================================================
DIALOGS — bordered box, no fill
============================================================ */
html[data-outlook-relook-scheme] [role="dialog"],
html[data-outlook-relook-scheme] [role="alertdialog"] {
background-color: var(--or-bg-primary) !important;
border: 2px solid var(--or-border) !important;
}
/* ============================================================
SCROLLBARS — hairline
============================================================ */
html[data-outlook-relook-scheme] ::-webkit-scrollbar {
width: 2px !important;
height: 2px !important;
}
html[data-outlook-relook-scheme] ::-webkit-scrollbar-track {
background: transparent !important;
}
html[data-outlook-relook-scheme] ::-webkit-scrollbar-thumb {
background: var(--or-text-primary) !important;
}

View File

@@ -1,6 +1,8 @@
/* /*
* Outlook Relook — Swiss / Helvetica Theme * Outlook Relook — Swiss / Helvetica Theme
* Monochrome, tight grid, no decoration. Content density is king. * Ruthlessly minimal. Black, white, grey, accent. Nothing else.
* Helvetica Neue everywhere. Zero decoration. Grid discipline.
* If an element doesn't serve the content, it should be invisible.
*/ */
/* ============================================================ /* ============================================================
@@ -8,18 +10,18 @@
============================================================ */ ============================================================ */
html[data-outlook-relook-scheme="light"] { html[data-outlook-relook-scheme="light"] {
--or-bg-primary: #ffffff; --or-bg-primary: #ffffff;
--or-bg-secondary: #fafafa; --or-bg-secondary: #ffffff;
--or-bg-tertiary: #f0f0f0; --or-bg-tertiary: #f7f7f7;
--or-bg-hover: #e8e8e8; --or-bg-hover: #f0f0f0;
--or-bg-selected: #e0e0e0; --or-bg-selected: #f0f0f0;
--or-text-primary: #000000; --or-text-primary: #000000;
--or-text-secondary: #333333; --or-text-secondary: #555555;
--or-text-tertiary: #666666; --or-text-tertiary: #888888;
--or-text-disabled: #999999; --or-text-disabled: #bbbbbb;
--or-border: #d0d0d0; --or-border: #000000;
--or-border-light: #e5e5e5; --or-border-light: #e0e0e0;
--or-accent: var(--or-accent-override, #000000); --or-accent: var(--or-accent-override, #ff0000);
--or-accent-hover: var(--or-accent-override, #333333); --or-accent-hover: var(--or-accent-override, #cc0000);
--or-shadow: none; --or-shadow: none;
} }
@@ -27,71 +29,86 @@ html[data-outlook-relook-scheme="light"] {
DARK VARIANT DARK VARIANT
============================================================ */ ============================================================ */
html[data-outlook-relook-scheme="dark"] { html[data-outlook-relook-scheme="dark"] {
--or-bg-primary: #1a1a1a; --or-bg-primary: #0a0a0a;
--or-bg-secondary: #222222; --or-bg-secondary: #0a0a0a;
--or-bg-tertiary: #2a2a2a; --or-bg-tertiary: #141414;
--or-bg-hover: #333333; --or-bg-hover: #1a1a1a;
--or-bg-selected: #3a3a3a; --or-bg-selected: #1a1a1a;
--or-text-primary: #e8e8e8; --or-text-primary: #ffffff;
--or-text-secondary: #cccccc; --or-text-secondary: #aaaaaa;
--or-text-tertiary: #999999; --or-text-tertiary: #666666;
--or-text-disabled: #666666; --or-text-disabled: #444444;
--or-border: #3a3a3a; --or-border: #ffffff;
--or-border-light: #2f2f2f; --or-border-light: #222222;
--or-accent: var(--or-accent-override, #e8e8e8); --or-accent: var(--or-accent-override, #ff3333);
--or-accent-hover: var(--or-accent-override, #ffffff); --or-accent-hover: var(--or-accent-override, #ff6666);
--or-shadow: none; --or-shadow: none;
} }
/* ============================================================ /* ============================================================
TYPOGRAPHY TYPOGRAPHY — Helvetica Neue, aggressively applied
============================================================ */ ============================================================ */
html[data-outlook-relook-scheme] body, html[data-outlook-relook-scheme] *,
html[data-outlook-relook-scheme] [role="main"], html[data-outlook-relook-scheme] *::before,
html[data-outlook-relook-scheme] [role="navigation"], html[data-outlook-relook-scheme] *::after {
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; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif !important;
letter-spacing: -0.01em !important; letter-spacing: -0.02em !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
} }
/* ============================================================ /* ============================================================
SURFACES & BACKGROUNDS STRIP ALL DECORATION — the core Swiss principle
============================================================ */
html[data-outlook-relook-scheme] * {
border-radius: 0 !important;
text-shadow: none !important;
box-shadow: none !important;
background-image: none !important;
text-decoration-color: var(--or-text-primary) !important;
outline-color: var(--or-accent) !important;
}
/* Remove ALL gradients, transitions, and transforms from non-essential elements */
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"],
html[data-outlook-relook-scheme] [role="option"],
html[data-outlook-relook-scheme] [role="listitem"],
html[data-outlook-relook-scheme] [role="treeitem"] {
transition: none !important;
animation: none !important;
}
/* ============================================================
SURFACES — flatten everything to white (or black in dark)
============================================================ */ ============================================================ */
html[data-outlook-relook-scheme] body { html[data-outlook-relook-scheme] body {
background-color: var(--or-bg-primary) !important; background-color: var(--or-bg-primary) !important;
color: var(--or-text-primary) !important; color: var(--or-text-primary) !important;
} }
/* Top bar */
html[data-outlook-relook-scheme] [role="banner"], html[data-outlook-relook-scheme] [role="banner"],
html[data-outlook-relook-scheme] header { html[data-outlook-relook-scheme] header {
background-color: var(--or-bg-primary) !important; background-color: var(--or-bg-primary) !important;
border-bottom: 1px solid var(--or-border) !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="navigation"],
html[data-outlook-relook-scheme] [role="complementary"] { html[data-outlook-relook-scheme] [role="complementary"] {
background-color: var(--or-bg-secondary) !important; background-color: var(--or-bg-primary) !important;
border-right: 1px solid var(--or-border-light) !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="listbox"],
html[data-outlook-relook-scheme] [role="list"] { html[data-outlook-relook-scheme] [role="list"] {
background-color: var(--or-bg-primary) !important; background-color: var(--or-bg-primary) !important;
} }
/* Message list items */
html[data-outlook-relook-scheme] [role="option"], html[data-outlook-relook-scheme] [role="option"],
html[data-outlook-relook-scheme] [role="listitem"] { html[data-outlook-relook-scheme] [role="listitem"] {
background-color: var(--or-bg-primary) !important; background-color: transparent !important;
border-bottom: 1px solid var(--or-border-light) !important; border-bottom: 1px solid var(--or-border-light) !important;
color: var(--or-text-primary) !important; color: var(--or-text-primary) !important;
} }
@@ -104,74 +121,81 @@ html[data-outlook-relook-scheme] [role="listitem"]:hover {
html[data-outlook-relook-scheme] [role="option"][aria-selected="true"], html[data-outlook-relook-scheme] [role="option"][aria-selected="true"],
html[data-outlook-relook-scheme] [role="listitem"][aria-selected="true"] { html[data-outlook-relook-scheme] [role="listitem"][aria-selected="true"] {
background-color: var(--or-bg-selected) !important; background-color: var(--or-bg-selected) !important;
border-left: 3px solid var(--or-accent) !important;
} }
/* Reading pane */
html[data-outlook-relook-scheme] [role="main"] { html[data-outlook-relook-scheme] [role="main"] {
background-color: var(--or-bg-primary) !important; background-color: var(--or-bg-primary) !important;
} }
/* ============================================================ /* ============================================================
DECORATIVE REMOVAL TOOLBAR — minimal, no background distinction
============================================================ */ ============================================================ */
html[data-outlook-relook-scheme] * { html[data-outlook-relook-scheme] [role="toolbar"] {
border-radius: 0 !important; background-color: var(--or-bg-primary) !important;
text-shadow: none !important; border-bottom: 1px solid var(--or-border-light) !important;
} }
html[data-outlook-relook-scheme] button, /* Toolbar buttons: text only, no background */
html[data-outlook-relook-scheme] [role="button"], html[data-outlook-relook-scheme] [role="toolbar"] button,
html[data-outlook-relook-scheme] [role="tab"], html[data-outlook-relook-scheme] [role="toolbar"] [role="button"] {
html[data-outlook-relook-scheme] [role="menuitem"] { background-color: transparent !important;
box-shadow: none !important; border: none !important;
background-image: none !important; color: var(--or-text-primary) !important;
}
html[data-outlook-relook-scheme] [role="toolbar"] button:hover,
html[data-outlook-relook-scheme] [role="toolbar"] [role="button"]:hover {
background-color: var(--or-bg-hover) !important;
} }
/* ============================================================ /* ============================================================
BUTTONS & INTERACTIVE BUTTONS — understate everything
============================================================ */ ============================================================ */
html[data-outlook-relook-scheme] button,
html[data-outlook-relook-scheme] [role="button"] {
background-color: transparent !important;
color: var(--or-text-primary) !important;
}
html[data-outlook-relook-scheme] button:hover, html[data-outlook-relook-scheme] button:hover,
html[data-outlook-relook-scheme] [role="button"]:hover { html[data-outlook-relook-scheme] [role="button"]:hover {
background-color: var(--or-bg-hover) !important; background-color: var(--or-bg-hover) !important;
} }
/* Links: accent color, no underline by default */
html[data-outlook-relook-scheme] a { html[data-outlook-relook-scheme] a {
color: var(--or-accent) !important; color: var(--or-accent) !important;
text-decoration: none !important;
} }
html[data-outlook-relook-scheme] a:hover { html[data-outlook-relook-scheme] a:hover {
text-decoration: underline !important;
color: var(--or-accent-hover) !important; color: var(--or-accent-hover) !important;
} }
/* ============================================================ /* ============================================================
TOOLBAR / COMMAND BAR FOLDER PANE — text hierarchy only, no visual chrome
============================================================ */
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"] { html[data-outlook-relook-scheme] [role="treeitem"] {
color: var(--or-text-secondary) !important; color: var(--or-text-secondary) !important;
background-color: transparent !important;
} }
html[data-outlook-relook-scheme] [role="treeitem"]:hover { html[data-outlook-relook-scheme] [role="treeitem"]:hover {
background-color: var(--or-bg-hover) !important;
color: var(--or-text-primary) !important; color: var(--or-text-primary) !important;
background-color: transparent !important;
text-decoration: underline !important;
} }
html[data-outlook-relook-scheme] [role="treeitem"][aria-selected="true"] { html[data-outlook-relook-scheme] [role="treeitem"][aria-selected="true"] {
background-color: var(--or-bg-selected) !important;
color: var(--or-text-primary) !important; color: var(--or-text-primary) !important;
font-weight: 600 !important; font-weight: 700 !important;
background-color: transparent !important;
} }
/* ============================================================ /* ============================================================
SECONDARY TEXT SECONDARY TEXT — clear hierarchy
============================================================ */ ============================================================ */
html[data-outlook-relook-scheme] [role="option"] span, html[data-outlook-relook-scheme] [role="option"] span,
html[data-outlook-relook-scheme] [role="listitem"] span { html[data-outlook-relook-scheme] [role="listitem"] span {
@@ -180,4 +204,32 @@ html[data-outlook-relook-scheme] [role="listitem"] span {
html[data-outlook-relook-scheme] time { html[data-outlook-relook-scheme] time {
color: var(--or-text-tertiary) !important; color: var(--or-text-tertiary) !important;
font-size: 11px !important;
text-transform: uppercase !important;
letter-spacing: 0.03em !important;
}
/* ============================================================
DIALOGS & OVERLAYS
============================================================ */
html[data-outlook-relook-scheme] [role="dialog"],
html[data-outlook-relook-scheme] [role="alertdialog"] {
background-color: var(--or-bg-primary) !important;
border: 1px solid var(--or-border) !important;
}
/* ============================================================
SCROLLBARS — thin, monochrome
============================================================ */
html[data-outlook-relook-scheme] ::-webkit-scrollbar {
width: 4px !important;
height: 4px !important;
}
html[data-outlook-relook-scheme] ::-webkit-scrollbar-track {
background: transparent !important;
}
html[data-outlook-relook-scheme] ::-webkit-scrollbar-thumb {
background: var(--or-text-tertiary) !important;
} }