Skip to content

Simplify extension controls and add options page#19

Open
ArsalaBangash wants to merge 2 commits intomainfrom
ArsalaBangash/simplify-popup-settings
Open

Simplify extension controls and add options page#19
ArsalaBangash wants to merge 2 commits intomainfrom
ArsalaBangash/simplify-popup-settings

Conversation

@ArsalaBangash
Copy link
Member

@ArsalaBangash ArsalaBangash commented Mar 9, 2026

This simplifies the extension into a more opinionated two-state experience and removes most popup customization and storage plumbing. It replaces the popup with a minimal status surface, adds an MV3 options page entry point, and updates the build and test setup to bundle that page. It removes per-site toggles, custom focus mode, quote display and text-size settings, and the old website config code while keeping the storage-based focus state flow intact. It also makes LinkedIn and X keep side distractions hidden in unfocused mode by default, including the LinkedIn layout-preserving panel concealment fix and regression tests.

Summary by CodeRabbit

  • New Features

    • Added support for X alongside LinkedIn and YouTube.
    • Added a dedicated Settings (Options) page accessible in a tab.
  • Changes

    • Popup UI simplified to a compact status view with shortcut and "Open Settings" action.
    • Focus modes reduced to Focused and Unfocused; per-site toggle controls removed.
    • Installation note: unpacked folder renamed to extension-build.
  • Tests

    • Added unit tests for LinkedIn, X, and the options scaffold.

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

Adds an options/settings page and X site support, simplifies popup to a compact status view, reduces focus modes from three to two (removes CustomFocus), removes several storage-driven behaviors, and refactors website/controller logic (LinkedIn/X controllers enhanced for panel/sidebar concealment).

Changes

Cohort / File(s) Summary
Docs & Manifest
README.md, src/manifest.json
Docs updated to list X and describe Focused/Unfocused modes; manifest gains options_uioptions/options.html with open_in_tab: true.
Build & Options UI
build.js, src/options/options.html, src/options/options.css, src/options/options.ts, src/options/options.html.d.ts, src/options/options.test.ts
New esbuild entry options; copies options assets into extension-build/options; adds static options HTML/CSS scaffold, typings, placeholder TS module, and tests asserting render.
Popup UI
src/popup/popup.html, src/popup/popup.css, src/popup/popup.ts, src/popup/popup.test.ts
Popup simplified to a compact status/shortcut view; removed per-site toggles and complex controls; added "Open Settings" action that invokes openOptionsPage; UI/stylesheet greatly reduced.
Background & Mocks
src/ts/background.ts, src/ts/__mocks__/webextension-polyfill-ts.ts
Background stops reading/writing showQuote/textSize and removes activeURL tracking; test mock adds browser.runtime.openOptionsPage.
Focus Core
src/ts/focus/types.ts, src/ts/focus/focus.ts
Removed CustomFocus from FocusMode; initialize/detection flow simplified, removed per-website toggles, reload handling, and many storage listeners; detectAndCreateController() signature simplified.
Quote/Text-size
src/ts/quotes/index.ts, src/ts/quotes/text-size.ts, src/ts/quotes/quotes.test.ts
Removed dynamic text-size API (SIZE_MAP, SizeKey) and updateQuoteTextSize; createQuoteElement is synchronous with hard-coded font sizes; tests adjusted accordingly.
Website config & Base Controller
src/ts/websites/website-config.ts, src/ts/websites/website-controller.ts
Deleted website-config.ts (URL predicates and toggle interfaces); removed storage-listener constructor and customFocus path from base WebsiteController; quote injection simplified.
LinkedIn Controller
src/ts/websites/linkedin/linkedin-controller.ts, src/ts/websites/linkedin/linkedin-controller.test.ts
Added concealedPanels map and helpers (concealPanel, restoreConcealedPanels, hidePanels) to preserve/restore inline styles when hiding panels; test verifies concealment behavior.
X Controller
src/ts/websites/x/x-controller.ts, src/ts/websites/x/x-controller.test.ts
Replaced customFocus() with unfocus() that hides sidebar and ensures feed visibility; test verifies sidebar hiding post-unfocus.
Static Report
reports/selector-health/selector-health-report.json
Updated generatedAt timestamp only.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User as "User (Popup click)"
participant Popup as "Popup UI (popup.ts)"
participant Browser as "Browser API (runtime)"
participant Options as "Options Page (options.html)"
User->>Popup: Click "Open Settings" button
Popup->>Browser: browser.runtime.openOptionsPage()
Browser->>Options: Open options/options.html in new tab
Options-->User: Settings page displayed

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hop, I nibble code and cheer,
Two modes now, one less to fear.
Popup slim, Settings open wide,
X joins LinkedIn at my side.
Panels tucked away — a tidy stride.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: simplifying extension controls (reducing popup UI complexity, removing toggles/settings) and adding an options page (new options UI with settings placeholder).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ArsalaBangash/simplify-popup-settings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

34-34: ⚠️ Potential issue | 🟡 Minor

Incorrect output folder in installation instructions.

The build script outputs to extension-build, but the README instructs users to select the dist folder. This will cause the extension to fail to load.

📝 Proposed fix
-5. Click "Load unpacked" and select the `dist` folder
+5. Click "Load unpacked" and select the `extension-build` folder
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 34, Update the README installation step that currently
says to select the `dist` folder: change the instruction to tell users to click
"Load unpacked" and select the `extension-build` folder (the actual output of
the build script) so the extension loads correctly; locate and update the line
that reads `Click "Load unpacked" and select the \`dist\` folder` to reference
`extension-build` instead.
🧹 Nitpick comments (10)
src/ts/quotes/index.ts (1)

25-46: Consider removing the async keyword since the function no longer awaits anything.

The createQuoteElement function is declared async but contains no await calls after the removal of storage-based font size lookups. The function can be simplified to return HTMLDivElement directly.

♻️ Suggested simplification
-const createQuoteElement = async (): Promise<HTMLDivElement> => {
+const createQuoteElement = (): HTMLDivElement => {
   const quote = getRandomQuote();
   const quoteDiv = document.createElement('div');

Note: This would also require updating callers that currently await this function, and updating the corresponding test's beforeEach to be synchronous.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ts/quotes/index.ts` around lines 25 - 46, The createQuoteElement function
is marked async but does not await anything; remove the async keyword and change
its return type from Promise<HTMLDivElement> to HTMLDivElement in the function
signature (createQuoteElement) so it returns the div synchronously, then update
any callers/tests that currently use await createQuoteElement() (and the related
beforeEach) to call it synchronously and adjust tests to be non-async.
src/options/options.ts (1)

1-1: Placeholder module noted.

This empty export creates a valid ES module entry point for the build system. Consider adding a TODO comment if this is intended to be expanded with options page logic in the future.

💡 Optional: Add intent comment
+// TODO: Add options page logic (e.g., settings persistence, UI interactions)
 export {};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/options/options.ts` at line 1, This module currently only exports an
empty object (export {}), acting as a placeholder; add a short TODO/intent
comment at the top of the file indicating that this is an intentional
placeholder for future options page logic (e.g., "TODO: implement options page
logic" or "Placeholder module for options exports") so maintainers know the
empty export is deliberate.
src/ts/websites/linkedin/linkedin-controller.ts (1)

129-136: Consider extracting restoreInlineStyle to the base class or a shared utility.

This helper function could be reused by other controllers (e.g., XController) if they adopt similar concealment patterns in the future. Currently it's a module-level function, which works but limits reusability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ts/websites/linkedin/linkedin-controller.ts` around lines 129 - 136, The
module-level helper restoreInlineStyle should be moved to a shared location so
other controllers can reuse it; extract restoreInlineStyle into a shared utility
module (or into the common/base controller class used by LinkedInController and
future XController) and replace the local function with an import or base-class
method call; update LinkedInController to call the new shared function or base
method (preserving the same signature restoreInlineStyle(element: HTMLElement,
property: string, value: string): void) and adjust other controllers to reuse it
to avoid duplication.
src/ts/websites/linkedin/linkedin-controller.test.ts (1)

4-12: Mock may be incomplete for hasFeedLoaded and isFeedHidden.

The LinkedInUtils mock doesn't include hasFeedLoaded or isFeedHidden, which are referenced by LinkedInController.focus() and the feed distraction setup. While unfocus() doesn't directly call these (it calls setFeedVisibility(true) which uses showFeed()), adding these mocks would make the mock more complete and prevent issues if test coverage expands.

💡 More complete mock
 jest.mock('./linkedin-utils', () => ({
   __esModule: true,
   default: {
     isHomePage: jest.fn(),
     getLinkedInFeed: jest.fn(),
     getLinkedInPanels: jest.fn(),
     arePanelsHidden: jest.fn(),
+    hasFeedLoaded: jest.fn(),
+    isFeedHidden: jest.fn(),
   },
 }))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ts/websites/linkedin/linkedin-controller.test.ts` around lines 4 - 12,
The LinkedInUtils mock is missing hasFeedLoaded and isFeedHidden which
LinkedInController.focus() and feed visibility logic reference; update the
jest.mock for LinkedInUtils to include hasFeedLoaded: jest.fn() and
isFeedHidden: jest.fn() (add them alongside isHomePage, getLinkedInFeed,
getLinkedInPanels, arePanelsHidden) so tests can stub/override their return
values when exercising LinkedInController.focus(), unfocus(),
setFeedVisibility(true) and showFeed().
src/ts/websites/x/x-controller.test.ts (2)

20-39: Consider adding an assertion for feed visibility restoration.

The test name states "while restoring the feed" but only asserts the sidebar state. Adding an assertion for the feed element's visibility would make the test more comprehensive and match its description.

💡 Suggested improvement
     const controller = new XController()
     controller.unfocus()
 
     expect(sidebar.style.display).toBe('none')
+    // Verify feed was restored (not hidden)
+    expect(feed.style.display).not.toBe('none')
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ts/websites/x/x-controller.test.ts` around lines 20 - 39, The test
currently asserts only that the sidebar remains hidden after controller.unfocus;
add an assertion that the feed element returned by XUtils.getXFeed (the feed
variable) is restored to visible. After calling controller.unfocus() in the
test, add an assertion such as expecting feed.style.display not to be 'none' (or
to equal the expected visible value), so the test name "while restoring the
feed" matches the verification of XController restoring the feed's visibility.

24-27: Consider cleaning up document.URL modification in afterEach.

Modifying document.URL with Object.defineProperty persists across tests if not reset. This could cause flaky behavior if more tests are added.

💡 Suggested cleanup
 describe('XController', () => {
+  const originalURL = document.URL
+
   beforeEach(() => {
     jest.clearAllMocks()
   })
+
+  afterEach(() => {
+    Object.defineProperty(document, 'URL', {
+      value: originalURL,
+      writable: true,
+    })
+  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ts/websites/x/x-controller.test.ts` around lines 24 - 27, The test
mutates document.URL via Object.defineProperty in x-controller.test.ts and
doesn't restore it; update the test to capture the original value before
modifying (cache original = document.URL), apply Object.defineProperty(document,
'URL', { value: 'https://x.com/home', writable: true }) in the test setup, and
add/modify an afterEach hook to restore document.URL by re-applying
Object.defineProperty(document, 'URL', { value: original, writable: true }) (use
the existing afterEach or add one if missing) so the mutation does not leak
between tests.
src/ts/background.ts (1)

25-31: The get() result is unused; consider simplifying.

The get(['appState']) call retrieves stored data but the callback ignores it, immediately overwriting with the baseline. If the intent is simply to reset state on worker restart, set() alone suffices.

💡 Simplified initialization
 // In MV3 the worker restarts frequently. Reset appState on startup so each
 // worker begins from the same focused baseline.
-browser.storage.local.get(['appState']).then(() => {
-  browser.storage.local.set({
-    appState: appState,
-  })
+browser.storage.local.set({
+  appState: appState,
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ts/background.ts` around lines 25 - 31, The storage initialization calls
browser.storage.local.get(['appState']) but ignores its result and immediately
overwrites the value; remove the unused get call and call
browser.storage.local.set({ appState: appState }) directly so the worker simply
resets state on startup—update the code that references
browser.storage.local.get and browser.storage.local.set and leave the appState
variable usage intact.
src/options/options.html (1)

2-6: Add lang attribute and move charset meta for better practices.

Consider adding the lang attribute to the <html> element for accessibility (screen readers) and moving the charset meta to be first in <head> for proper encoding detection.

♻️ Suggested improvements
 <!DOCTYPE html>
-<html>
+<html lang="en">
   <head>
+    <meta charset="UTF-8" />
     <title>Focused Browsing Settings</title>
     <link rel="stylesheet" type="text/css" href="options.css" />
-    <meta charset="UTF-8" />
   </head>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/options/options.html` around lines 2 - 6, Update the <html> element to
include a lang attribute (e.g., lang="en") and move the <meta charset="UTF-8" />
element to be the very first child inside the <head> so encoding is detected
early; locate these changes around the existing <html> and <head> tags in
options.html and adjust accordingly.
src/popup/popup.ts (1)

7-9: Consider adding error handling for openOptionsPage.

If openOptionsPage() fails (e.g., options page not found), the rejected promise is unhandled. While rare, adding a catch or removing the unnecessary await would be cleaner.

♻️ Option 1: Remove unnecessary await
   openOptionsButton.addEventListener('click', async () => {
-    await browser.runtime.openOptionsPage();
+    browser.runtime.openOptionsPage();
   });
♻️ Option 2: Add error handling
   openOptionsButton.addEventListener('click', async () => {
-    await browser.runtime.openOptionsPage();
+    try {
+      await browser.runtime.openOptionsPage();
+    } catch (err) {
+      console.error('Failed to open options page:', err);
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/popup/popup.ts` around lines 7 - 9, The click handler for
openOptionsButton currently awaits browser.runtime.openOptionsPage() without
error handling; either remove the unnecessary await to let the promise resolve
naturally or wrap the call in a try/catch and handle failures (e.g., log the
error via console.error or show a user-friendly message). Locate the event
listener on openOptionsButton and update it to either call
browser.runtime.openOptionsPage() without await or surround the await
browser.runtime.openOptionsPage() with try { ... } catch (err) {
console.error('Failed to open options page', err); } so rejected promises are
handled.
src/popup/popup.css (1)

113-117: Reconsider removing outline on focus-visible for accessibility.

Setting outline: none on :focus-visible removes the default focus indicator for keyboard users. While the color change provides some feedback, consider adding a visible focus ring or underline for better keyboard navigation accessibility.

♿ Proposed fix to improve focus visibility
 .options-button:hover,
 .options-button:focus-visible {
   color: var(--ink);
-  outline: none;
+  outline: 2px solid var(--accent);
+  outline-offset: 2px;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/popup/popup.css` around lines 113 - 117, The CSS rule for
.options-button:focus-visible currently removes the browser focus indicator
(outline: none), harming keyboard accessibility; update the
.options-button:hover and .options-button:focus-visible rules to keep hover
styling but remove the outline:none and instead add a visible focus style for
.options-button:focus-visible (e.g., a clearly contrasting outline or focus ring
using outline or box-shadow tied to a CSS variable like --focus or --ink) so
keyboard users get a high-contrast, visible indicator while preserving the
existing color change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@README.md`:
- Line 34: Update the README installation step that currently says to select the
`dist` folder: change the instruction to tell users to click "Load unpacked" and
select the `extension-build` folder (the actual output of the build script) so
the extension loads correctly; locate and update the line that reads `Click
"Load unpacked" and select the \`dist\` folder` to reference `extension-build`
instead.

---

Nitpick comments:
In `@src/options/options.html`:
- Around line 2-6: Update the <html> element to include a lang attribute (e.g.,
lang="en") and move the <meta charset="UTF-8" /> element to be the very first
child inside the <head> so encoding is detected early; locate these changes
around the existing <html> and <head> tags in options.html and adjust
accordingly.

In `@src/options/options.ts`:
- Line 1: This module currently only exports an empty object (export {}), acting
as a placeholder; add a short TODO/intent comment at the top of the file
indicating that this is an intentional placeholder for future options page logic
(e.g., "TODO: implement options page logic" or "Placeholder module for options
exports") so maintainers know the empty export is deliberate.

In `@src/popup/popup.css`:
- Around line 113-117: The CSS rule for .options-button:focus-visible currently
removes the browser focus indicator (outline: none), harming keyboard
accessibility; update the .options-button:hover and
.options-button:focus-visible rules to keep hover styling but remove the
outline:none and instead add a visible focus style for
.options-button:focus-visible (e.g., a clearly contrasting outline or focus ring
using outline or box-shadow tied to a CSS variable like --focus or --ink) so
keyboard users get a high-contrast, visible indicator while preserving the
existing color change.

In `@src/popup/popup.ts`:
- Around line 7-9: The click handler for openOptionsButton currently awaits
browser.runtime.openOptionsPage() without error handling; either remove the
unnecessary await to let the promise resolve naturally or wrap the call in a
try/catch and handle failures (e.g., log the error via console.error or show a
user-friendly message). Locate the event listener on openOptionsButton and
update it to either call browser.runtime.openOptionsPage() without await or
surround the await browser.runtime.openOptionsPage() with try { ... } catch
(err) { console.error('Failed to open options page', err); } so rejected
promises are handled.

In `@src/ts/background.ts`:
- Around line 25-31: The storage initialization calls
browser.storage.local.get(['appState']) but ignores its result and immediately
overwrites the value; remove the unused get call and call
browser.storage.local.set({ appState: appState }) directly so the worker simply
resets state on startup—update the code that references
browser.storage.local.get and browser.storage.local.set and leave the appState
variable usage intact.

In `@src/ts/quotes/index.ts`:
- Around line 25-46: The createQuoteElement function is marked async but does
not await anything; remove the async keyword and change its return type from
Promise<HTMLDivElement> to HTMLDivElement in the function signature
(createQuoteElement) so it returns the div synchronously, then update any
callers/tests that currently use await createQuoteElement() (and the related
beforeEach) to call it synchronously and adjust tests to be non-async.

In `@src/ts/websites/linkedin/linkedin-controller.test.ts`:
- Around line 4-12: The LinkedInUtils mock is missing hasFeedLoaded and
isFeedHidden which LinkedInController.focus() and feed visibility logic
reference; update the jest.mock for LinkedInUtils to include hasFeedLoaded:
jest.fn() and isFeedHidden: jest.fn() (add them alongside isHomePage,
getLinkedInFeed, getLinkedInPanels, arePanelsHidden) so tests can stub/override
their return values when exercising LinkedInController.focus(), unfocus(),
setFeedVisibility(true) and showFeed().

In `@src/ts/websites/linkedin/linkedin-controller.ts`:
- Around line 129-136: The module-level helper restoreInlineStyle should be
moved to a shared location so other controllers can reuse it; extract
restoreInlineStyle into a shared utility module (or into the common/base
controller class used by LinkedInController and future XController) and replace
the local function with an import or base-class method call; update
LinkedInController to call the new shared function or base method (preserving
the same signature restoreInlineStyle(element: HTMLElement, property: string,
value: string): void) and adjust other controllers to reuse it to avoid
duplication.

In `@src/ts/websites/x/x-controller.test.ts`:
- Around line 20-39: The test currently asserts only that the sidebar remains
hidden after controller.unfocus; add an assertion that the feed element returned
by XUtils.getXFeed (the feed variable) is restored to visible. After calling
controller.unfocus() in the test, add an assertion such as expecting
feed.style.display not to be 'none' (or to equal the expected visible value), so
the test name "while restoring the feed" matches the verification of XController
restoring the feed's visibility.
- Around line 24-27: The test mutates document.URL via Object.defineProperty in
x-controller.test.ts and doesn't restore it; update the test to capture the
original value before modifying (cache original = document.URL), apply
Object.defineProperty(document, 'URL', { value: 'https://x.com/home', writable:
true }) in the test setup, and add/modify an afterEach hook to restore
document.URL by re-applying Object.defineProperty(document, 'URL', { value:
original, writable: true }) (use the existing afterEach or add one if missing)
so the mutation does not leak between tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8518e6f3-f4f4-4974-ad66-8255d6701ac4

📥 Commits

Reviewing files that changed from the base of the PR and between 552945f and 34250af.

📒 Files selected for processing (26)
  • README.md
  • build.js
  • reports/selector-health/selector-health-report.json
  • src/manifest.json
  • src/options/options.css
  • src/options/options.html
  • src/options/options.html.d.ts
  • src/options/options.test.ts
  • src/options/options.ts
  • src/popup/popup.css
  • src/popup/popup.html
  • src/popup/popup.test.ts
  • src/popup/popup.ts
  • src/ts/__mocks__/webextension-polyfill-ts.ts
  • src/ts/background.ts
  • src/ts/focus/focus.ts
  • src/ts/focus/types.ts
  • src/ts/quotes/index.ts
  • src/ts/quotes/quotes.test.ts
  • src/ts/quotes/text-size.ts
  • src/ts/websites/linkedin/linkedin-controller.test.ts
  • src/ts/websites/linkedin/linkedin-controller.ts
  • src/ts/websites/website-config.ts
  • src/ts/websites/website-controller.ts
  • src/ts/websites/x/x-controller.test.ts
  • src/ts/websites/x/x-controller.ts
💤 Files with no reviewable changes (2)
  • src/ts/websites/website-config.ts
  • src/ts/quotes/text-size.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
README.md (2)

9-12: Clarify YouTube behavior across both modes.

LinkedIn and X explain what changes in Focused vs Unfocused, but YouTube only describes one state. In the new two-state UX, that leaves it unclear whether Unfocused behaves differently on YouTube or is intentionally identical.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 9 - 12, Update the "YouTube" bullet in the "Supported
sites" section of README.md to describe both Focused and Unfocused modes
explicitly: state what Focused mode hides (recommended videos, comments,
suggestions, etc.) and what Unfocused mode restores (e.g., brings back
recommendations and comments while still preserving video playback) or note if
Unfocused is identical to Focused; ensure the bullet matches the style of the
LinkedIn and X entries so readers can clearly see the differences between
Focused and Unfocused for YouTube.

23-26: Document how users open the Settings page.

This reads like a future placeholder, but the PR adds an options page entry point now. Please either add a concrete access path for users or explicitly say the page is scaffold-only today so the README does not undersell or obscure the new entry point.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 23 - 26, Update the "Settings page" README section to
either (a) document the concrete access path for users (e.g., explain how to
open the Options/Settings page via the extension menu or a specific URL like
chrome-extension://<extension-id>/options.html and mention any keyboard/toolbar
steps), or (b) explicitly state that the page is scaffold-only today and not yet
user-facing; modify the "Settings page" header content accordingly so readers
know whether the options page is reachable now or is a future placeholder.
src/ts/websites/x/x-controller.test.ts (1)

31-57: Consider adding a focus-mode test case for completeness.

The test verifies unfocus behavior, but there's no corresponding test for focus mode (verifying both feed and sidebar are hidden). This would provide better coverage of the two-state behavior mentioned in the PR objectives.

Example additional test
it('hides both feed and sidebar in focused mode', () => {
  const feed = document.createElement('div')
  const sidebar = document.createElement('aside')

  Object.defineProperty(document, 'URL', {
    value: 'https://x.com/home',
    writable: true,
  })

  ;(XUtils.isHomePage as jest.Mock).mockReturnValue(true)
  ;(XUtils.getXFeed as jest.Mock).mockReturnValue(feed)
  ;(XUtils.getXSidebar as jest.Mock).mockReturnValue(sidebar)
  ;(XUtils.hasFeedLoaded as jest.Mock).mockReturnValue(true)
  ;(XUtils.isFeedHidden as jest.Mock).mockReturnValue(false)
  ;(XUtils.isSidebarHidden as jest.Mock).mockReturnValue(false)

  const controller = new XController()
  controller.focus()

  expect(feed.style.display).toBe('none')
  expect(sidebar.style.display).toBe('none')
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ts/websites/x/x-controller.test.ts` around lines 31 - 57, Add a
complementary focused-mode unit test to x-controller.test.ts that mirrors the
existing unfocused test: mock document.URL to a home URL and set
XUtils.isHomePage, XUtils.getXFeed, XUtils.getXSidebar, XUtils.hasFeedLoaded,
XUtils.isFeedHidden and XUtils.isSidebarHidden appropriately, instantiate
XController and call controller.focus(), then assert that both the feed and
sidebar elements have style.display === 'none' (use XController and its focus()
method and the XUtils mocks to locate the behavior to test).
src/ts/background.ts (1)

25-29: Consider handling the storage initialization promise.

The browser.storage.local.set() call at module scope doesn't handle potential rejection. While MV3 workers commonly use this pattern, a silent failure during startup could leave the extension in an inconsistent state without any indication.

♻️ Optional: Add error handling for visibility
 // In MV3 the worker restarts frequently. Reset appState on startup so each
 // worker begins from the same focused baseline.
 browser.storage.local.set({
   appState: appState,
-})
+}).catch((error) => {
+  console.error('Failed to initialize appState:', error);
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ts/background.ts` around lines 25 - 29, The call to
browser.storage.local.set({ appState }) at module scope ignores the returned
promise and can silently fail; update the startup flow (e.g., in an async init
function or an IIFE) to await or attach a .catch to browser.storage.local.set({
appState }) and surface failures via logging (console.error or your extension
logger) and optional fallback behavior so errors during the appState
initialization are visible and handled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/popup/popup.css`:
- Around line 53-59: The .title CSS rule uses a very tight line-height (0.92)
which risks clipping descenders when the title wraps due to the max-width:
250px; update the .title rule to increase line-height to at least 1.0–1.1 (or
remove the explicit line-height to use the browser default) and/or ensure the
title is constrained to a single line (e.g., disable wrapping) so descenders (g,
y, p, q, j) do not overlap; adjust the .title selector accordingly.
- Around line 87-92: The --accent color (`#3b82f6`) does not meet WCAG AA contrast
(4.5:1) against --paper (`#f5f7fa`), causing .index-label and .info-label to be
insufficiently readable; update the --accent CSS variable to a darker hue that
yields at least 4.5:1 contrast against --paper (or alternatively increase
font-weight/size for .index-label and .info-label), then verify contrast with a
tool. Locate the --accent definition (CSS custom property) and change it to a
darker hex that passes the 4.5:1 check, or adjust .index-label and .info-label
styles to ensure their rendered text meets the required contrast threshold.
Ensure you test the final color against --paper to confirm compliance.

In `@src/ts/websites/x/x-controller.test.ts`:
- Line 55: The test is asserting the wrong element — change the assertion to
verify the feed element that the controller manipulates: obtain the element from
getFeedElement() (which wraps XUtils.getXFeed()) or the mocked feed variable and
assert on its style.display after calling setFeedVisibility(), instead of
asserting on feedItem.style.display; update the test to call
XController.getFeedElement() or reference the mocked feed and
expect(feed.style.display).not.toBe('none') (and adjust any related setup to
ensure the feed is the mocked element).

---

Nitpick comments:
In `@README.md`:
- Around line 9-12: Update the "YouTube" bullet in the "Supported sites" section
of README.md to describe both Focused and Unfocused modes explicitly: state what
Focused mode hides (recommended videos, comments, suggestions, etc.) and what
Unfocused mode restores (e.g., brings back recommendations and comments while
still preserving video playback) or note if Unfocused is identical to Focused;
ensure the bullet matches the style of the LinkedIn and X entries so readers can
clearly see the differences between Focused and Unfocused for YouTube.
- Around line 23-26: Update the "Settings page" README section to either (a)
document the concrete access path for users (e.g., explain how to open the
Options/Settings page via the extension menu or a specific URL like
chrome-extension://<extension-id>/options.html and mention any keyboard/toolbar
steps), or (b) explicitly state that the page is scaffold-only today and not yet
user-facing; modify the "Settings page" header content accordingly so readers
know whether the options page is reachable now or is a future placeholder.

In `@src/ts/background.ts`:
- Around line 25-29: The call to browser.storage.local.set({ appState }) at
module scope ignores the returned promise and can silently fail; update the
startup flow (e.g., in an async init function or an IIFE) to await or attach a
.catch to browser.storage.local.set({ appState }) and surface failures via
logging (console.error or your extension logger) and optional fallback behavior
so errors during the appState initialization are visible and handled.

In `@src/ts/websites/x/x-controller.test.ts`:
- Around line 31-57: Add a complementary focused-mode unit test to
x-controller.test.ts that mirrors the existing unfocused test: mock document.URL
to a home URL and set XUtils.isHomePage, XUtils.getXFeed, XUtils.getXSidebar,
XUtils.hasFeedLoaded, XUtils.isFeedHidden and XUtils.isSidebarHidden
appropriately, instantiate XController and call controller.focus(), then assert
that both the feed and sidebar elements have style.display === 'none' (use
XController and its focus() method and the XUtils mocks to locate the behavior
to test).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ef8f0d28-d0ea-4c2d-89d9-3c068a218755

📥 Commits

Reviewing files that changed from the base of the PR and between 34250af and b7cc127.

📒 Files selected for processing (12)
  • README.md
  • reports/selector-health/selector-health-report.json
  • src/options/options.html
  • src/options/options.ts
  • src/popup/popup.css
  • src/popup/popup.ts
  • src/ts/background.ts
  • src/ts/quotes/index.ts
  • src/ts/quotes/quotes.test.ts
  • src/ts/websites/linkedin/linkedin-controller.test.ts
  • src/ts/websites/website-controller.ts
  • src/ts/websites/x/x-controller.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/ts/websites/website-controller.ts
  • reports/selector-health/selector-health-report.json
  • src/ts/websites/linkedin/linkedin-controller.test.ts
  • src/options/options.ts
  • src/options/options.html

Comment on lines +53 to 59
.title {
font-size: 38px;
line-height: 0.92;
letter-spacing: -0.06em;
max-width: 250px;
margin-top: 6px;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tight line-height may cause text clipping on multi-line titles.

line-height: 0.92 combined with max-width: 250px could cause descenders (g, y, p, q, j) to overlap with the next line if the title wraps. If the title content is guaranteed to be single-line, this is fine; otherwise consider increasing to at least 1.0 or 1.1.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/popup/popup.css` around lines 53 - 59, The .title CSS rule uses a very
tight line-height (0.92) which risks clipping descenders when the title wraps
due to the max-width: 250px; update the .title rule to increase line-height to
at least 1.0–1.1 (or remove the explicit line-height to use the browser default)
and/or ensure the title is constrained to a single line (e.g., disable wrapping)
so descenders (g, y, p, q, j) do not overlap; adjust the .title selector
accordingly.

Comment on lines +87 to 92
.summary {
max-width: 240px;
font-size: 15px;
line-height: 1.4;
color: var(--muted);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Calculate contrast ratios for the color combinations

python3 << 'EOF'
def hex_to_rgb(hex_color):
    hex_color = hex_color.lstrip('#')
    return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))

def relative_luminance(rgb):
    def channel(c):
        c = c / 255.0
        return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
    r, g, b = rgb
    return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)

def contrast_ratio(hex1, hex2):
    l1 = relative_luminance(hex_to_rgb(hex1))
    l2 = relative_luminance(hex_to_rgb(hex2))
    lighter = max(l1, l2)
    darker = min(l1, l2)
    return (lighter + 0.05) / (darker + 0.05)

paper = "#f5f7fa"
ink = "#0f172a"
muted = "#5b6474"
accent = "#3b82f6"

print("WCAG AA requires 4.5:1 for normal text, 3:1 for large text (18px+ or 14px+ bold)")
print()
print(f"--ink on --paper: {contrast_ratio(ink, paper):.2f}:1")
print(f"--muted on --paper: {contrast_ratio(muted, paper):.2f}:1")
print(f"--accent on --paper: {contrast_ratio(accent, paper):.2f}:1")
EOF

Repository: grey-software/focused-browsing

Length of output: 236


Fix --accent color contrast to meet WCAG AA standards.

The .summary text using --muted (#5b6474) on --paper (#f5f7fa) achieves 5.56:1 contrast, which exceeds WCAG AA requirements. However, .index-label and .info-label using --accent (#3b82f6) on the same background only achieve 3.43:1 contrast, falling short of the required 4.5:1 for normal text at 15px font size. The --accent color needs adjustment to improve readability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/popup/popup.css` around lines 87 - 92, The --accent color (`#3b82f6`) does
not meet WCAG AA contrast (4.5:1) against --paper (`#f5f7fa`), causing
.index-label and .info-label to be insufficiently readable; update the --accent
CSS variable to a darker hue that yields at least 4.5:1 contrast against --paper
(or alternatively increase font-weight/size for .index-label and .info-label),
then verify contrast with a tool. Locate the --accent definition (CSS custom
property) and change it to a darker hex that passes the 4.5:1 check, or adjust
.index-label and .info-label styles to ensure their rendered text meets the
required contrast threshold. Ensure you test the final color against --paper to
confirm compliance.

controller.focus()
controller.unfocus()

expect(feedItem.style.display).not.toBe('none')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assertion checks wrong element — should verify feed, not feedItem.

Looking at x-controller.ts, getFeedElement() returns the result of XUtils.getXFeed() (which is feed here), and setFeedVisibility() operates on that element. The test asserts on feedItem.style.display, but feedItem is never directly manipulated by the controller. This assertion passes trivially because feedItem's display is never set.

Proposed fix
-    expect(feedItem.style.display).not.toBe('none')
+    expect(feed.style.display).not.toBe('none')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(feedItem.style.display).not.toBe('none')
expect(feed.style.display).not.toBe('none')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ts/websites/x/x-controller.test.ts` at line 55, The test is asserting the
wrong element — change the assertion to verify the feed element that the
controller manipulates: obtain the element from getFeedElement() (which wraps
XUtils.getXFeed()) or the mocked feed variable and assert on its style.display
after calling setFeedVisibility(), instead of asserting on
feedItem.style.display; update the test to call XController.getFeedElement() or
reference the mocked feed and expect(feed.style.display).not.toBe('none') (and
adjust any related setup to ensure the feed is the mocked element).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant