Conversation
* feat: add zip configuration to exclude .env files * feat: exclude playwright dirs from firefox sources zip
* fix: refactor enableJump handling in defaults and migrations * fix: update import paths for consistency in Options.svelte * fix: improve messaging for reduced motion settings in BasicSettings.svelte * refactor: update scroll behavior settings to Auto/Instant - change existing "Animated" option to "Auto (Recommended)" in UI - remove complex first-install Reduced Motion detection logic - allow users to select Auto scroll even if Reduced Motion is enabled (with info message) - simplify default settings handling * feat: update version to 1.6.0 and add release notes for enhanced scrolling options and accessibility improvements * feat: refactor migration from v1 to v2 for smoothScroll settings and add corresponding tests * chore: format * fix: remove unused import for devLog in defaults.ts
* fix: update screenshot in readme * chore: update dependancies * refactor: update icons to fix svelte and build warnings * chore: format code * chore: update pull request branches to include dev
This is to align with the rest of the codebase.
* feat: add FlagIcon component
* feat: add ReportButton component
Opens a pre-filled Google Form with the chapter URL and current date
for users to report broken recaps.
* feat: show report button in popup on chapter pages
Detects the active tab URL and conditionally renders the ReportButton
in the popup header when the user is on a RoyalRoad chapter page.
* feat: wire up Google Form for popup report button
Replace placeholder form URL and field IDs with actual Google Form.
Remove date field since Google Sheets adds a timestamp automatically.
* feat: add openUrl message handler to background script
* feat: add report broken recap link to chapter pages
Inject a report link into the chapter page sidebar that opens
a pre-filled Google Form with the current chapter URL.
* chore: bump version to 1.7.0 and add patch notes
Add patch notes for bug reporting feature release.
* style: pnpm format
* refactor: extract shared report form config and remove unused handler
Move Google Form URL and builder to shared report-form.ts config.
Remove unused openUrl message handler from background script.
* chore: split host_permissions out of permissions in wxt.config
Required for proper MV3 compliance. Note: existing users will be
prompted to re-approve the royalroad.com host permission on update.
* feat: enrich broken-report with version/browser/type and blurb support
Extend buildReportFormUrl to prefill version, browser, and content type
alongside the chapter URL, and support reporting broken blurbs in
addition to recaps. Introduce a shared ContentType ("recap" | "blurb")
type, extract isChapterUrl into dom-utils, and expose the report link
placement in advanced settings. Adds unit tests for URL building.
NOTE: version/browser/type Google Form entry IDs are still placeholders
(entry.PLACEHOLDER_*) and must be replaced with real field IDs before
those prefills will populate.
* feat: wire real Google Form entry IDs for version/browser/type prefill
Replace placeholder entry IDs with the real field IDs from the report
form (Version, Browser type, Content Type), so all four values now
prefill correctly.
* chore: pin pnpm build-script approvals
Record approved dependency build scripts (esbuild enabled, spawn-sync
disabled) via pnpm approve-builds so contributors and CI share the same
build policy.
* chore: add .gitattributes to normalize line endings to LF
Pin text files to LF in the repo and working trees so Windows
core.autocrlf no longer produces phantom CRLF-only diffs. Mark binary
asset types so their bytes are left untouched.
* docs: update v1.7.0 patch notes date and content
Set release date to 2026-06-21 and reflect what actually shipped: blurb
reporting in addition to recaps, and auto-included version/browser
diagnostics.
* ci: prevent needrestart hang and add timeout to CSS Selectors workflow
Set NEEDRESTART_MODE=a / NEEDRESTART_SUSPEND=1 so `playwright install
--with-deps` no longer hangs on the needrestart prompt on Ubuntu 24.04,
and add timeout-minutes: 15 so any future hang fails fast instead of
squatting a runner. Closes #127.
* ci: upgrade @playwright/test to 1.61.0 to fix browser-install hang
@playwright/test 1.57.0 is affected by a regression (Playwright
1.55.1 to <1.60.0) where `playwright install` downloads Chromium then
hangs indefinitely on system-deps, stalling the CSS Selectors workflow
until the runner times out. Upgrade to 1.61.0, which includes the fix.
Ref: microsoft/playwright#40998
Publishes to the Chrome Web Store and Firefox Add-ons on merge to main (or via manual dispatch). Builds the zips, runs wxt submit, then tags the version and creates a GitHub release. Idempotent: skips if the version is already tagged. Chrome activates once a refresh token secret is present; Firefox publishes independently. Closes #96
* feat: add loading state for recap and blurb fetches Show a centered "Loading…" indicator while a fetch is in flight, scroll to it as soon as it appears (rather than waiting for content), and disable the toggle to ignore repeat clicks during an in-flight fetch. Loading and error states reuse RoyalRoad's Bootstrap utility classes (text-muted/text-danger) so they inherit the page theme on light and dark backgrounds instead of fighting it with inline overrides. * feat: make recap fetching resilient to timeouts and cache misses Abort fetches that hang past 15s so a stuck connection surfaces a real error instead of an indefinite loading state, and map HTTP/network failures to reader-friendly messages. When a word-count change misses the cache (expired TTL), re-fetch the chapter instead of silently leaving stale content on screen. Pass the fetcher's messages straight through rather than double-wrapping them. * refactor: clarify user-facing error messages and guard sanitization Reword selector-failure messages to plain language and point readers to the Report button when RoyalRoad's layout has likely changed. Guard against the sanitizer silently swallowing all content, surfacing an explicit error instead of an empty recap. * chore: bump version to 1.7.1
… e2e) (#130) * test: configure Vitest runner and split Playwright projects Add Vitest (happy-dom) via WXT's WxtVitest plugin for fast unit/component tests, with @testing-library/svelte and an explicit Svelte transform. Separate the two runners: Vitest collects *.unit.test.ts / *.svelte.test.ts, while Playwright is split into an `e2e` project (mocked) and a `canary` project (live), with testIgnore so neither runner double-collects. * test: add unit and component test suites Cover the core logic with Vitest: ContentProcessor (word-count selection from the end, truncation marker, sanitization, error branches), ContentCache TTL/expiry, ContentManager cache hit/miss, dom-utils URL resolution, plus the recap-state runes and the ToggleButton/RecapContainer/ReportLink components. Port the migrations and report-form tests off the Playwright runner onto Vitest and remove their old copies. * test: add mocked Playwright e2e suite and fixtures Drive the real built extension while fulfilling all royalroad.com requests from hand-built HTML fixtures via page.route, so the e2e suite is fast and offline: recap injection/render, the blurb path, and the fetch-error state. Fix the extension-loading fixture to point at .output/chrome-mv3 and load the unpacked extension via channel: "chromium". * test: skip live selector canary when RoyalRoad is unreachable Distinguish a third-party outage from a real selector regression: on a network/DNS/timeout failure the canary skips with a "re-run once the site is back up" message instead of failing, while a loaded-but-missing selector still fails loudly. * ci: split test workflows and isolate live selector canary Run fast Vitest unit/component tests (unit.yaml) and mocked Playwright e2e tests (e2e.yaml) on every non-draft PR into dev/main. Repurpose the existing css-selectors workflow as the live canary: PR-triggered with a source path filter and draft guard, plus manual dispatch.
…yout is served (#136) * chore: ignore local env files * feat: support RoyalRoad redesign (beta) via UI-adapter split Adds a UI-adapter (Strategy) layer so the extension works on both the legacy and "Redesign (beta)" RoyalRoad layouts. Version-agnostic services (fetch, cache, process, mount) consume a resolved adapter; legacy/redesign DOM knowledge and selectors live behind LegacyAdapter/RedesignAdapter. Selectors are stored as per-UI overrides (migrated from the old flat set) and remain user-editable. Also adds a RoyalRoad layout switch backed by an editable, on-demand cookie (optional `cookies` permission): Redesign forces `always`, Classic forces `never`, clearing every cookie scope so RoyalRoad's own cookie can't win. The selector is seeded from the live cookie on open. Removes the injected report link (reporting lives in the popup). * fix: move reduced-motion banner under the scroll behavior control * fix: tidy adapter resolution, component teardown, and cookie sync - mountComponent: unmount the Svelte 5 component on cleanup instead of the dead Svelte 4 $destroy branch, so injected components tear down - isRedesign: honor the user-editable cookie name/value from settings (threaded via resolveAdapter/buildPageContext) and match name=value exactly instead of a loose substring check - background: skip re-applying the gating cookie when only unrelated settings change * chore: apply prettier formatting * fix(adapters): revert host-page tweaks on teardown and scope prev-chapter selector `resolveMounts` restyled RoyalRoad's nav bar (flex-wrap, position, classes) with nothing to undo it, so disabling or updating the extension left the page mutated until reload. Renamed to `prepareMounts` to make the side effect visible at the call site, and it now returns a `cleanup` that restores the previous inline styles and removes only the classes it actually added. `content.ts` registers that with `ctx.onInvalidated`. Also scopes the redesign's previous-chapter selector to `a[href*='/chapter/']`, mirroring legacy. The bare `a:has(> i.fa-arrow-left)` matched any left-arrow anchor — a breadcrumb or back-to-fiction link would win on document order and send the recap fetch to the wrong page. Minor: trim cookie values before comparing in `liveCookieEquals`, drop a redundant null check in `findBlurb`, and note that `resolveAdapter` keys off a chapter-only sentinel. * fix: restore the in-page report link on both layouts The UI-adapter split dropped ReportLink.svelte, its test, and the `reportPlacement` selector, removing the in-page "Report Broken Recap" shortcut shipped in #125/#126 — from the legacy UI as well as the redesign. Only the popup's report button survived. This restores it as a first-class mount point rather than the ad-hoc querySelector it used to be. Legacy keeps its sidebar placement. The redesign inserts the link directly after RoyalRoad's own /report/chapter/ anchor, and both styling decisions there were verified against the live beta: - No `whitespace-nowrap`. That action column is `md:w-auto`, i.e. shrink-to-fit, so a label wider than RoyalRoad's own buttons widened the entire column (184px -> 208px), which stretched Fiction Page and made Report Chapter look shrunken. - Colours come from RoyalRoad's `--color-secondary`/`--color-on-secondary` rather than `color: inherit`. That column inherits `color: black` against a near-black dark-theme background, so the link was effectively invisible. Custom properties also survive RoyalRoad's Tailwind purge, which its colour utilities may not. - `grid-column: 1 / -1` makes it span the full row in the mobile two-up grid and is inert once the column becomes a flex column at `md`. Labels are shortened to "Report Recap"/"Report Blurb" so they fit on one line in the redesign's ~184px column. * refactor: pin settings migrations to frozen schema snapshots Migrations read live values from config/defaults, so today's defaults defined how old data migrated. That wasn't hypothetical: because the current defaults already contain `betaCookie`, `migrateV2toV3` emitted it too, which tripped `migrateV3toV4`'s "already migrated" guard — so the v4 migration never actually ran for anyone upgrading from v2. Each step now carries its own frozen `as const` snapshot of the schema it migrates from, and imports nothing from defaults. `V3_DEFAULTS` deliberately omits `betaCookie` so the v3->v4 step is reachable. Adds a v2->v3->v4 chain test covering that, plus one asserting a customised `reportPlacement` survives as a legacy override. * fix: make beta-cookie sync race-free and drop the unused clear helper `syncFromStorage()` ran fire-and-forget while `watchSettings` registered right after it, so a settings change landing in that window set the dedupe key to the new value only for the in-flight startup read to overwrite it with the stale one — after which reverting to that value was skipped as a false no-op. `syncBetaCookie` now owns the key exclusively, so ordering stops mattering. It also records the key only after a successful apply, so a sync skipped for missing permission retries once granted. `clearBetaCookie` was dead: nothing outside its own test called it, and there is deliberately no "let RoyalRoad decide" mode, since removing the cookie doesn't reliably revert to legacy. Removed it and `applyBetaCookie` from the public surface; the latter is internal to `applyLayoutCookie`, which the tests now exercise instead. * fix(settings): show the layout hint only after a change, and trust exact cookie values The "Save, then reload a RoyalRoad page to apply" note rendered unconditionally, so it sat under the layout dropdown permanently as clutter even when nothing had changed. It's now gated on the selection actually differing from what the view opened with — the baseline is captured after the live-cookie seeding, so seeding itself never counts as a change. Seeding also mapped any unrecognised cookie value to "Classic", which can be flatly wrong if RoyalRoad changes its values. It now only adopts a value that exactly matches the configured beta or classic string, and otherwise leaves the stored choice alone. * refactor: derive settings validity instead of pushing it up through an effect BasicSettings computed word-count validity, then reported it to its parents via an `onValidationChange` callback fired from an `$effect`, which each parent assigned to a `$state` flag gating the Save button. Validity is a pure function of the settings object, so routing it through an effect made the flag lag a render behind and left it stale whenever BasicSettings unmounted — in the popup it stays mounted only in the settings view, while `isValid` is also read by the advanced view. The bounds and the validator now live in `lib/config/validation.ts`, and each side `$derived`s from it independently: the form for its inline error, the parents for the Save gate. The callback prop is gone. The number input's min/max now reference the same constants the validator enforces, so they can't drift. * refactor: address Svelte LSP findings across the components Sweep prompted by the Svelte language server, which hadn't been run over these files in a while. Subscriptions: the `watchSettings` effects read the very state their callback assigned, so every incoming settings change invalidated the effect and tore down and rebuilt the subscription. They now read no state and subscribe once for the component's lifetime, which also makes the `untrack` calls around them unnecessary. `bind:this` + `onMount` replaced with attachments, per the LSP's suggestion: - SettingsButton attaches its host-footer layout to the legacy button itself, so the redesign branch structurally cannot restyle the host dialog and the version guard disappears. The attachment also restores the footer's original inline styles on teardown, which the onMount version never did. - ToggleButton keeps its native, non-delegated click listener — that is load-bearing, since Svelte delegates `onclick` to the app root and a delegated `stopPropagation` runs too late to stop RoyalRoad's `.rr-dialog` from opening — but no longer needs a separate element ref to blur. - PatchNotes dropped its map of article refs entirely; the card to scroll is the trigger's own `<article>` ancestor, reachable with `closest`. Also removes an unused GearIcon import in AdvancedSettingsView, and documents why RecapContainer's `{@html}` is safe (DOMPurify via HtmlSanitizer, strict allowlist) so the warning isn't re-triaged next time. * feat: adapt to whichever RoyalRoad layout is served RoyalRoad is rolling out a Tailwind-based redesign, and readers can be on either UI. The extension now detects which one was served, via the redesign-only `#chapterHeroData` element, and mounts the matching adapter per page load — so the recap button works throughout the rollout without the reader configuring anything. Detection deliberately ignores the `beta-ui-v2` cookie. The sentinel reflects what RoyalRoad actually rendered, whereas a stale `always` could force the redesign adapter onto a page served as legacy. Verified live against RoyalRoad's own layout toggle in both directions. This drops an earlier approach that let the extension *force* a layout by writing that cookie. It needed the optional `cookies` permission, which turns out to be effectively ungrantable on Firefox for Android: `permissions.request()` resolves false with no prompt, a temporarily-installed add-on exposes no permission UI to grant it by hand, and `permissions.contains()` reports the permission absent even when it is held. The switch was therefore inert on mobile while still costing a permission and a settings control that looked functional — and RoyalRoad ships its own layout toggle regardless. Removed: beta-cookie.ts and its tests, the `betaCookie` setting, the layout select in BasicSettings, the cookie section in AdvancedSettings, the background sync, and `optional_permissions: ["cookies"]`. The manifest now requests nothing beyond storage and the royalroad.com host. migrateV4toV5 drops the stored `betaCookie`. v3→v4 stays in place above it, since settings still at v2 or v3 must pass through it to get here. * chore: release notes for v1.8.0 * test(migrations): pin the v2 fixture to its own frozen snapshot The scenarios spread the legacy adapter's live LEGACY_SELECTORS as v2-era input, while the code under test compares against migrations.ts's own frozen V2_LEGACY_SELECTORS. They match today, so the tests passed — but editing any legacy selector would have silently reclassified these "user customised nothing" inputs as "user customised everything", and the resulting failures would have pointed at the migration rather than at the edit. Same reasoning that made the migrations themselves hermetic in a0756e8: a fixture describing schema v2 must not track what the adapters look like now. * refactor(adapters): make RoyalRoad layouts pluggable via a registry Supporting two layouts during the redesign rollout is temporary — one of them will die — but removing either meant touching 11 files, because the layout names were spread across the codebase: hand-written UiVersion union, two by-version maps, hardcoded `{ legacy: {}, redesign: {} }` literals in three places, a `version === "redesign"` branch in a component, and defaults.ts importing the redesign adapter while the legacy adapter imported back from defaults.ts. registry.ts is now the only module in src/ that names a layout. ADAPTERS is tried in order with the last entry as the unconditional fallback, and UiVersion, the by-layout default maps, emptyOverrides(), resolveAdapter() and the Advanced Settings picker all derive from it. Supporting moves: - detect(doc) and label move onto UiAdapter, retiring isRedesign(); each adapter now owns its own sentinel. - LEGACY_SELECTORS/LEGACY_CHROME move into legacy-adapter.ts, breaking the defaults.ts <-> adapter import cycle and making the two adapters symmetric. - selectorOverrides is keyed by `string` rather than the UiVersion union, so adding or dropping a layout needs no storage migration; orphaned overrides sit unread until restoreSelectors() prunes them. - HostClasses becomes HostChrome and carries the styles, icons and labels that differ per layout, so SettingsButton no longer branches on a layout name. Its legacy modal-footer restyling moves into LegacyAdapter.prepareMounts, where MountSet.cleanup already reverts host-page mutations on teardown. Both rip-out directions were performed and typechecked rather than assumed. Deleting the legacy adapter surfaced a latent break: detect() declared with no parameter typechecks while a union of adapters exists, but fails the moment it is the only adapter left. It now takes an unused `_doc`, with a comment. migrations.ts keeps spelling layout keys out literally — migrations describe a schema as it stood at a point in time and must not track the live registry. Drops REDESIGN.md; the adapter guide is kept outside the repo.
PR #131 landed on main as a squash commit, so git lost the ancestry link between the two branches and every dev->main merge re-applied changes main already had. This merge records main as an ancestor without changing any files (dev is already a content superset of main).
Takes `pnpm audit` from 82 advisories (1 critical, 34 high) to 1, and `pnpm audit --prod` to zero. Only dompurify ships in the extension bundle; everything else here is dev or build tooling. dompurify 3.3.1 -> 3.4.12 clears all 17 production advisories. Remaining transitive advisories come in via build tooling (mostly wxt -> web-ext-run) whose parents have not bumped them yet, so they are pinned through pnpm overrides. The one left is yauzl, which has no patched version published. Two things needed handling beyond version bumps: - Test DOM moved from happy-dom to jsdom. DOMPurify >=3.4 mis-walks the DOM under happy-dom and removes the wrong nodes, so <script> survived sanitization while <p> was stripped, which silently defeated the sanitizer tests. The same input sanitizes correctly under jsdom and in real Chromium, so the extension itself was never affected. jsdom does not implement scrollIntoView, so it is stubbed in the Vitest setup. - @sveltejs/vite-plugin-svelte held at 6.x. The 7.x line peers vite ^8 while wxt and vitest resolve vite 7, which installed two copies of vite and broke `pnpm check` on mismatched Plugin types. Moving forward instead would require vitest 4, whose worker start timeout is hardcoded and too short for this repo on WSL, making the suite flake. Verified: 87 unit tests, svelte-check, web-ext lint, prettier, and both Chrome and Firefox builds all pass, including a frozen-lockfile install.
Seismix
added a commit
that referenced
this pull request
Jul 23, 2026
Release PR #138 landed on main as a squash commit (the repo enforces linear history, so squash is the only merge method available). That breaks the ancestry link between the branches, so without this the next dev -> main merge would re-apply changes main already has and conflict on every touched file. Records main as an ancestor without changing any files.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Release v1.8.0 — support for RoyalRoad's new beta layout, plus a dependency and security pass.
Important
Merge this with "Squash and merge" — it is the only method this repo allows
(
mainenforcesrequired_linear_history, and merge/rebase merges are disabled).Squashing flattens
dev's history into one commit onmain, which breaks theancestry link between the branches: the next
dev -> mainmerge would otherwisere-apply changes
mainalready has and conflict on every touched file.After merging, reconcile the branches so the next release merges cleanly:
This records
mainas an ancestor without changing any files. It is a standingpost-release step for this workflow, not a one-time repair.
Changes
Redesign support (#136)
src/lib/adapters/)Fixes (#136)
Dependencies and security (#137)
pnpm auditreduced from 82 advisories (1 critical, 34 high) to 1;pnpm audit --prodfrom 17 to 0dompurify3.3.1 -> 3.4.12 clears every production advisory. It is the only dependency that ships in the bundle.yauzl(moderate), which has no patched version published.Testing
pnpm check,pnpm lint,pnpm format:checkall passOn merge
Merging this to
maintriggerspublish.yaml, which will:v1.8.0and create a GitHub release, once both stores succeedRelated Issues
Includes #136 and #137.
Closes #135 — "Scrolling before recap loaded is jarring", fixed by deferring the scroll until the recap has finished fetching.