Skip to content

Latest commit

 

History

History
84 lines (59 loc) · 5.29 KB

File metadata and controls

84 lines (59 loc) · 5.29 KB

Follow-up: Second-pass findings

This is a follow-up to the initial feedback submitted after building the dark mode extension project.

What the regeneration fixed

The regenerated course project (version e8bb55c6-7857-4b45-b7fc-8eba761ad144_v1) addressed 9 of the 10 original defects:

  1. Domain corrected — match patterns and URL guards now target nextwork.ai (and handle the .org redirect)
  2. Toggle switch made clickable — the container is now a <label>, not a <div>, so the whole control is hit-testable
  3. host_permissions added — explicit host permissions declared, so insertCSS has authority on all NextWork tabs
  4. Content-script architecture taught — the service-worker-only design replaced with a content script that applies theme from stored preferences
  5. Flash-of-white fixed — gate attribute set at document_start from a synchronous localStorage mirror
  6. Cross-engine hardening — dual-pattern chrome.* calls (callback + promise) for WebKit-derived browsers
  7. @layer precedence explained — Step 6 correctly teaches that NextWork's Tailwind uses !important inside @layer, and that a layered !important beats an unlayered one
  8. Surface measurement taughtgetComputedStyle sweep replaces guessed structural class names
  9. Dead code removedNEXTWORK_PATTERNS constant eliminated
  10. Documentation errors fixed — file-count mismatch corrected, Orion caching note added

The project now links to this repo as the reference implementation. All the blocking defects are gone, and the taught architecture is sound.

Two remaining issues

1. Popup state race (minor)

What happens: If a user clicks the toggle while chrome.storage.local.get() is still in flight, their click writes the new state but the stale read resolves afterward and flips the switch back. The UI and stored state then disagree.

Reproduction:

// popup.js loads and starts reading
chrome.storage.local.get(['darkMode'], (result) => {
  // This callback runs AFTER a click that happened while it was slow
  darkModeToggle.checked = result.darkMode ?? true;  // clobbers the user's action
});

// User clicks ON before the read completes
// Click handler writes { darkMode: true }
// Then the read resolves and sets .checked = false (stale value)

Likelihood: Low in practice. A cold service worker, a WebKit compatibility layer, or slow disk can delay the read enough to hit the race window. Most clicks land after the read settles.

Fix: Per-key ownership. The popup owns the UI and reads on load; click handlers write immediately without re-reading. Example:

// On load
let state = { darkMode: true, dimImages: false };
chrome.storage.local.get(['darkMode', 'dimImages'], (result) => {
  state = { darkMode: result.darkMode ?? true, dimImages: result.dimImages ?? false };
  darkModeToggle.checked = state.darkMode;
  dimImagesToggle.checked = state.dimImages;
});

// On click
darkModeToggle.addEventListener('change', () => {
  state.darkMode = darkModeToggle.checked;  // local state tracks UI
  chrome.storage.local.set({ darkMode: state.darkMode });  // write, don't re-read
});

The key is: never re-read inside a click handler. The toggle's .checked is the source of truth for what the user just requested.

2. Shadow DOM not taught (major for published projects)

What happens: Published NextWork projects render through a Web Component library (nw-code-block, nw-button, nw-validation-box, etc.). Measured on one project page: 1,032 shadow roots containing 596 light surfaces across 19 component types.

Page CSS cannot cross a shadow boundary. A learner following the project to completion gets an extension that themes the main page but leaves every component interior in light mode — cream code blocks, white buttons, mint reflection cards.

Why it's invisible during learning: The project page the learner writes their answers into is rendered from the editor in plain DOM. The failure only appears on published projects, which is where they will actually use the extension.

Fix: The taught getComputedStyle sweep (Step 6) handles light DOM only. Add a subsection on shadow root piercing:

  1. Detect web components via querySelectorAll('*') and check .shadowRoot
  2. Construct a CSSStyleSheet with unlayered rules (opposite of the light DOM case — components use @layer, so an unlayered declaration outranks them)
  3. Adopt the sheet into each open root: root.adoptedStyleSheets = [sheet]
  4. Watch for new components via MutationObserver and adopt retroactively

Alternatively, a callout in Step 6 noting that dark mode stops at shadow boundaries — and linking to this repo's pierceShadowRoots() function (lines 28-165) — would give learners the vocabulary to research it themselves. The concept matters as much as the fix; most learners will meet shadow DOM for the first time here.

Measured impact: Before shadow piercing, 596 light surfaces on component interiors. After, 2. The page is 2,000+ elements; ~600 of those need this technique.


Both findings were verified empirically. The popup race was reproduced with a synthetic delay; the shadow DOM issue was measured on a live published project page and confirmed by comparing element counts with and without piercing logic.