Summary
Eliminate the bidirectional coupling between src/features/buttonPlacement/ and src/features/featureMenu/ by merging them into a single ButtonController class that owns all button DOM, menu DOM, fullscreen handling, theater observer, and shared tracking state.
This is Candidate 1 from the architecture exploration doc, next after the config type system (Closes #1348).
Current Architecture
FeatureButtonManager (registry)
-> btn.add(config) -> addFeatureButton(8 params) -> enableFeatureMenu()
-> makeFeatureButton()
-> placeButton()
-> addFeatureItemToMenu() (for feature_menu)
-> trackButton()
-> startFullscreenObserver()
-> handleFullscreenChange()
-> btn.remove() -> removeFeatureButton()
Friction Points
1. Duplicate container creation
#yte-right-controls-container created with identical code in two files:
buttonPlacement/utils.ts (placeButton, player_controls_right case)
featureMenu/index.ts (createFeatureMenuButton)
If selectors drift apart, one placement breaks silently.
2. Three parallel tracking systems
trackedButtons (Map, buttonPlacement/utils.ts) - button state for fullscreen transitions
featuresInMenu (Set, featureMenu/utils.ts) - menu item presence
buttonState (Map, featureButtonManager.ts) - enabled/placement per feature
Fullscreen transitions must manually keep all three in sync.
3. 8-positional-parameter surface
addFeatureButton() takes 8 positional params. Every feature's btn.add() repeats the same boilerplate.
4. Generic unsoundness
addFeatureItemToMenu accepts ListenerType<Toggle> but gets ListenerType<boolean> from handleFullscreenChange.
5. Error recovery gap in addFeatureButton
Early return in placement logic runs before trackButton() - if button creation rejects, tracking state goes inconsistent.
6. Theater observer in wrong module
Theater observer lives in buttonPlacement/utils.ts but manages the "below player" container - a cross-cutting DOM concern.
Files Involved
src/features/buttonPlacement/index.ts - addFeatureButton, removeFeatureButton, getPlacementSelector
src/features/buttonPlacement/utils.ts - makeFeatureButton, placeButton, trackButton, handleFullscreenChange, theater observer
src/features/buttonPlacement/fullscreen.ts - fullscreen observer
src/features/featureMenu/index.ts - enableFeatureMenu, createFeatureMenuButton, setupFeatureMenuEventListeners
src/features/featureMenu/utils.ts - addFeatureItemToMenu, removeFeatureItemFromMenu, featuresInMenu
src/features/_registry/featureButtonManager.ts - orchestrator
Proposed Boundary
Create ButtonController that owns:
- DOM container creation (#yte-right-controls-container, #yte-button-container)
- Button DOM creation (makeFeatureButton)
- Menu DOM creation (addFeatureItemToMenu / removeFeatureItemFromMenu)
- Fullscreen observer + handler
- Theater observer + handler
- Combined tracking state (replacing trackedButtons + featuresInMenu)
The outward API surface shrinks to roughly:
class ButtonController {
async addButton(name, config, icon, listener, isToggle)
async removeButton(name, placement?)
getButton(name): HTMLElement | null
}
featureMenu/utils.ts gets inlined into the controller. featureMenu/index.ts keeps only createFeatureMenuButton / setupFeatureMenuEventListeners (the menu toggle UI).
Steps
- Create ButtonController class with combined state + all methods
- Inline fullscreen.ts observer into the controller
- Inline featureMenu/utils.ts exports into the controller
- Deduplicate container creation
- Collapse addFeatureButton 8 params into options object
- Update all 12 feature index.ts files to call buttonController.addButton
- Remove old files: buttonPlacement/index.ts, buttonPlacement/utils.ts, buttonPlacement/fullscreen.ts, featureMenu/utils.ts
- Prune re-exports and dead code
Summary
Eliminate the bidirectional coupling between
src/features/buttonPlacement/andsrc/features/featureMenu/by merging them into a singleButtonControllerclass that owns all button DOM, menu DOM, fullscreen handling, theater observer, and shared tracking state.This is Candidate 1 from the architecture exploration doc, next after the config type system (Closes #1348).
Current Architecture
Friction Points
1. Duplicate container creation
#yte-right-controls-containercreated with identical code in two files:buttonPlacement/utils.ts(placeButton, player_controls_right case)featureMenu/index.ts(createFeatureMenuButton)If selectors drift apart, one placement breaks silently.
2. Three parallel tracking systems
trackedButtons(Map, buttonPlacement/utils.ts) - button state for fullscreen transitionsfeaturesInMenu(Set, featureMenu/utils.ts) - menu item presencebuttonState(Map, featureButtonManager.ts) - enabled/placement per featureFullscreen transitions must manually keep all three in sync.
3. 8-positional-parameter surface
addFeatureButton()takes 8 positional params. Every feature'sbtn.add()repeats the same boilerplate.4. Generic unsoundness
addFeatureItemToMenuacceptsListenerType<Toggle>but getsListenerType<boolean>fromhandleFullscreenChange.5. Error recovery gap in
addFeatureButtonEarly return in placement logic runs before
trackButton()- if button creation rejects, tracking state goes inconsistent.6. Theater observer in wrong module
Theater observer lives in buttonPlacement/utils.ts but manages the "below player" container - a cross-cutting DOM concern.
Files Involved
src/features/buttonPlacement/index.ts- addFeatureButton, removeFeatureButton, getPlacementSelectorsrc/features/buttonPlacement/utils.ts- makeFeatureButton, placeButton, trackButton, handleFullscreenChange, theater observersrc/features/buttonPlacement/fullscreen.ts- fullscreen observersrc/features/featureMenu/index.ts- enableFeatureMenu, createFeatureMenuButton, setupFeatureMenuEventListenerssrc/features/featureMenu/utils.ts- addFeatureItemToMenu, removeFeatureItemFromMenu, featuresInMenusrc/features/_registry/featureButtonManager.ts- orchestratorProposed Boundary
Create
ButtonControllerthat owns:The outward API surface shrinks to roughly:
featureMenu/utils.ts gets inlined into the controller. featureMenu/index.ts keeps only createFeatureMenuButton / setupFeatureMenuEventListeners (the menu toggle UI).
Steps