Repository navigation
Conversation
Typecheck runs again: tsconfig listed uninstalled mocha and chai types, hiding 82 real errors. Content-script JSX is typed as DOM through a local dom-chef jsx-runtime selected per file with a jsxImportSource pragma. Build: webpack and Babel replaced by Bun.build in scripts/build.ts, Tailwind through its PostCSS API from tailwind.config.ts, CSS via the manifest and <link> instead of JS imports. Fifteen dev dependencies go. Audio preset finished, Audio Mode removed: profiles replaced the feature. watch.layout no-video collapses the player and keeps playback; the overlay, visualizer, custom images, pins, B key, popup toggle, options page and store subtree are gone, with watch.visuals and the unimplemented theatre layout. Bugs: Shorts button ids started with '#', the auto-scroll toggle never reached storage, the playback-menu marker class did not match its query, cssText was set to "null". Structure: app/ holds the four surfaces; features/ (playback, playlist, profiles, segments, shorts); lib/ plumbing; ui/ shared React. core/, data/, utils/, components/ and the primitive barrels are gone; cross- folder imports use @/. SegmentPanel.tsx is features/segments/panel/, one file per section. Website copy follows the Audio preset. Storage keys, message protocol, primitive and preset ids, default keybindings and manifest permissions are unchanged.
…ader The options and popup stylesheets import ui/theme.css; the PostCSS pipeline does not resolve @import, so the compiled CSS kept a raw import that pointed nowhere and every --color-* variable was undefined. The build now inlines relative imports first. The no-video layout collapsed #player-container-outer, which YouTube empties in theater mode, moving the player into #full-bleed-container. The container is now found by what it holds, and re-applied when the theater attribute changes. The playlist runtime section depended on a generated class name that YouTube has since renamed. The header is resolved through strategies covering both playlist layouts and reported to the capability cache.
The strangler starts here. kernel/ is the runtime features plug into: a Feature contract mounted and unmounted per navigation, settings slices under settings:<feature> with a one-time migration out of the legacy store, typed request/response messaging alongside the old string protocol, DOM resolution with capability recording (moved out of features/profiles), and a shadow-root mount for Preact UI. youtube/ holds route parsing on yt-navigate-finish and the per-page DOM strategies. playlist-runtime is the first port: the playlist page section and the watch-page badge, rendered with Preact inside shadow roots, fetched through a background message handler that owns the cache, with the enable toggle on the Playlist options page reading the new slice. The legacy context no longer carries playlist data and the old features/playlist folder is gone. Tests run under Bun with happy-dom: settings slices and migration, route parsing, and the feature mounted against fixtures of both playlist header layouts. 19 tests.
Deploying toppings-website with
|
| Latest commit: |
d2cf43e
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://39ffaa34.toppings-website-bq3.pages.dev |
| Branch Preview URL: | https://web-ext-kernel-playlist.toppings-website-bq3.pages.dev |
kernel/keys.ts: a feature declares its actions once (defineKeys) and binds handlers at mount (bindKeys); one keydown dispatcher, unbound on unmount, user overrides in a settings:keybindings slice by action id. The legacy-store mapping for bindings lives with the background's migrations, not the kernel. youtube/shorts.ts resolves the reel video and the action bar separately: the newer layout reuses one renderer without is-active and extracts the action bar beside it. The feature covers both layouts in tests and was checked live across a reel change. Shorts settings are typed numbers now, migrated from the old strings; the options page reads the slice. The playlist header gets header-level fallbacks while the logged-in layout's selector is confirmed.
Default rate, rate and seek keys, and custom rates rendered as chips inside YouTube's speed panel (the menu-list panel it replaced no longer exists). Keyboard seeks get a small overlay of our own since the desktop player has no double-tap overlay to reuse. The legacy watch page keeps only segments, profiles and the gear menu until their own ports. The build script's output directory is overridable so a Chrome and a Firefox build can coexist.
A primitive is now one declarative knob on YouTube's page (selectors, apply, reset, parse), catalogued in youtube/primitives.ts; the kernel keeps a set of values applied while the page re-renders and restores everything on unmount, which replaces the per-primitive hidden-by-us flags and the one-shot apply that missed thumbnails loaded on scroll. The profile store is a local-area settings slice with the same API as before. Gear-menu panel and native settings overlay are Preact, the overlay reading YouTube's --yt-sys-color-baseline tokens so it follows the page theme. The guide link is re-added when the Polymer guide re-renders. Selectors gain YouTube's view-model markup for Shorts shelves and thumbnails. The test harness storage stub now emits change events.
A per-video SegmentSession owns loaded config, playback state and every change, replacing the module-level state that the old orchestration, panel and markers shared. Persistence sits behind a SegmentStorage interface, so the session and feature are tested against an in-memory store; writes to a video's record are serialised so rapid edits cannot clobber each other. Markers, control-bar button and the panel are Preact: markers re-render in place during a drag instead of being rebuilt, the panel uses YouTube's theme tokens in a shadow root, and toast moved into the kernel. With segments ported the content script boots only kernel features, so the legacy context protocol between background and content is gone.
The two pages run on Preact directly (React, react-router, immer and dom-chef are gone), route by hash, and read every value from a settings slice; the legacy store, its context protocol and the copied Tailwind preflight go with it. An app slice carries the master switch and theme: the content script gates every feature on it and the background keeps the toolbar icon in step. The theme is YouTube's own palette and Roboto stack in both modes, with pill buttons and YouTube-style toggles, so the pages read as part of YouTube. The Shortcuts page is rendered from the key registries, and the in-page playlist section uses the same tokens as the rest of the injected UI. Legacy migration now ignores fields the old store lacked, and is covered end to end by a test.
Key matching and YouTube URL constants move to where they belong (kernel/, youtube/), the manifest drops the webNavigation permission the old context protocol needed, and AGENTS/web-ext.md describes the extension as it now is: kernel features, Preact pages, settings slices, and the YouTube quirks the code works around.
This was referenced Sep 27, 2026
The README still described the old setup: a GOOGLE_SECRET the Worker never reads (it wants YOUTUBE_DATA_API_V3_KEY), a global wrangler install that the backend's own devDependency replaces, no step for loading the unpacked extension, and links to Discussions and a wiki that are both disabled. It now reads in the project's own voice, points docs at toppings.enry.ch, and says plainly that playlist runtime is the one feature that talks to the server; the FAQ says the same. The backend and website agent notes pointed at web-ext files the rebuild moved or removed.
Firefox only returns promises from its browser.* namespace under MV2; its chrome.* namespace wants callbacks and returns undefined otherwise. The rebuild calls every extension API promise-style, so in Firefox no setting, profile or keybinding ever loaded: the options page showed no built-in presets, and the failed storage reads left its tabs unable to switch pages. The Firefox build now rewrites the chrome global to browser at bundle time. Verified in Firefox 156 with the extension installed: options tabs, presets, settings writes, popup, and every content feature on live YouTube.
The markers floated above the progress bar as tall numbered flags, with nothing marking the exact cut and nothing showing the range between them. Each point is now a small trapezoid head on a cut line through the bar, the range between in and out is tinted in the segment's colour, and the grab area is wider than the head so dragging stays easy. Segment colours no longer start with red, which disappeared into YouTube's played fill. Checked on a chaptered video at rest and with the bar hovered, and marker dragging in Chromium and Firefox.
The cut line and range tint were drawn over YouTube's bar, covering the played fill and the playhead dot, and the tint leaked through chapter gaps. Markers now mount inside the bar's own stacking context just under the playhead dot, and everything is drawn above the bar: a thin rail in the segment's colour spans the range, with a trapezoid head at each end resting on the bar's top edge. The bar itself is left exactly as YouTube draws it. Checked on a chaptered video with the playhead on a marker, at rest and hovered, and marker dragging in Chromium and Firefox.
Brings back the pin: the trapezoid head on a short needle, now standing on the bar's top edge instead of piercing it, with the range rail joining the heads above the bar. The drop shadow, stroke and hover zoom are gone; at this size each left the shape soft once rasterised at a fractional position. The first segment is YouTube's own red again, since the markers no longer sit on the red played fill.
Dev builds called a local Worker at 127.0.0.1:8787, so without the backend running every playlist lookup failed silently and nothing showed on playlist or watch pages. Builds now use the live API unless TOPPINGS_API points them elsewhere. On a watch page opened from a playlist, the runtime is a line under the panel's "channel · 2/10" row instead of falling to the bottom of the panel. On the playlist page it is one metadata line in the header card's own type and colour. Both use YouTube's clock format. Runtimes are cached for a day, since they only change when the playlist does; refresh bypasses the cache. Checked on a real playlist in Firefox with a development build.
YouTube keeps pages you navigate away from alive but hidden, and parks unused header layouts under display:none. The kernel's element lookup took the first selector match anywhere, so after navigating from a channel to a playlist the runtime mounted into the hidden channel header, and on regular playlists into a parked copy of the page header: nothing showed until a reload, and on regular playlists not even then. resolveTarget now accepts only rendered elements and keeps waiting until one appears, which protects every feature, and it no longer needs the element-ready dependency. Playlist header selectors are also scoped to the playlist page. The runtime sits under "10 videos · Last updated" on course playlists and under "Playlist · 34 videos · views" on regular ones. Checked course and regular playlists, loaded directly and reached from the channel page, plus every other feature, in Chromium and Firefox.
The runtime row and the watch-panel badge now appear as soon as their spot is found, as a shimmering placeholder sized like the text, and fill in when the lookup answers; a failed lookup removes them. The page no longer shifts when the numbers arrive.
Playlist pages still come one at a time, since each page's token arrives with the previous page, but a page's duration lookup no longer blocks the next page request. On a 183-video playlist a lookup went from about 5.1s to 2.3-3.2s, with identical totals.
The runtime row guessed its type (12px at 80% opacity, a middle dot) and so never quite matched the row above it. It now copies font, size, weight, line height and colour from YouTube's own metadata text beside it, and uses YouTube's separator where YouTube has one (the bullet on regular playlists) and plain spacing where it does not (course cards, the watch panel). The playlist row and the watch-panel badge are one component. Injected UI no longer carries the Toppings logo; the credit moves into the hover tooltip of every control we add. Refresh shows the skeleton while it re-fetches. Checked computed styles against YouTube's on regular and course playlists and the watch panel.
The site moves from ink, bone and ember to YouTube's dark theme: its base, text and outline tones, YouTube red for brand accents (the highlighted word, eyebrow marks, cursor, selection, active docs link) and its link blue for focus rings and link hovers. The backdrop's greys lose their blue cast and its progress red becomes YouTube's. Tokens are renamed for what they are now (text, text-2, text-3, red, link). Fonts are unchanged. The custom-rates blurb now says the rates appear in the speed panel.
The extension's UI is English throughout, so a locale folder holding only the store description and the toolbar title localised nothing. Both now sit in the manifest, and the toolbar title reads Toppings instead of Quick Access. Localisation, if it comes, belongs to the whole UI at once.
The pizza-slice illustration gives way to a mark built for small sizes: a heavy lowercase t in YouTube red with a deep-red dot sitting on its stem, the topping. assets/logo.svg is the master; the toolbar icons, the greyed set shown while Toppings is off, the README logo files and the website favicon (plus an SVG icon for browsers that take one) are all exported from it. Checked at 16px on light and dark toolbars, and in the popup and options headers.
Reverts the move to YouTube's palette. Blending in is for what Toppings draws inside YouTube; the site and the brand keep their ink, bone and ember, and all-over YouTube red read as too much. The custom-rates copy fix and the corrected backdrop comment from that change stay.
Replaces the red t with the mark that won: a pizza slice that doubles as a play button, crust along its back edge and three pepperoni, in the site's ember with bone pepperoni. The brand keeps its own colours; YouTube red is for what Toppings draws inside YouTube. Exported to the toolbar icons, the greyed set, the README logo files and the website favicon from assets/logo.svg.
Blending in is for what Toppings draws inside YouTube; the popup and options are Toppings' own space, so they go back to the brand the website wears: ink, bone and ember, with Gloock for headings and the wordmark and Instrument Sans for text. Both fonts ship with the extension (OFL, licences included) rather than loading from Google Fonts. The body now sets the font itself, because Chrome styles extension page bodies with its own system font.
Replaces the ember slice. The crust is a solid band along the arc rather than a ring, the drip is traced from the Recraft V4 study, and 16/32px icons drop the drip so they stay legible.
Restores the pre-rebrand icons, favicon and brand images, and drops the SVGs the rebrand introduced.
After navigating from a watch page, the first engagement panel in the document is the parked watch page's, so auto-scroll advanced with the comments open. The Next button was also matched by its English label. Point-in-time lookups now go through findTarget, the synchronous form of resolveTarget, and any open reel panel holds auto-scroll.
The row was matched by its English label, so outside English the custom rate chips never appeared and the row kept YouTube's stale value. It is now matched by its icon, reported as the watch.speedRow capability, and 1x shows YouTube's own localised word instead of a hard-coded "Normal". findTarget returns a PrimitiveResolution, like resolveTarget.
Requiring controls to be on screen broke three things YouTube hides on purpose: the settings menu while closed (the speed row went stale and profiles could leave YouTube's settings panel hidden on unmount) and the Shorts navigation buttons in a narrow window (auto-scroll stopped). findTarget becomes findWithin: the root (the player, the Shorts page) is the live part, and lookups inside it do not need visibility. The speed row falls back to its English label, the 1x word check accepts any script's digits, the menu hook runs without custom rates, reports the row until it is found, and clears its timer on unmount.
- release.ts zips DIST rather than always dist/, and replaces an existing archive instead of adding to it - type-check covers the tests, and the six errors it found are fixed - CI installs from the lockfile with the pinned Bun - watch mode runs one build at a time - rates round to four places, so sixteenth steps stay exact - index.d.ts declared image modules nothing imports any more
- The capability cache moves from IndexedDB to extension storage. Content scripts wrote it into youtube.com's IndexedDB, so the options page and background, which read the extension's, never saw an entry. - Playlist runtime returns from mount once the skeleton is up; awaiting the backend held every later feature behind the network. A failed or thrown fetch removes the skeleton, a failed refresh keeps the last runtime, and a late answer after navigating away draws nothing. - The background shares one in-flight request per playlist and does not cache an empty answer; Watch Later, Liked and mixes are skipped. - A superseded navigation stops mounting further features. - Keys typed into fields inside Toppings' shadow roots no longer trigger shortcuts, and stored modifier names match regardless of case.
It imported types from react, which is not a dependency. Local checks passed only because a stale react sat in node_modules; CI's clean install has none, so every run failed at type-check.
The manifest takes only numbers as version, so it carries 4.0.0 with 4.0.0-alpha as version_name; the pages, zip name and capability stamps use the full name. The website keeps 3.0.3 until a store release.
- Nothing activates after dispose, so a restore that finishes after navigating away no longer drives the next video. - Reaching a segment's end always advances; only a user seek adopts another segment, so adjacent and nested segments stop looping. - Save as named gets its own id instead of overwriting its source. - Segments wait for a finite duration and for pre-roll ads to end. - Name fields keep what is typed; merge-by-drag cancels on release, ends the drag and clears its timer; stop only undoes a rate the engine set; edits no longer jump to the sequence start.
- Profile import works (the input was read after its event ended). - Shortcut fields let Tab through, Escape cancels, and a shortcut can be reset to its default. - Blank seconds and steps are rejected; the nudge step stays under its maximum. - Select opens on the saved value; Profiles follows changes made elsewhere; a quick edit is committed on navigation. - Profile name label, keyboard-reachable Import and field descriptions. - Search highlights the row in the right section. - Export revokes its blob URL after Firefox has started the download.
- Content features boot once the master switch is known, so a page load with Toppings off no longer applies the default rate, and a theme change no longer remounts every feature. - A route's features mount together; each gets an AbortSignal, and one finishing after the next navigation is undone. - mount() replaces only hosts no live mount owns, so a stale mount can no longer remove the new page's UI. - Profiles act on the live page, not one YouTube keeps hidden, and try strategies in order. - The resolver rechecks on a timer, which also runs in background tabs. - Settings migrate at every background start, for sync that lands after install. - Segments have their own switch, migrated from the old watch-page one; the kernel carries no YouTube selector for typing detection.
- The gear menu reopens on YouTube's own settings after being closed from the Toppings panel, instead of an empty main panel. - A profile change that resolves after unmount no longer starts a primitives run nothing stops.
- A malformed playlist ID gets a 400 before any Data API call. - Once a duration request has failed, no further pages are fetched: the answer is already lost and each page costs quota. - Durations over a day (P1DT2H...) were read as 0; the day part counts.
YouTube plays a mid-roll ad in the video's own element, on the ad's clock, so the engine read the ad's time as a playhead outside the segment and seeked the ad. It now holds while the player shows an ad and picks up where it was when the video returns.
AMO requires data_collection_permissions. The playlist runtime sends the viewed playlist's ID to Toppings' server, declared as the required browsingActivity category. web-ext lint passes with no errors.
- The engine no longer re-seeks forever toward a segment past the end of a trimmed video; toggling a last-used config clamps it like restore. - Seeking into a segment outside the sequence, as when dragging its marker, leaves the playhead there until it leaves the segment. - Settings writes to one slice queue, so two in the same tick both land. - The background marks a reported feature recovered when a content script next records it as supported; checking on update saw only entries stamped with the previous version. - Unchanged capability entries and absent legacy keys are not rewritten, so navigations stop waking the background for nothing. - Content features still boot if the master switch cannot be read.
The active state drew its own square highlight and the inactive one was dimmed, neither of which YouTube's control-bar toggles do; the filled or outlined icon already carries the state, and hover comes from YouTube's own .ytp-button styling.
Selectors, ids, limits and module state now sit above the functions that use them instead of partway down the file; AGENTS.md states the order. Values built from a file's own helpers stay after those helpers.
Collapsing the player hid its controls along with the video. Profiles get back 3.x's Video screen (watch.visuals): real video, black, the audio visualizer or a custom image, drawn over the picture beneath the controls so seeking and the progress bar keep working. The Audio preset uses the visualizer. - Every profile can have a shortcut that switches to it and, pressed again, back to the profile before; Audio defaults to B, as audio mode was, and 3.x's key carries over. - A headphones button in the player toggles Audio (can be turned off). - The visualizer takes the video's audio only once its AudioContext is running, so a page loaded without a gesture never goes silent. - A 3.x custom image carries over; a newly picked one shows at once. - Search finds Re-scan features, which is now a labelled button.
Hiding the sidebar had nothing to do with listening without the picture; Focus and custom profiles are where that belongs.
A schedule turns a profile on during a weekly time window or while at a saved place, and at the end hands back the profile that was on before, unless the user switched away meanwhile; it only acts at its edges. - The background runs on a one-minute alarm; list order is priority and a skip from the popup lasts the current window only. - Feedback for set-and-forget: the toolbar badge shows the profile's initial while a schedule is in charge and counts down the last five minutes before a change, and YouTube tabs toast each change and the heads-up before it. - Places are checked every five minutes, only while a place schedule exists. Location is asked for the way websites ask, from the options page, so the manifest gains no location permission; Chrome's background reads it through an offscreen document. - The build folds process.env.BROWSER, so Firefox bundles carry no Chrome-only calls.
Schedules: the Profiles page gets Schedules (profile, time window or place, enabled, priority order) and Places (saved from the browser's own location prompt, with a radius); a place in use cannot be deleted. The popup shows what a schedule is doing and can skip it for the current window. With Toppings off the runner clears its status line. Sidebar entry: it only switched profiles and sat in YouTube's left drawer; the options page does this more plainly. The overlay, its setting, the open-options message and youtube/guide.ts go; the settings-menu lookup the gear entry needs moves to youtube/player.ts. tsconfig now reports unused locals, which caught two leftover imports. Docs: README, FAQ and changelog describe schedules and correct the permission list; AGENTS/web-ext.md covers the offscreen entry, where state lives, process.env.BROWSER and why location has no permission.
A tab open across an install or update had no working Toppings until it reloaded: the old content script is cut off, so a schedule that switched the profile reached nothing. The background now starts the new script in open YouTube tabs (scripting permission, no new warning), and a new copy asks an older one to stop so shortcuts do not fire twice. Covers left by an old copy are cleared. Schedules switched up to a minute late: the tick was a one-minute alarm recreated on every wake. It is now created once, and an alarm is set for the next heads-up, start or end, which landed within 3 s in Firefox.
Both were written only when Toppings set a rate, and the row was looked up in the page, but while a submenu is open YouTube takes the main list out of the page. A shortcut pressed in the speed panel changed neither the panel nor the row, which caught up only at the next change and looked as if speeds had been skipped. They now follow the video's ratechange: the row is kept once found and updated even while out of the page, and an open speed panel's display, slider and chips redraw, with or without custom rates. Tests fire ratechange as browsers do.
The Toppings entry drew its own chevron next to the one YouTube adds, had no icon, and its panel sat beside YouTube's instead of in its place, with no header of YouTube's and no size. It is now one row built from YouTube's markup (icon, Profile, the active profile's name) that opens a Profile submenu shaped like Quality: YouTube's header and back button, and radio rows with its checkmark. The submenu is swapped in for YouTube's panel the way YouTube swaps its own, sized to fit, and swapped back on back, on picking, when the menu closes and on unmount. Clicks stop at our rows, since YouTube's own back handling would close a menu it never opened a submenu in. The menu grows by the row's height on the first open, which YouTube sized before the row was there. The quick toggles are gone; profiles cover them.
YouTube draws the filled part of its speed track from a CSS variable, --yt-slider-shape-gradient-percent, not from the input's value, so a rate set by Toppings moved the thumb and left the fill behind. The sync now sets it too, along with the slider's own speed text, and widens the range at both ends for rates outside YouTube's 0.25-2.
This branch has not been deployed
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.
Rebuilds the extension on a small kernel and retires the old path in one go, so nothing ships in a mixed state.
Architecture
kernel/): aFeaturecontract mounted per navigation and unmounted on the next; settings slices (defineSettings) with one-shot migration from the old store; a keybinding registry with user overrides; typed background messaging; a primitive engine that keeps page tweaks applied while YouTube re-renders; DOM resolve with a capability cache; Preactmountinto shadow roots; a shared toast and YouTube's own colour tokens for injected UI.youtube/): routes and the navigation event, every selector the extension uses, and the catalogue of page primitives profiles are built from.features/): playlist runtime, Shorts, playback, profiles and segments, each a folder with its settings, keys, messages and a testedFeature. Segments got a per-video session behind a storage interface, with writes serialised.Fixes along the way
webNavigationpermission and the content stylesheet are gone.Verified
bun run check: types clean, 66 tests.Not verifiable headless: the playlist header on a signed-in account's newer layout, which has header-level fallback strategies.
Supersedes #109 and #79.