From 63a0fecc8b1ca7d9334d38af0a5d2a2032bf29fa Mon Sep 17 00:00:00 2001 From: npeter83 Date: Thu, 22 Oct 2026 23:12:45 +0200 Subject: [PATCH] refactor(frontend): the flat components/ folder moves into shell/, modules// and features// MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pure move (git mv, history kept): 333 files leave components/, which now holds only the platform layer (ui/) and the legal pages. The shell is the frame (App, header, nav rail, palette, boot); each page lives in modules// with its rail, store and tests; what several pages share sits in features//; the shared-link page is watch/. Domain-free helpers went straight to ui/ (ViewSwitcher, FilterRail, the confirm dialog, the language switcher, the copy rows). Every relative specifier was rewritten (imports, dynamic imports, vi.mock, new URL), and the contract tests that scan sources now scan the whole app source, not only components/ — each glob was counted before and after so none went green over fewer files. The locale-format rule matched `/src/components/` only, so the move would have taken ~300 files out from under its i18n-singleton check; it now covers every folder that holds components, with a test per folder. --- docs/gates.md | 2 +- frontend/eslint.config.js | 5 +- frontend/reactPlugin.ts | 2 +- .../PlaylistEditFailures.dom.test.tsx | 16 ++-- .../WriteFailureReporting.dom.test.tsx | 14 +-- .../compilerCoverage.contract.test.ts | 6 +- .../{components => }/compilerSkips.testkit.ts | 2 +- frontend/src/components/legal/LegalLayout.tsx | 2 +- .../components/{ => ui}/ConfirmProvider.tsx | 4 +- .../src/components/{ => ui}/FilterRail.tsx | 6 +- .../components/{ => ui}/LanguageSwitcher.tsx | 14 +-- .../src/components/ui/NavRail.dom.test.tsx | 2 +- .../src/components/ui/Toaster.hosts.test.ts | 10 +- .../src/components/{ => ui}/ViewSwitcher.tsx | 10 +- .../src/components/{ => ui}/confirmContext.ts | 0 .../src/components/{ => ui}/copyItems.tsx | 6 +- .../ui/hiddenFilterCue.contract.test.ts | 4 +- .../ui/pageMenuScope.contract.test.ts | 18 ++-- .../ui/pressableSelectable.contract.test.ts | 84 +++++++++-------- .../{ => ui}/useCopyFeedback.dom.test.ts | 4 +- .../components/{ => ui}/useCopyFeedback.ts | 4 +- .../comfort}/comfortPin.dom.test.ts | 2 +- .../comfortPin.langSwitch.dom.test.tsx | 4 +- .../comfort}/comfortPin.tsx | 12 +-- .../diagnostics}/DebugRecorder.dom.test.tsx | 27 +++--- .../diagnostics}/DebugRecorder.tsx | 30 +++--- .../diagnostics}/HistoryRow.tsx | 4 +- .../diagnostics}/IssueComposer.dom.test.tsx | 8 +- .../diagnostics}/IssueComposer.tsx | 38 ++++---- .../diagnostics}/LogLine.tsx | 2 +- .../diagnostics}/logLineParts.ts | 2 +- .../diagnostics}/useDebugRecording.ts | 18 ++-- .../downloads}/DownloadButton.tsx | 6 +- .../downloads}/DownloadDialog.dom.test.tsx | 10 +- .../downloads}/DownloadDialog.tsx | 28 +++--- .../downloads}/DownloadIntake.dom.test.tsx | 8 +- .../downloads}/DownloadIntake.tsx | 2 +- .../downloads}/DownloadTray.dom.test.tsx | 26 +++--- .../downloads}/DownloadTray.menu.dom.test.tsx | 14 +-- .../downloads}/DownloadTray.tsx | 50 +++++----- .../downloadModals.error.dom.test.tsx | 2 +- .../downloads}/downloadModals.tsx | 12 +-- .../downloads}/downloadRow.dom.test.tsx | 2 +- .../downloads}/downloadRow.tsx | 10 +- .../downloads}/downloadTitle.ts | 2 +- .../downloads}/useDownloadActions.tsx | 14 +-- .../filters}/ActiveFilterChips.tsx | 20 ++-- .../filters}/PersonalViewsModal.dom.test.tsx | 6 +- .../filters}/PersonalViewsModal.tsx | 35 ++++--- .../filters}/SavedViewsPopover.dom.test.tsx | 2 +- .../filters}/SavedViewsPopover.tsx | 14 +-- .../filters}/SelectedViewChip.tsx | 2 +- .../header}/PaletteLauncher.dom.test.tsx | 16 ++-- .../header}/PaletteLauncher.tsx | 6 +- .../header}/SearchBar.dom.test.tsx | 0 .../header}/SearchBar.keys.dom.test.tsx | 6 +- .../header}/SearchBar.tsx | 10 +- .../NavigationProvider.dom.test.tsx | 10 +- .../navigation}/NavigationProvider.tsx | 20 ++-- .../navigation}/navigation.dom.test.ts | 6 +- .../navigation}/navigationContext.ts | 2 +- .../navigation}/navigationInit.ts | 6 +- .../player}/ClipPanel.dom.test.tsx | 18 ++-- .../player}/ClipPanel.tsx | 36 ++++---- .../player}/PlayerModal.dom.test.tsx | 16 ++-- .../player}/PlayerModal.tsx | 62 ++++++------- .../player}/VideoEditor.partial.dom.test.tsx | 8 +- .../player}/VideoEditor.tsx | 38 ++++---- .../player}/descriptionLinks.dom.test.tsx | 0 .../player}/descriptionLinks.tsx | 2 +- .../player}/playerModalControls.tsx | 10 +- .../plexCards}/PlexCollectionAdd.tsx | 14 +-- .../plexCards}/PlexPlaylistAdd.tsx | 20 ++-- .../plexCards}/plexCards.dom.test.tsx | 4 +- .../plexCards}/plexCards.lang.dom.test.tsx | 2 +- .../plexCards.langSwitch.dom.test.tsx | 4 +- .../plexCards}/plexCards.tsx | 42 ++++----- .../plexCards}/usePlexCardActions.tsx | 10 +- .../prefs}/PrefsProvider.dom.test.tsx | 6 +- .../prefs}/PrefsProvider.tsx | 18 ++-- .../prefs}/prefsContext.ts | 6 +- .../video}/AddToPlaylist.tsx | 20 ++-- .../video}/ChannelNameLink.dom.test.tsx | 0 .../video}/ChannelNameLink.tsx | 4 +- .../video}/MediaResumeButtons.tsx | 4 +- .../video}/VideoCard.actions.dom.test.tsx | 6 +- .../video}/VideoCard.channel.dom.test.tsx | 4 +- .../video}/VideoCard.nav.dom.test.tsx | 6 +- .../video}/VideoCard.tsx | 32 +++---- .../video}/VideoFollowUp.tsx | 4 +- .../video}/VideoInfoCard.dom.test.tsx | 4 +- .../video}/VideoInfoCard.tsx | 20 ++-- .../video}/mediaPicture.contract.test.ts | 6 +- .../video}/useVideoActions.dom.test.tsx | 8 +- .../video}/useVideoActions.tsx | 14 +-- .../OnboardingWizard.dialog.dom.test.tsx | 6 +- .../OnboardingWizard.escape.dom.test.tsx | 6 +- .../wizard}/OnboardingWizard.tsx | 24 ++--- .../wizard}/WizardProvider.dom.test.tsx | 6 +- .../wizard}/WizardProvider.tsx | 8 +- .../wizard}/wizardContext.ts | 0 frontend/src/lib/downloadIntake.ts | 2 +- frontend/src/lib/focusSafe.contract.test.ts | 2 +- frontend/src/lib/headings.contract.test.ts | 4 +- .../src/lib/localeFormat.contract.test.ts | 8 ++ .../lib/navPosition.wiring.contract.test.ts | 2 +- frontend/src/lib/navRing.contract.test.ts | 4 +- .../src/lib/nextPage.wiring.contract.test.ts | 6 +- frontend/src/lib/playerModes.ts | 2 +- frontend/src/lib/plexImage.contract.test.ts | 8 +- frontend/src/lib/subscriptionCache.test.ts | 7 +- .../tagChipNames.dom.test.tsx | 4 +- frontend/src/lib/theme.contract.test.ts | 6 +- .../listNav.wiring.contract.test.ts | 6 +- frontend/src/main.tsx | 30 +++--- .../activity}/Activity.dom.test.tsx | 12 +-- .../activity}/Activity.tsx | 32 +++---- .../activity}/ActivityAudit.tsx | 28 +++--- .../activity}/ActivityQuota.tsx | 14 +-- .../activity}/ActivityRuns.tsx | 24 ++--- .../activity}/AdminLogs.dom.test.tsx | 10 +- .../activity}/AdminLogs.tsx | 34 +++---- .../activity}/HistoryList.tsx | 10 +- .../activity}/RunStatusDot.tsx | 0 .../channels}/AboutCell.tsx | 10 +- .../channels}/ChannelAbout.tsx | 8 +- .../channels}/ChannelDiscovery.tsx | 36 ++++---- .../channels}/ChannelLayoutGrid.dom.test.tsx | 8 +- .../channels}/ChannelLayoutGrid.tsx | 10 +- .../channels}/ChannelLink.tsx | 8 +- .../ChannelStatsPopover.dom.test.tsx | 4 +- .../channels}/ChannelStatsPopover.tsx | 12 +-- .../channels}/ChannelTags.editor.dom.test.tsx | 2 +- .../ChannelTags.suggest.dom.test.tsx | 2 +- .../channels}/ChannelTags.tsx | 20 ++-- .../channels}/Channels.tsx | 88 +++++++++--------- .../channels}/ChannelsProvider.dom.test.tsx | 6 +- .../channels}/ChannelsProvider.tsx | 24 ++--- .../ExcludedChannelList.dom.test.tsx | 0 .../channels}/ExcludedChannelList.tsx | 10 +- .../channels}/SyncStatus.tsx | 20 ++-- .../channels}/TagManager.tsx | 30 +++--- .../channels}/channelCells.tsx | 6 +- .../channels}/channelColumns.dom.test.tsx | 2 +- .../channels}/channelColumns.tsx | 14 +-- .../channels}/channelSync.ts | 2 +- .../channels}/channelsContext.ts | 6 +- .../channels}/channelsTagRow.contract.test.ts | 0 .../channels}/useChannelActions.dom.test.tsx | 12 +-- .../channels}/useChannelActions.tsx | 24 ++--- .../channels}/useTagSuggestions.dom.test.tsx | 8 +- .../channels}/useTagSuggestions.ts | 6 +- .../config}/ConfigPanel.choice.dom.test.tsx | 8 +- .../config}/ConfigPanel.field.dom.test.tsx | 8 +- .../config}/ConfigPanel.sections.dom.test.tsx | 8 +- .../config}/ConfigPanel.tsx | 38 ++++---- .../feed}/ChannelPage.backfill.dom.test.tsx | 10 +- .../feed}/ChannelPage.banner.dom.test.tsx | 12 +-- .../feed}/ChannelPage.busyFocus.dom.test.tsx | 12 +-- .../feed}/ChannelPage.fold.dom.test.tsx | 14 +-- .../feed}/ChannelPage.tsx | 68 +++++++------- .../feed}/ChannelPage.watchState.dom.test.tsx | 16 ++-- .../feed}/ChannelPicker.tsx | 6 +- .../feed}/DateFilterSelect.dom.test.tsx | 2 +- .../feed}/DateFilterSelect.tsx | 20 ++-- .../feed}/Feed.countStale.dom.test.tsx | 14 +-- .../feed}/Feed.nextPageFailed.dom.test.tsx | 12 ++- .../feed}/Feed.overrides.dom.test.tsx | 8 +- .../feed}/Feed.playerSession.dom.test.tsx | 14 +-- .../feed}/Feed.reset.dom.test.tsx | 10 +- .../src/{components => modules/feed}/Feed.tsx | 64 ++++++------- .../feed}/Feed.ytsort.dom.test.tsx | 8 +- .../feed}/FeedFiltersProvider.dom.test.ts | 2 +- .../FeedFiltersProvider.seed.dom.test.tsx | 8 +- .../feed}/FeedFiltersProvider.tsx | 12 +-- .../feed}/FeedScopeToggle.tsx | 2 +- .../feed}/FeedViewProvider.tsx | 6 +- .../feed}/SavedViewsProvider.dom.test.tsx | 14 +-- .../feed}/SavedViewsProvider.tsx | 18 ++-- .../{components => modules/feed}/Sidebar.tsx | 30 +++--- .../feed}/TagFilterChips.dom.test.tsx | 4 +- .../feed}/TagFilterChips.tsx | 16 ++-- .../feed}/VideoMenu.dom.test.tsx | 12 +-- .../feed}/VideoMenu.tsx | 8 +- .../feed}/VirtualFeed.tsx | 14 +-- .../feed}/YearFilter.tsx | 20 ++-- .../feed}/channelChipNames.dom.test.tsx | 8 +- .../feed}/facetOption.test.ts | 2 +- .../feed}/facetOption.ts | 4 +- .../feed}/feedFiltersContext.ts | 2 +- .../feed}/feedFiltersInit.ts | 4 +- .../feed}/feedViewContext.ts | 2 +- .../feed}/modulePages.tsx | 8 +- .../feed}/savedViewsContext.ts | 2 +- .../files}/FileCard.tsx | 16 ++-- .../files}/FilePreview.dom.test.tsx | 4 +- .../files}/FilePreview.tsx | 30 +++--- .../files}/Files.menu.dom.test.tsx | 22 ++--- .../{components => modules/files}/Files.tsx | 66 ++++++------- .../files}/FilesProvider.dom.test.tsx | 4 +- .../files}/FilesProvider.tsx | 6 +- .../files}/FilesRail.dom.test.tsx | 8 +- .../files}/FilesRail.tsx | 26 +++--- .../files}/LibraryPlayer.dom.test.tsx | 6 +- .../files}/LibraryPlayer.tsx | 20 ++-- .../{components => modules/files}/LinkBox.tsx | 8 +- .../files}/ShareDialog.dom.test.tsx | 8 +- .../files}/ShareDialog.tsx | 36 +++++--- .../files}/UploadDialog.dom.test.tsx | 10 +- .../files}/UploadDialog.tsx | 28 +++--- .../files}/UserPicker.tsx | 2 +- .../files}/fileBadges.tsx | 6 +- .../files}/fileFolders.tsx | 4 +- .../files}/filesContext.ts | 0 .../files}/useFileActions.tsx | 20 ++-- .../files}/useFolderActions.tsx | 12 +-- .../home}/Home.dom.test.tsx | 12 +-- .../home}/Home.pin.dom.test.tsx | 6 +- .../src/{components => modules/home}/Home.tsx | 71 +++++++------- .../NotificationsPanel.menu.dom.test.tsx | 32 ++++--- .../notifications}/NotificationsPanel.tsx | 62 ++++++------- .../PlaylistLimitsHint.dom.test.tsx | 0 .../playlists}/PlaylistLimitsHint.tsx | 8 +- .../playlists}/Playlists.dom.test.tsx | 18 ++-- .../playlists}/Playlists.tsx | 82 ++++++++--------- .../playlists}/PlaylistsProvider.tsx | 6 +- .../PlaylistsRail.rename.dom.test.tsx | 10 +- .../playlists}/PlaylistsRail.tsx | 52 +++++------ .../playlists}/playlistColumns.tsx | 10 +- .../playlists}/playlistsContext.ts | 0 .../playlists}/playlistsInit.test.ts | 4 +- .../playlists}/playlistsInit.ts | 4 +- .../usePlaylistActions.dom.test.tsx | 12 +-- .../playlists}/usePlaylistActions.tsx | 24 ++--- .../plex}/PlexBrowse.playShow.dom.test.tsx | 2 +- .../plex}/PlexBrowse.tsx | 64 ++++++------- .../plex}/PlexCollectionEditor.tsx | 22 ++--- .../plex}/PlexCollectionView.dom.test.tsx | 10 +- .../plex}/PlexCollectionView.tsx | 54 +++++------ .../plex}/PlexDiscover.dom.test.tsx | 6 +- .../plex}/PlexDiscover.tsx | 22 ++--- .../plex}/PlexInfo.dom.test.tsx | 2 +- .../{components => modules/plex}/PlexInfo.tsx | 40 ++++---- .../plex}/PlexLibraryPicker.dom.test.tsx | 4 +- .../plex}/PlexLibraryPicker.tsx | 12 +-- .../plex}/PlexMenu.dom.test.tsx | 19 ++-- .../{components => modules/plex}/PlexMenu.tsx | 10 +- .../plex}/PlexPersonView.dom.test.tsx | 6 +- .../plex}/PlexPlayer.backspace.dom.test.tsx | 4 +- .../plex}/PlexPlayer.controls.dom.test.tsx | 6 +- .../plex}/PlexPlayer.hls.dom.test.tsx | 4 +- .../plex}/PlexPlayer.panels.dom.test.tsx | 2 +- .../plex}/PlexPlayer.session.dom.test.tsx | 8 +- .../plex}/PlexPlayer.strictmode.dom.test.tsx | 4 +- .../plex}/PlexPlayer.tabtrap.dom.test.tsx | 4 +- .../plex}/PlexPlayer.tsx | 74 +++++++-------- .../plex}/PlexPlayerSettings.dom.test.tsx | 2 +- .../plex}/PlexPlayerSettings.tsx | 12 +-- .../plex}/PlexPlaylistReorder.dom.test.tsx | 4 +- .../PlexPlaylistView.header.dom.test.tsx | 2 +- .../plex}/PlexPlaylistView.menu.dom.test.tsx | 12 +-- .../plex}/PlexPlaylistView.tsx | 62 ++++++------- .../plex}/PlexProvider.dom.test.tsx | 4 +- .../plex}/PlexProvider.tsx | 18 ++-- .../plex}/PlexScopeToggle.dom.test.tsx | 0 .../plex}/PlexScopeToggle.tsx | 2 +- .../plex}/PlexSearchBox.dom.test.tsx | 4 +- .../plex}/PlexSearchBox.tsx | 22 ++--- .../plex}/PlexSidebar.picker.dom.test.tsx | 2 +- .../plex}/PlexSidebar.tsx | 38 ++++---- .../plex}/PlexStatsOverlay.tsx | 6 +- .../plex}/PlexSubtitles.dom.test.tsx | 0 .../plex}/PlexSubtitles.tsx | 4 +- .../plex}/PlexViewBridge.tsx | 0 .../plex}/PlexViewToggle.dom.test.tsx | 0 .../plex}/PlexViewToggle.tsx | 2 +- .../plex}/PosterTile.tsx | 12 +-- .../plex}/UpNextCard.dom.test.tsx | 6 +- .../plex}/UpNextCard.tsx | 16 ++-- .../plex}/plexBrowseLists.tsx | 22 ++--- .../plex}/plexContext.ts | 8 +- .../plex}/plexDetailAside.tsx | 12 +-- .../plex}/plexDetailUi.tsx | 18 ++-- .../plex}/plexPlayable.ts | 0 .../plex}/plexPlayerControls.tsx | 6 +- .../plex}/plexShow.aside.dom.test.tsx | 2 +- .../plex}/plexSubUrl.test.ts | 0 .../plex}/plexSubUrl.ts | 4 +- .../plex}/plexViewContext.ts | 0 .../plex}/plexViews.restart.dom.test.tsx | 2 +- .../plex}/plexViews.tsx | 58 ++++++------ .../plex}/usePlayShow.dom.test.tsx | 6 +- .../plex}/usePlayShow.ts | 10 +- .../plex}/usePlexActiveFilters.dom.test.tsx | 4 +- .../plex}/usePlexActiveFilters.ts | 10 +- .../plex}/usePlexPlaylistEdits.dom.test.tsx | 8 +- .../plex}/usePlexPlaylistEdits.ts | 12 +-- .../scheduler}/Scheduler.dom.test.tsx | 6 +- .../scheduler}/Scheduler.menu.dom.test.tsx | 4 +- .../scheduler}/Scheduler.tsx | 46 +++++----- .../settings}/ApiTokens.dom.test.tsx | 6 +- .../settings}/ApiTokens.tsx | 48 +++++----- .../DiagnosticsSettings.dom.test.tsx | 8 +- .../settings}/DiagnosticsSettings.tsx | 18 ++-- .../settings}/MyLogViewer.dom.test.tsx | 4 +- .../settings}/MyLogViewer.tsx | 18 ++-- .../settings}/ProfileEditor.dom.test.tsx | 4 +- .../settings}/ProfileEditor.tsx | 34 +++---- .../SettingsPanel.account.dom.test.tsx | 6 +- .../SettingsPanel.downloadAuth.dom.test.tsx | 10 +- .../SettingsPanel.downloads.dom.test.tsx | 18 ++-- .../SettingsPanel.signIn.dom.test.tsx | 16 ++-- .../settings}/SettingsPanel.tsx | 61 ++++++------ .../{components => modules/stats}/Stats.tsx | 26 +++--- .../users}/AccountPanel.tsx | 28 +++--- .../users}/AdminUsers.dom.test.tsx | 18 ++-- .../users}/AdminUsers.tsx | 60 ++++++------ .../users}/accountPanel.dom.test.tsx | 10 +- .../users}/adminInvites.dom.test.tsx | 22 ++--- .../users}/adminPendingAccess.dom.test.tsx | 20 ++-- .../users}/downloadAdmin.dom.test.tsx | 2 +- .../users}/downloadAdmin.storage.dom.test.tsx | 8 +- .../users}/downloadAdmin.tsx | 44 ++++----- frontend/src/{components => shell}/About.tsx | 4 +- frontend/src/{ => shell}/App.tsx | 92 +++++++++---------- .../AppCommandPalette.dom.test.tsx | 12 +-- .../AppCommandPalette.tsx | 12 +-- .../BootGate.dom.test.tsx | 4 +- .../src/{components => shell}/BootGate.tsx | 2 +- .../ErrorBoundary.message.dom.test.tsx | 0 .../ErrorBoundary.staleBuild.dom.test.tsx | 0 .../{components => shell}/ErrorBoundary.tsx | 2 +- .../src/{components => shell}/ErrorDialog.tsx | 4 +- .../Header.searchTip.dom.test.tsx | 12 ++- frontend/src/{components => shell}/Header.tsx | 24 ++--- .../NavSidebar.account.dom.test.tsx | 4 +- .../src/{components => shell}/NavSidebar.tsx | 18 ++-- .../{components => shell}/ReleaseNotes.tsx | 6 +- .../src/{components => shell}/SetupWizard.tsx | 8 +- .../{components => shell}/VerifyBanner.tsx | 2 +- .../VersionBanner.dom.test.tsx | 0 .../{components => shell}/VersionBanner.tsx | 2 +- .../src/{components => shell}/Welcome.tsx | 18 ++-- .../authCardStates.dom.test.tsx | 0 .../clientLogWiring.dom.test.tsx | 0 .../formAlerts.dom.test.tsx | 0 .../{components => shell}/moduleRegistry.tsx | 36 ++++---- .../welcomeMode.dom.test.ts | 0 .../welcomeMode.render.dom.test.tsx | 0 .../src/{components => shell}/welcomeMode.ts | 0 .../WatchPage.dom.test.tsx | 0 .../src/{components => watch}/WatchPage.tsx | 16 ++-- .../watchStrings.dom.test.ts | 0 .../src/{components => watch}/watchStrings.ts | 0 frontend/src/watchEntry.contract.test.ts | 2 +- frontend/src/watchMain.tsx | 4 +- 356 files changed, 2433 insertions(+), 2332 deletions(-) rename frontend/src/{components => }/PlaylistEditFailures.dom.test.tsx (95%) rename frontend/src/{components => }/WriteFailureReporting.dom.test.tsx (93%) rename frontend/src/{components => }/compilerCoverage.contract.test.ts (96%) rename frontend/src/{components => }/compilerSkips.testkit.ts (99%) rename frontend/src/components/{ => ui}/ConfirmProvider.tsx (96%) rename frontend/src/components/{ => ui}/FilterRail.tsx (94%) rename frontend/src/components/{ => ui}/LanguageSwitcher.tsx (90%) rename frontend/src/components/{ => ui}/ViewSwitcher.tsx (93%) rename frontend/src/components/{ => ui}/confirmContext.ts (100%) rename frontend/src/components/{ => ui}/copyItems.tsx (93%) rename frontend/src/components/{ => ui}/useCopyFeedback.dom.test.ts (94%) rename frontend/src/components/{ => ui}/useCopyFeedback.ts (94%) rename frontend/src/{components => features/comfort}/comfortPin.dom.test.ts (98%) rename frontend/src/{components => features/comfort}/comfortPin.langSwitch.dom.test.tsx (95%) rename frontend/src/{components => features/comfort}/comfortPin.tsx (96%) rename frontend/src/{components => features/diagnostics}/DebugRecorder.dom.test.tsx (95%) rename frontend/src/{components => features/diagnostics}/DebugRecorder.tsx (95%) rename frontend/src/{components => features/diagnostics}/HistoryRow.tsx (94%) rename frontend/src/{components => features/diagnostics}/IssueComposer.dom.test.tsx (99%) rename frontend/src/{components => features/diagnostics}/IssueComposer.tsx (96%) rename frontend/src/{components => features/diagnostics}/LogLine.tsx (97%) rename frontend/src/{components => features/diagnostics}/logLineParts.ts (95%) rename frontend/src/{components => features/diagnostics}/useDebugRecording.ts (91%) rename frontend/src/{components => features/downloads}/DownloadButton.tsx (93%) rename frontend/src/{components => features/downloads}/DownloadDialog.dom.test.tsx (91%) rename frontend/src/{components => features/downloads}/DownloadDialog.tsx (84%) rename frontend/src/{components => features/downloads}/DownloadIntake.dom.test.tsx (95%) rename frontend/src/{components => features/downloads}/DownloadIntake.tsx (97%) rename frontend/src/{components => features/downloads}/DownloadTray.dom.test.tsx (94%) rename frontend/src/{components => features/downloads}/DownloadTray.menu.dom.test.tsx (95%) rename frontend/src/{components => features/downloads}/DownloadTray.tsx (91%) rename frontend/src/{components => features/downloads}/downloadModals.error.dom.test.tsx (96%) rename frontend/src/{components => features/downloads}/downloadModals.tsx (94%) rename frontend/src/{components => features/downloads}/downloadRow.dom.test.tsx (98%) rename frontend/src/{components => features/downloads}/downloadRow.tsx (95%) rename frontend/src/{components => features/downloads}/downloadTitle.ts (94%) rename frontend/src/{components => features/downloads}/useDownloadActions.tsx (91%) rename frontend/src/{components => features/filters}/ActiveFilterChips.tsx (92%) rename frontend/src/{components => features/filters}/PersonalViewsModal.dom.test.tsx (98%) rename frontend/src/{components => features/filters}/PersonalViewsModal.tsx (93%) rename frontend/src/{components => features/filters}/SavedViewsPopover.dom.test.tsx (98%) rename frontend/src/{components => features/filters}/SavedViewsPopover.tsx (87%) rename frontend/src/{components => features/filters}/SelectedViewChip.tsx (94%) rename frontend/src/{components => features/header}/PaletteLauncher.dom.test.tsx (83%) rename frontend/src/{components => features/header}/PaletteLauncher.tsx (93%) rename frontend/src/{components => features/header}/SearchBar.dom.test.tsx (100%) rename frontend/src/{components => features/header}/SearchBar.keys.dom.test.tsx (95%) rename frontend/src/{components => features/header}/SearchBar.tsx (95%) rename frontend/src/{components => features/navigation}/NavigationProvider.dom.test.tsx (98%) rename frontend/src/{components => features/navigation}/NavigationProvider.tsx (96%) rename frontend/src/{components => features/navigation}/navigation.dom.test.ts (96%) rename frontend/src/{components => features/navigation}/navigationContext.ts (98%) rename frontend/src/{components => features/navigation}/navigationInit.ts (95%) rename frontend/src/{components => features/player}/ClipPanel.dom.test.tsx (94%) rename frontend/src/{components => features/player}/ClipPanel.tsx (93%) rename frontend/src/{components => features/player}/PlayerModal.dom.test.tsx (98%) rename frontend/src/{components => features/player}/PlayerModal.tsx (97%) rename frontend/src/{components => features/player}/VideoEditor.partial.dom.test.tsx (97%) rename frontend/src/{components => features/player}/VideoEditor.tsx (95%) rename frontend/src/{components => features/player}/descriptionLinks.dom.test.tsx (100%) rename frontend/src/{components => features/player}/descriptionLinks.tsx (99%) rename frontend/src/{components => features/player}/playerModalControls.tsx (95%) rename frontend/src/{components => features/plexCards}/PlexCollectionAdd.tsx (91%) rename frontend/src/{components => features/plexCards}/PlexPlaylistAdd.tsx (92%) rename frontend/src/{components => features/plexCards}/plexCards.dom.test.tsx (99%) rename frontend/src/{components => features/plexCards}/plexCards.lang.dom.test.tsx (97%) rename frontend/src/{components => features/plexCards}/plexCards.langSwitch.dom.test.tsx (94%) rename frontend/src/{components => features/plexCards}/plexCards.tsx (95%) rename frontend/src/{components => features/plexCards}/usePlexCardActions.tsx (96%) rename frontend/src/{components => features/prefs}/PrefsProvider.dom.test.tsx (95%) rename frontend/src/{components => features/prefs}/PrefsProvider.tsx (93%) rename frontend/src/{components => features/prefs}/prefsContext.ts (75%) rename frontend/src/{components => features/video}/AddToPlaylist.tsx (90%) rename frontend/src/{components => features/video}/ChannelNameLink.dom.test.tsx (100%) rename frontend/src/{components => features/video}/ChannelNameLink.tsx (95%) rename frontend/src/{components => features/video}/MediaResumeButtons.tsx (94%) rename frontend/src/{components => features/video}/VideoCard.actions.dom.test.tsx (97%) rename frontend/src/{components => features/video}/VideoCard.channel.dom.test.tsx (96%) rename frontend/src/{components => features/video}/VideoCard.nav.dom.test.tsx (96%) rename frontend/src/{components => features/video}/VideoCard.tsx (97%) rename frontend/src/{components => features/video}/VideoFollowUp.tsx (91%) rename frontend/src/{components => features/video}/VideoInfoCard.dom.test.tsx (98%) rename frontend/src/{components => features/video}/VideoInfoCard.tsx (88%) rename frontend/src/{components => features/video}/mediaPicture.contract.test.ts (93%) rename frontend/src/{components => features/video}/useVideoActions.dom.test.tsx (95%) rename frontend/src/{components => features/video}/useVideoActions.tsx (94%) rename frontend/src/{components => features/wizard}/OnboardingWizard.dialog.dom.test.tsx (97%) rename frontend/src/{components => features/wizard}/OnboardingWizard.escape.dom.test.tsx (92%) rename frontend/src/{components => features/wizard}/OnboardingWizard.tsx (95%) rename frontend/src/{components => features/wizard}/WizardProvider.dom.test.tsx (95%) rename frontend/src/{components => features/wizard}/WizardProvider.tsx (95%) rename frontend/src/{components => features/wizard}/wizardContext.ts (100%) rename frontend/src/{components => lib}/tagChipNames.dom.test.tsx (94%) rename frontend/src/{components => }/listNav.wiring.contract.test.ts (96%) rename frontend/src/{components => modules/activity}/Activity.dom.test.tsx (98%) rename frontend/src/{components => modules/activity}/Activity.tsx (90%) rename frontend/src/{components => modules/activity}/ActivityAudit.tsx (89%) rename frontend/src/{components => modules/activity}/ActivityQuota.tsx (93%) rename frontend/src/{components => modules/activity}/ActivityRuns.tsx (94%) rename frontend/src/{components => modules/activity}/AdminLogs.dom.test.tsx (98%) rename frontend/src/{components => modules/activity}/AdminLogs.tsx (94%) rename frontend/src/{components => modules/activity}/HistoryList.tsx (93%) rename frontend/src/{components => modules/activity}/RunStatusDot.tsx (100%) rename frontend/src/{components => modules/channels}/AboutCell.tsx (91%) rename frontend/src/{components => modules/channels}/ChannelAbout.tsx (95%) rename frontend/src/{components => modules/channels}/ChannelDiscovery.tsx (89%) rename frontend/src/{components => modules/channels}/ChannelLayoutGrid.dom.test.tsx (93%) rename frontend/src/{components => modules/channels}/ChannelLayoutGrid.tsx (93%) rename frontend/src/{components => modules/channels}/ChannelLink.tsx (91%) rename frontend/src/{components => modules/channels}/ChannelStatsPopover.dom.test.tsx (97%) rename frontend/src/{components => modules/channels}/ChannelStatsPopover.tsx (90%) rename frontend/src/{components => modules/channels}/ChannelTags.editor.dom.test.tsx (98%) rename frontend/src/{components => modules/channels}/ChannelTags.suggest.dom.test.tsx (98%) rename frontend/src/{components => modules/channels}/ChannelTags.tsx (96%) rename frontend/src/{components => modules/channels}/Channels.tsx (93%) rename frontend/src/{components => modules/channels}/ChannelsProvider.dom.test.tsx (98%) rename frontend/src/{components => modules/channels}/ChannelsProvider.tsx (95%) rename frontend/src/{components => modules/channels}/ExcludedChannelList.dom.test.tsx (100%) rename frontend/src/{components => modules/channels}/ExcludedChannelList.tsx (88%) rename frontend/src/{components => modules/channels}/SyncStatus.tsx (92%) rename frontend/src/{components => modules/channels}/TagManager.tsx (90%) rename frontend/src/{components => modules/channels}/channelCells.tsx (88%) rename frontend/src/{components => modules/channels}/channelColumns.dom.test.tsx (99%) rename frontend/src/{components => modules/channels}/channelColumns.tsx (96%) rename frontend/src/{components => modules/channels}/channelSync.ts (85%) rename frontend/src/{components => modules/channels}/channelsContext.ts (94%) rename frontend/src/{components => modules/channels}/channelsTagRow.contract.test.ts (100%) rename frontend/src/{components => modules/channels}/useChannelActions.dom.test.tsx (95%) rename frontend/src/{components => modules/channels}/useChannelActions.tsx (92%) rename frontend/src/{components => modules/channels}/useTagSuggestions.dom.test.tsx (96%) rename frontend/src/{components => modules/channels}/useTagSuggestions.ts (96%) rename frontend/src/{components => modules/config}/ConfigPanel.choice.dom.test.tsx (95%) rename frontend/src/{components => modules/config}/ConfigPanel.field.dom.test.tsx (93%) rename frontend/src/{components => modules/config}/ConfigPanel.sections.dom.test.tsx (91%) rename frontend/src/{components => modules/config}/ConfigPanel.tsx (94%) rename frontend/src/{components => modules/feed}/ChannelPage.backfill.dom.test.tsx (92%) rename frontend/src/{components => modules/feed}/ChannelPage.banner.dom.test.tsx (87%) rename frontend/src/{components => modules/feed}/ChannelPage.busyFocus.dom.test.tsx (92%) rename frontend/src/{components => modules/feed}/ChannelPage.fold.dom.test.tsx (89%) rename frontend/src/{components => modules/feed}/ChannelPage.tsx (93%) rename frontend/src/{components => modules/feed}/ChannelPage.watchState.dom.test.tsx (86%) rename frontend/src/{components => modules/feed}/ChannelPicker.tsx (90%) rename frontend/src/{components => modules/feed}/DateFilterSelect.dom.test.tsx (97%) rename frontend/src/{components => modules/feed}/DateFilterSelect.tsx (93%) rename frontend/src/{components => modules/feed}/Feed.countStale.dom.test.tsx (90%) rename frontend/src/{components => modules/feed}/Feed.nextPageFailed.dom.test.tsx (90%) rename frontend/src/{components => modules/feed}/Feed.overrides.dom.test.tsx (97%) rename frontend/src/{components => modules/feed}/Feed.playerSession.dom.test.tsx (96%) rename frontend/src/{components => modules/feed}/Feed.reset.dom.test.tsx (95%) rename frontend/src/{components => modules/feed}/Feed.tsx (95%) rename frontend/src/{components => modules/feed}/Feed.ytsort.dom.test.tsx (95%) rename frontend/src/{components => modules/feed}/FeedFiltersProvider.dom.test.ts (96%) rename frontend/src/{components => modules/feed}/FeedFiltersProvider.seed.dom.test.tsx (88%) rename frontend/src/{components => modules/feed}/FeedFiltersProvider.tsx (93%) rename frontend/src/{components => modules/feed}/FeedScopeToggle.tsx (95%) rename frontend/src/{components => modules/feed}/FeedViewProvider.tsx (90%) rename frontend/src/{components => modules/feed}/SavedViewsProvider.dom.test.tsx (95%) rename frontend/src/{components => modules/feed}/SavedViewsProvider.tsx (95%) rename frontend/src/{components => modules/feed}/Sidebar.tsx (92%) rename frontend/src/{components => modules/feed}/TagFilterChips.dom.test.tsx (96%) rename frontend/src/{components => modules/feed}/TagFilterChips.tsx (91%) rename frontend/src/{components => modules/feed}/VideoMenu.dom.test.tsx (94%) rename frontend/src/{components => modules/feed}/VideoMenu.tsx (72%) rename frontend/src/{components => modules/feed}/VirtualFeed.tsx (89%) rename frontend/src/{components => modules/feed}/YearFilter.tsx (92%) rename frontend/src/{components => modules/feed}/channelChipNames.dom.test.tsx (94%) rename frontend/src/{components => modules/feed}/facetOption.test.ts (94%) rename frontend/src/{components => modules/feed}/facetOption.ts (82%) rename frontend/src/{components => modules/feed}/feedFiltersContext.ts (95%) rename frontend/src/{components => modules/feed}/feedFiltersInit.ts (96%) rename frontend/src/{components => modules/feed}/feedViewContext.ts (92%) rename frontend/src/{components => modules/feed}/modulePages.tsx (86%) rename frontend/src/{components => modules/feed}/savedViewsContext.ts (98%) rename frontend/src/{components => modules/files}/FileCard.tsx (88%) rename frontend/src/{components => modules/files}/FilePreview.dom.test.tsx (98%) rename frontend/src/{components => modules/files}/FilePreview.tsx (92%) rename frontend/src/{components => modules/files}/Files.menu.dom.test.tsx (98%) rename frontend/src/{components => modules/files}/Files.tsx (94%) rename frontend/src/{components => modules/files}/FilesProvider.dom.test.tsx (96%) rename frontend/src/{components => modules/files}/FilesProvider.tsx (95%) rename frontend/src/{components => modules/files}/FilesRail.dom.test.tsx (97%) rename frontend/src/{components => modules/files}/FilesRail.tsx (93%) rename frontend/src/{components => modules/files}/LibraryPlayer.dom.test.tsx (97%) rename frontend/src/{components => modules/files}/LibraryPlayer.tsx (90%) rename frontend/src/{components => modules/files}/LinkBox.tsx (94%) rename frontend/src/{components => modules/files}/ShareDialog.dom.test.tsx (97%) rename frontend/src/{components => modules/files}/ShareDialog.tsx (94%) rename frontend/src/{components => modules/files}/UploadDialog.dom.test.tsx (97%) rename frontend/src/{components => modules/files}/UploadDialog.tsx (94%) rename frontend/src/{components => modules/files}/UserPicker.tsx (97%) rename frontend/src/{components => modules/files}/fileBadges.tsx (94%) rename frontend/src/{components => modules/files}/fileFolders.tsx (94%) rename frontend/src/{components => modules/files}/filesContext.ts (100%) rename frontend/src/{components => modules/files}/useFileActions.tsx (94%) rename frontend/src/{components => modules/files}/useFolderActions.tsx (93%) rename frontend/src/{components => modules/home}/Home.dom.test.tsx (99%) rename frontend/src/{components => modules/home}/Home.pin.dom.test.tsx (98%) rename frontend/src/{components => modules/home}/Home.tsx (95%) rename frontend/src/{components => modules/notifications}/NotificationsPanel.menu.dom.test.tsx (93%) rename frontend/src/{components => modules/notifications}/NotificationsPanel.tsx (93%) rename frontend/src/{components => modules/playlists}/PlaylistLimitsHint.dom.test.tsx (100%) rename frontend/src/{components => modules/playlists}/PlaylistLimitsHint.tsx (88%) rename frontend/src/{components => modules/playlists}/Playlists.dom.test.tsx (98%) rename frontend/src/{components => modules/playlists}/Playlists.tsx (95%) rename frontend/src/{components => modules/playlists}/PlaylistsProvider.tsx (92%) rename frontend/src/{components => modules/playlists}/PlaylistsRail.rename.dom.test.tsx (95%) rename frontend/src/{components => modules/playlists}/PlaylistsRail.tsx (92%) rename frontend/src/{components => modules/playlists}/playlistColumns.tsx (96%) rename frontend/src/{components => modules/playlists}/playlistsContext.ts (100%) rename frontend/src/{components => modules/playlists}/playlistsInit.test.ts (93%) rename frontend/src/{components => modules/playlists}/playlistsInit.ts (86%) rename frontend/src/{components => modules/playlists}/usePlaylistActions.dom.test.tsx (95%) rename frontend/src/{components => modules/playlists}/usePlaylistActions.tsx (94%) rename frontend/src/{components => modules/plex}/PlexBrowse.playShow.dom.test.tsx (98%) rename frontend/src/{components => modules/plex}/PlexBrowse.tsx (94%) rename frontend/src/{components => modules/plex}/PlexCollectionEditor.tsx (93%) rename frontend/src/{components => modules/plex}/PlexCollectionView.dom.test.tsx (97%) rename frontend/src/{components => modules/plex}/PlexCollectionView.tsx (92%) rename frontend/src/{components => modules/plex}/PlexDiscover.dom.test.tsx (99%) rename frontend/src/{components => modules/plex}/PlexDiscover.tsx (94%) rename frontend/src/{components => modules/plex}/PlexInfo.dom.test.tsx (99%) rename frontend/src/{components => modules/plex}/PlexInfo.tsx (96%) rename frontend/src/{components => modules/plex}/PlexLibraryPicker.dom.test.tsx (95%) rename frontend/src/{components => modules/plex}/PlexLibraryPicker.tsx (88%) rename frontend/src/{components => modules/plex}/PlexMenu.dom.test.tsx (97%) rename frontend/src/{components => modules/plex}/PlexMenu.tsx (75%) rename frontend/src/{components => modules/plex}/PlexPersonView.dom.test.tsx (98%) rename frontend/src/{components => modules/plex}/PlexPlayer.backspace.dom.test.tsx (96%) rename frontend/src/{components => modules/plex}/PlexPlayer.controls.dom.test.tsx (99%) rename frontend/src/{components => modules/plex}/PlexPlayer.hls.dom.test.tsx (99%) rename frontend/src/{components => modules/plex}/PlexPlayer.panels.dom.test.tsx (99%) rename frontend/src/{components => modules/plex}/PlexPlayer.session.dom.test.tsx (99%) rename frontend/src/{components => modules/plex}/PlexPlayer.strictmode.dom.test.tsx (96%) rename frontend/src/{components => modules/plex}/PlexPlayer.tabtrap.dom.test.tsx (98%) rename frontend/src/{components => modules/plex}/PlexPlayer.tsx (97%) rename frontend/src/{components => modules/plex}/PlexPlayerSettings.dom.test.tsx (97%) rename frontend/src/{components => modules/plex}/PlexPlayerSettings.tsx (96%) rename frontend/src/{components => modules/plex}/PlexPlaylistReorder.dom.test.tsx (98%) rename frontend/src/{components => modules/plex}/PlexPlaylistView.header.dom.test.tsx (98%) rename frontend/src/{components => modules/plex}/PlexPlaylistView.menu.dom.test.tsx (97%) rename frontend/src/{components => modules/plex}/PlexPlaylistView.tsx (94%) rename frontend/src/{components => modules/plex}/PlexProvider.dom.test.tsx (91%) rename frontend/src/{components => modules/plex}/PlexProvider.tsx (94%) rename frontend/src/{components => modules/plex}/PlexScopeToggle.dom.test.tsx (100%) rename frontend/src/{components => modules/plex}/PlexScopeToggle.tsx (96%) rename frontend/src/{components => modules/plex}/PlexSearchBox.dom.test.tsx (97%) rename frontend/src/{components => modules/plex}/PlexSearchBox.tsx (93%) rename frontend/src/{components => modules/plex}/PlexSidebar.picker.dom.test.tsx (98%) rename frontend/src/{components => modules/plex}/PlexSidebar.tsx (94%) rename frontend/src/{components => modules/plex}/PlexStatsOverlay.tsx (97%) rename frontend/src/{components => modules/plex}/PlexSubtitles.dom.test.tsx (100%) rename frontend/src/{components => modules/plex}/PlexSubtitles.tsx (98%) rename frontend/src/{components => modules/plex}/PlexViewBridge.tsx (100%) rename frontend/src/{components => modules/plex}/PlexViewToggle.dom.test.tsx (100%) rename frontend/src/{components => modules/plex}/PlexViewToggle.tsx (95%) rename frontend/src/{components => modules/plex}/PosterTile.tsx (89%) rename frontend/src/{components => modules/plex}/UpNextCard.dom.test.tsx (95%) rename frontend/src/{components => modules/plex}/UpNextCard.tsx (92%) rename frontend/src/{components => modules/plex}/plexBrowseLists.tsx (93%) rename frontend/src/{components => modules/plex}/plexContext.ts (92%) rename frontend/src/{components => modules/plex}/plexDetailAside.tsx (96%) rename frontend/src/{components => modules/plex}/plexDetailUi.tsx (95%) rename frontend/src/{components => modules/plex}/plexPlayable.ts (100%) rename frontend/src/{components => modules/plex}/plexPlayerControls.tsx (95%) rename frontend/src/{components => modules/plex}/plexShow.aside.dom.test.tsx (99%) rename frontend/src/{components => modules/plex}/plexSubUrl.test.ts (100%) rename frontend/src/{components => modules/plex}/plexSubUrl.ts (97%) rename frontend/src/{components => modules/plex}/plexViewContext.ts (100%) rename frontend/src/{components => modules/plex}/plexViews.restart.dom.test.tsx (99%) rename frontend/src/{components => modules/plex}/plexViews.tsx (95%) rename frontend/src/{components => modules/plex}/usePlayShow.dom.test.tsx (96%) rename frontend/src/{components => modules/plex}/usePlayShow.ts (88%) rename frontend/src/{components => modules/plex}/usePlexActiveFilters.dom.test.tsx (98%) rename frontend/src/{components => modules/plex}/usePlexActiveFilters.ts (95%) rename frontend/src/{components => modules/plex}/usePlexPlaylistEdits.dom.test.tsx (97%) rename frontend/src/{components => modules/plex}/usePlexPlaylistEdits.ts (95%) rename frontend/src/{components => modules/scheduler}/Scheduler.dom.test.tsx (99%) rename frontend/src/{components => modules/scheduler}/Scheduler.menu.dom.test.tsx (96%) rename frontend/src/{components => modules/scheduler}/Scheduler.tsx (95%) rename frontend/src/{components => modules/settings}/ApiTokens.dom.test.tsx (97%) rename frontend/src/{components => modules/settings}/ApiTokens.tsx (92%) rename frontend/src/{components => modules/settings}/DiagnosticsSettings.dom.test.tsx (98%) rename frontend/src/{components => modules/settings}/DiagnosticsSettings.tsx (93%) rename frontend/src/{components => modules/settings}/MyLogViewer.dom.test.tsx (97%) rename frontend/src/{components => modules/settings}/MyLogViewer.tsx (94%) rename frontend/src/{components => modules/settings}/ProfileEditor.dom.test.tsx (97%) rename frontend/src/{components => modules/settings}/ProfileEditor.tsx (93%) rename frontend/src/{components => modules/settings}/SettingsPanel.account.dom.test.tsx (90%) rename frontend/src/{components => modules/settings}/SettingsPanel.downloadAuth.dom.test.tsx (93%) rename frontend/src/{components => modules/settings}/SettingsPanel.downloads.dom.test.tsx (77%) rename frontend/src/{components => modules/settings}/SettingsPanel.signIn.dom.test.tsx (84%) rename frontend/src/{components => modules/settings}/SettingsPanel.tsx (95%) rename frontend/src/{components => modules/stats}/Stats.tsx (89%) rename frontend/src/{components => modules/users}/AccountPanel.tsx (92%) rename frontend/src/{components => modules/users}/AdminUsers.dom.test.tsx (92%) rename frontend/src/{components => modules/users}/AdminUsers.tsx (95%) rename frontend/src/{components => modules/users}/accountPanel.dom.test.tsx (94%) rename frontend/src/{components => modules/users}/adminInvites.dom.test.tsx (95%) rename frontend/src/{components => modules/users}/adminPendingAccess.dom.test.tsx (91%) rename frontend/src/{components => modules/users}/downloadAdmin.dom.test.tsx (99%) rename frontend/src/{components => modules/users}/downloadAdmin.storage.dom.test.tsx (95%) rename frontend/src/{components => modules/users}/downloadAdmin.tsx (91%) rename frontend/src/{components => shell}/About.tsx (96%) rename frontend/src/{ => shell}/App.tsx (90%) rename frontend/src/{components => shell}/AppCommandPalette.dom.test.tsx (94%) rename frontend/src/{components => shell}/AppCommandPalette.tsx (94%) rename frontend/src/{components => shell}/BootGate.dom.test.tsx (98%) rename frontend/src/{components => shell}/BootGate.tsx (99%) rename frontend/src/{components => shell}/ErrorBoundary.message.dom.test.tsx (100%) rename frontend/src/{components => shell}/ErrorBoundary.staleBuild.dom.test.tsx (100%) rename frontend/src/{components => shell}/ErrorBoundary.tsx (92%) rename frontend/src/{components => shell}/ErrorDialog.tsx (86%) rename frontend/src/{components => shell}/Header.searchTip.dom.test.tsx (82%) rename frontend/src/{components => shell}/Header.tsx (87%) rename frontend/src/{components => shell}/NavSidebar.account.dom.test.tsx (96%) rename frontend/src/{components => shell}/NavSidebar.tsx (95%) rename frontend/src/{components => shell}/ReleaseNotes.tsx (96%) rename frontend/src/{components => shell}/SetupWizard.tsx (98%) rename frontend/src/{components => shell}/VerifyBanner.tsx (98%) rename frontend/src/{components => shell}/VersionBanner.dom.test.tsx (100%) rename frontend/src/{components => shell}/VersionBanner.tsx (96%) rename frontend/src/{components => shell}/Welcome.tsx (98%) rename frontend/src/{components => shell}/authCardStates.dom.test.tsx (100%) rename frontend/src/{components => shell}/clientLogWiring.dom.test.tsx (100%) rename frontend/src/{components => shell}/formAlerts.dom.test.tsx (100%) rename frontend/src/{components => shell}/moduleRegistry.tsx (76%) rename frontend/src/{components => shell}/welcomeMode.dom.test.ts (100%) rename frontend/src/{components => shell}/welcomeMode.render.dom.test.tsx (100%) rename frontend/src/{components => shell}/welcomeMode.ts (100%) rename frontend/src/{components => watch}/WatchPage.dom.test.tsx (100%) rename frontend/src/{components => watch}/WatchPage.tsx (97%) rename frontend/src/{components => watch}/watchStrings.dom.test.ts (100%) rename frontend/src/{components => watch}/watchStrings.ts (100%) diff --git a/docs/gates.md b/docs/gates.md index effe4bcc..da680464 100644 --- a/docs/gates.md +++ b/docs/gates.md @@ -114,7 +114,7 @@ project checks needs a row. | CVE gate | HIGH/CRITICAL with a fix available only | `--ignore-unfixed` + `--severity HIGH,CRITICAL`: a finding with no fix cannot be acted on by an upgrade; it shows in the scan output. A fixable one fails the publish, and the only way past it is `.trivyignore.yaml`. | 0 open (E32 S1) | | npm audit | `--omit=dev --audit-level=high` | What ships to the browser, at high or above. The dev toolchain's advisories were burned down by hand in E32 S1 (18 → 0). | 0 | | knip | `entry` — one file added | Not a narrowing: knip's Vite plugin reads entries from index.html only, so the shared link's page (`watch.html` → `src/watchMain.tsx`) is named here, or the lane reports that whole entry unused. It adds files to the lane, never takes any out. | 1 entry | -| React Compiler | the functions it skips, pinned | `components/compilerCoverage.contract.test.ts` — the list may only shrink. End state: only VirtualGrid (TanStack Virtual, an IncompatibleLibrary) remains. | 93 (E32 S1) → 29 (E32 S3) → 1, VirtualGrid (E32 S4) | +| React Compiler | the functions it skips, pinned | `src/compilerCoverage.contract.test.ts` — the list may only shrink. End state: only VirtualGrid (TanStack Virtual, an IncompatibleLibrary) remains. | 93 (E32 S1) → 29 (E32 S3) → 1, VirtualGrid (E32 S4) | ## Accepted findings diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 39420b27..1cb8a807 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -99,7 +99,8 @@ const localeFormat = { // is a clock face with its own 12/24h switch, not a date. const formats = /\/src\/lib\/(format|plexPlayerPrefs)\.ts$/.test(path); const collates = /\/src\/lib\/collate\.ts$/.test(path); - const component = /\/src\/components\//.test(path); + // Every folder that holds components (CLAUDE.md "Architecture shape"), not lib/ below them. + const component = /\/src\/(components|shell|modules|features|watch)\//.test(path); const importedFromI18n = (node) => { for (let scope = context.sourceCode.getScope(node); scope; scope = scope.upper) { const v = scope.set.get(node.name); @@ -536,7 +537,7 @@ export default tseslint.config( // function the compiler skips over a suppression is caught by the coverage contract below. // `todo` does NOT see every such shape — a conditional, a loop // or an optional call inside a try skips the function with no lint hit at all — so the full - // per-function picture is components/compilerCoverage.contract.test.ts. + // per-function picture is src/compilerCoverage.contract.test.ts. "react-hooks/todo": "error", "react-hooks/memo-dependencies": "error", "react-hooks/invariant": "error", diff --git a/frontend/reactPlugin.ts b/frontend/reactPlugin.ts index a85a2219..cfdbfae9 100644 --- a/frontend/reactPlugin.ts +++ b/frontend/reactPlugin.ts @@ -15,7 +15,7 @@ import reactCompiler from "babel-plugin-react-compiler"; // there is no logger here, so a skipped component still works and simply re-renders everything under // it. This comment used to say "its rules are also linted"; it was false while `react-hooks/refs` was // off, and 95 functions sat uncompiled behind it. What actually holds the line now is -// src/components/compilerCoverage.contract.test.ts, which runs THIS compiler over the shipped source +// src/compilerCoverage.contract.test.ts, which runs THIS compiler over the shipped source // and fails on any skip not on its list; the react-hooks lint rules are the early warning. None of // them is off (three run at the preset's warn, which `--max-warnings 0` fails — docs/gates.md), but // three of the compiler's diagnostics cannot report under the installed plugin (eslint.config.js diff --git a/frontend/src/components/PlaylistEditFailures.dom.test.tsx b/frontend/src/PlaylistEditFailures.dom.test.tsx similarity index 95% rename from frontend/src/components/PlaylistEditFailures.dom.test.tsx rename to frontend/src/PlaylistEditFailures.dom.test.tsx index 0a5605c6..ce2d7c23 100644 --- a/frontend/src/components/PlaylistEditFailures.dom.test.tsx +++ b/frontend/src/PlaylistEditFailures.dom.test.tsx @@ -20,20 +20,20 @@ vi.mock("react-i18next", async (importOriginal) => { }), }; }); -vi.mock("./confirmContext", () => ({ useConfirm: () => async () => true })); +vi.mock("./components/ui/confirmContext", () => ({ useConfirm: () => async () => true })); const { notify } = vi.hoisted(() => ({ notify: vi.fn((_n: { level: string; message: string }) => 0), })); -vi.mock("../lib/notifications", async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock("./lib/notifications", async (importOriginal) => ({ + ...(await importOriginal()), notify, })); -import PlexPlaylistView from "./PlexPlaylistView"; -import AddToPlaylist, { AddToPlaylistAt } from "./AddToPlaylist"; -import PlexPlaylistAdd from "./PlexPlaylistAdd"; -import TagManager from "./TagManager"; -import { api, HttpError, type PlexCard } from "../lib/api"; +import PlexPlaylistView from "./modules/plex/PlexPlaylistView"; +import AddToPlaylist, { AddToPlaylistAt } from "./features/video/AddToPlaylist"; +import PlexPlaylistAdd from "./features/plexCards/PlexPlaylistAdd"; +import TagManager from "./modules/channels/TagManager"; +import { api, HttpError, type PlexCard } from "./lib/api"; beforeAll(() => { class Stub { diff --git a/frontend/src/components/WriteFailureReporting.dom.test.tsx b/frontend/src/WriteFailureReporting.dom.test.tsx similarity index 93% rename from frontend/src/components/WriteFailureReporting.dom.test.tsx rename to frontend/src/WriteFailureReporting.dom.test.tsx index 1037abf7..abffcd22 100644 --- a/frontend/src/components/WriteFailureReporting.dom.test.tsx +++ b/frontend/src/WriteFailureReporting.dom.test.tsx @@ -17,19 +17,19 @@ vi.mock("react-i18next", async (importOriginal) => { useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }), }; }); -vi.mock("./confirmContext", () => ({ useConfirm: () => async () => true })); +vi.mock("./components/ui/confirmContext", () => ({ useConfirm: () => async () => true })); const { notify } = vi.hoisted(() => ({ notify: vi.fn((_n: { level: string; title?: string; message: string }) => 0), })); -vi.mock("../lib/notifications", async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock("./lib/notifications", async (importOriginal) => ({ + ...(await importOriginal()), notify, })); -import PlexCollectionEditor from "./PlexCollectionEditor"; -import UploadDialog from "./UploadDialog"; -import DownloadDialog from "./DownloadDialog"; -import { api, HttpError } from "../lib/api"; +import PlexCollectionEditor from "./modules/plex/PlexCollectionEditor"; +import UploadDialog from "./modules/files/UploadDialog"; +import DownloadDialog from "./features/downloads/DownloadDialog"; +import { api, HttpError } from "./lib/api"; beforeAll(() => { class Stub { diff --git a/frontend/src/components/compilerCoverage.contract.test.ts b/frontend/src/compilerCoverage.contract.test.ts similarity index 96% rename from frontend/src/components/compilerCoverage.contract.test.ts rename to frontend/src/compilerCoverage.contract.test.ts index 9c30c0fb..9d63722e 100644 --- a/frontend/src/components/compilerCoverage.contract.test.ts +++ b/frontend/src/compilerCoverage.contract.test.ts @@ -26,15 +26,15 @@ const KNOWN_SKIPS: CompileReport["skipped"] = { // The files a regression hurt most: compiled, and they must stay so (a positive check too, so a // harness that compiled nothing could not pass as "no skips"). const MUST_COMPILE: Record = { - "components/Channels.tsx": ["Channels"], + "modules/channels/Channels.tsx": ["Channels"], "components/ui/DataTable.tsx": ["DataTable"], // Under every button and every popover in the app. "components/ui/FocusSafeButton.tsx": ["FocusSafeButton"], "components/ui/anchoredPopoverHooks.ts": ["useAnchoredPopover"], "components/ui/AnchoredPopover.tsx": ["PopoverSurfaceInner"], // The two players — the largest components here, and the last to compile. - "components/PlexPlayer.tsx": ["PlexPlayer"], - "components/PlayerModal.tsx": ["PlayerModal"], + "modules/plex/PlexPlayer.tsx": ["PlexPlayer"], + "features/player/PlayerModal.tsx": ["PlayerModal"], "lib/playerModalHooks.ts": ["useYtProgress"], }; diff --git a/frontend/src/components/compilerSkips.testkit.ts b/frontend/src/compilerSkips.testkit.ts similarity index 99% rename from frontend/src/components/compilerSkips.testkit.ts rename to frontend/src/compilerSkips.testkit.ts index d42731e3..7d9d982b 100644 --- a/frontend/src/components/compilerSkips.testkit.ts +++ b/frontend/src/compilerSkips.testkit.ts @@ -7,7 +7,7 @@ import reactCompiler from "babel-plugin-react-compiler"; // word (`panicThreshold` is "none" and the build has no logger), so this is the only place a skip is // visible at all. Used by compilerCoverage.contract.test.ts. -const SRC = new URL("../", import.meta.url); +const SRC = new URL("./", import.meta.url); /** Every file the production build compiles: src, minus tests and test kits. */ export function shippedSources(): string[] { diff --git a/frontend/src/components/legal/LegalLayout.tsx b/frontend/src/components/legal/LegalLayout.tsx index 6fdb4b15..26892c2d 100644 --- a/frontend/src/components/legal/LegalLayout.tsx +++ b/frontend/src/components/legal/LegalLayout.tsx @@ -12,7 +12,7 @@ import { useTranslation } from "react-i18next"; import { setLanguage, type LangCode } from "../../i18n"; import { formatDate } from "../../lib/format"; import { send } from "../../lib/http"; -import LanguageSwitcher from "../LanguageSwitcher"; +import LanguageSwitcher from "../ui/LanguageSwitcher"; import { SURFACE } from "../ui/surfaces"; // The day the text last changed materially — bump it with every edit of either body. diff --git a/frontend/src/components/ConfirmProvider.tsx b/frontend/src/components/ui/ConfirmProvider.tsx similarity index 96% rename from frontend/src/components/ConfirmProvider.tsx rename to frontend/src/components/ui/ConfirmProvider.tsx index fb58a293..9017af91 100644 --- a/frontend/src/components/ConfirmProvider.tsx +++ b/frontend/src/components/ui/ConfirmProvider.tsx @@ -1,7 +1,7 @@ import { useCallback, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import Modal, { ModalFooter } from "./ui/Modal"; -import { Button } from "./ui/Button"; +import Modal, { ModalFooter } from "./Modal"; +import { Button } from "./Button"; import { ChoiceContext, type ChoiceFn, type ChoiceOptions } from "./confirmContext"; // The dialog behind useConfirm()/useChoice() (see confirmContext.ts for the hooks and their contract). diff --git a/frontend/src/components/FilterRail.tsx b/frontend/src/components/ui/FilterRail.tsx similarity index 94% rename from frontend/src/components/FilterRail.tsx rename to frontend/src/components/ui/FilterRail.tsx index a0d6e241..fc1968c3 100644 --- a/frontend/src/components/FilterRail.tsx +++ b/frontend/src/components/ui/FilterRail.tsx @@ -1,8 +1,8 @@ import { useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { defaultLayout, type PanelId, type PanelLayout } from "../lib/panelLayout"; -import SidePanel from "./ui/SidePanel"; -import PanelGroups, { type PanelItem } from "./ui/PanelGroups"; +import { defaultLayout, type PanelId, type PanelLayout } from "../../lib/panelLayout"; +import SidePanel from "./SidePanel"; +import PanelGroups, { type PanelItem } from "./PanelGroups"; // The shared filter-rail shell, extracted from the three rails that all render the same SidePanel // chrome around a PanelGroups body: the feed Sidebar, the PlexSidebar, and the PlaylistsRail. It owns diff --git a/frontend/src/components/LanguageSwitcher.tsx b/frontend/src/components/ui/LanguageSwitcher.tsx similarity index 90% rename from frontend/src/components/LanguageSwitcher.tsx rename to frontend/src/components/ui/LanguageSwitcher.tsx index 2a848c25..9704afa8 100644 --- a/frontend/src/components/LanguageSwitcher.tsx +++ b/frontend/src/components/ui/LanguageSwitcher.tsx @@ -1,12 +1,12 @@ import { useState } from "react"; import { Globe } from "lucide-react"; -import { MenuCheck } from "./ui/MenuCheck"; -import { LANGUAGES, type LangCode } from "../i18n"; -import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover"; -import { useAnchoredPopover } from "./ui/anchoredPopoverHooks"; -import { Pressable } from "./ui/Pressable"; -import { RAIL_GLYPH, RAIL_ROW } from "./ui/NavRail"; -import { SURFACE } from "./ui/surfaces"; +import { MenuCheck } from "./MenuCheck"; +import { LANGUAGES, type LangCode } from "../../i18n"; +import { PopoverItem, PopoverSurface } from "./AnchoredPopover"; +import { useAnchoredPopover } from "./anchoredPopoverHooks"; +import { Pressable } from "./Pressable"; +import { RAIL_GLYPH, RAIL_ROW } from "./NavRail"; +import { SURFACE } from "./surfaces"; // Compact language picker (globe + current code). Presentational: the parent decides what // changing the language does (set it; persist server-side when signed in). diff --git a/frontend/src/components/ui/NavRail.dom.test.tsx b/frontend/src/components/ui/NavRail.dom.test.tsx index 7c71e99c..cd99d259 100644 --- a/frontend/src/components/ui/NavRail.dom.test.tsx +++ b/frontend/src/components/ui/NavRail.dom.test.tsx @@ -119,7 +119,7 @@ describe("NavRailAction", () => { }); it("the nav rail holds no toolbar icon button", () => { - const src = readFileSync(resolve(__dirname, "../NavSidebar.tsx"), "utf8"); + const src = readFileSync(resolve(__dirname, "../../shell/NavSidebar.tsx"), "utf8"); expect(src).not.toMatch(/]/); }); }); diff --git a/frontend/src/components/ui/Toaster.hosts.test.ts b/frontend/src/components/ui/Toaster.hosts.test.ts index c2d866de..fd1cc2f3 100644 --- a/frontend/src/components/ui/Toaster.hosts.test.ts +++ b/frontend/src/components/ui/Toaster.hosts.test.ts @@ -18,12 +18,12 @@ import { readSources } from "../../lib/rawSources.testkit"; // BOTH patterns: `../**/*.tsx` from this directory does not match the siblings in `ui/` itself, so // ui/Modal.tsx — the overlay behind every dialog in the app — was never checked by this guard at all // (found by a later review). -const sources = readSources(__dirname, ["../**/*.tsx", "./*.tsx"]); +const sources = readSources(__dirname, ["../../**/*.tsx", "!../../design-lab/**"]); // Overlays with nothing a toast could be about: a lazy chunk's black placeholder while the player // loads, and the signed-out landing page's screenshot zoom (the app's Toaster is not even mounted // there — App renders Welcome instead of the shell). -const EXEMPT = new Set(["../PlexBrowse.tsx", "../Welcome.tsx"]); +const EXEMPT = new Set(["../../modules/plex/PlexBrowse.tsx", "../../shell/Welcome.tsx"]); /** Every class string in the file (quoted or templated), including multi-line ones. */ const classStrings = (src: string): string[] => @@ -67,9 +67,9 @@ describe("full-screen overlays host their own toasts", () => { expect(paths).toEqual( expect.arrayContaining([ "./Modal.tsx", - "../PlayerModal.tsx", - "../PlexPlayer.tsx", - "../OnboardingWizard.tsx", + "../../features/player/PlayerModal.tsx", + "../../modules/plex/PlexPlayer.tsx", + "../../features/wizard/OnboardingWizard.tsx", ]), ); }); diff --git a/frontend/src/components/ViewSwitcher.tsx b/frontend/src/components/ui/ViewSwitcher.tsx similarity index 93% rename from frontend/src/components/ViewSwitcher.tsx rename to frontend/src/components/ui/ViewSwitcher.tsx index 710bf418..9473b87b 100644 --- a/frontend/src/components/ViewSwitcher.tsx +++ b/frontend/src/components/ui/ViewSwitcher.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { type LucideIcon } from "lucide-react"; import clsx from "clsx"; -import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover"; -import { useAnchoredPopover } from "./ui/anchoredPopoverHooks"; -import { MenuCheck } from "./ui/MenuCheck"; -import { SelectTrigger } from "./ui/SelectTrigger"; -import { SURFACE } from "./ui/surfaces"; +import { PopoverItem, PopoverSurface } from "./AnchoredPopover"; +import { useAnchoredPopover } from "./anchoredPopoverHooks"; +import { MenuCheck } from "./MenuCheck"; +import { SelectTrigger } from "./SelectTrigger"; +import { SURFACE } from "./surfaces"; export interface ViewOption { id: T; diff --git a/frontend/src/components/confirmContext.ts b/frontend/src/components/ui/confirmContext.ts similarity index 100% rename from frontend/src/components/confirmContext.ts rename to frontend/src/components/ui/confirmContext.ts diff --git a/frontend/src/components/copyItems.tsx b/frontend/src/components/ui/copyItems.tsx similarity index 93% rename from frontend/src/components/copyItems.tsx rename to frontend/src/components/ui/copyItems.tsx index d46d6b9b..0f24e2b7 100644 --- a/frontend/src/components/copyItems.tsx +++ b/frontend/src/components/ui/copyItems.tsx @@ -1,9 +1,9 @@ import { useTranslation } from "react-i18next"; import { Copy, Link2 } from "lucide-react"; -import { copyText } from "../lib/clipboard"; -import { notify } from "../lib/notifications"; -import { type ActionMenuItem } from "./ui/ActionMenu"; +import { copyText } from "../../lib/clipboard"; +import { notify } from "../../lib/notifications"; +import { type ActionMenuItem } from "./ActionMenu"; // "Copy title" and "Copy link", the two rows every object's menu ends its middle group with // (DESIGN.md C1, "Text stays selectable"): on touch the long-press belongs to the context menu, so the text a diff --git a/frontend/src/components/ui/hiddenFilterCue.contract.test.ts b/frontend/src/components/ui/hiddenFilterCue.contract.test.ts index 9c8d5df8..60b98eee 100644 --- a/frontend/src/components/ui/hiddenFilterCue.contract.test.ts +++ b/frontend/src/components/ui/hiddenFilterCue.contract.test.ts @@ -6,9 +6,9 @@ import { describe, expect, it } from "vitest"; // or have no header at all. Each renders the hidden-filter cue, so no filter narrows its rows unseen. describe("every view that filters rows by column", () => { it("renders the hidden-filter cue", () => { - const root = new URL("../", import.meta.url); + const root = new URL("../../", import.meta.url); const sites = readdirSync(root, { recursive: true, encoding: "utf8" }) - .filter((f) => f.endsWith(".tsx") && !f.includes(".test.")) + .filter((f) => f.endsWith(".tsx") && !f.includes(".test.") && !f.startsWith("design-lab")) .map((f) => [f, readFileSync(new URL(f.replace(/\\/g, "/"), root), "utf8")] as const) .filter(([, src]) => /filterRows\(/.test(src)); expect(sites.length).toBeGreaterThanOrEqual(3); diff --git a/frontend/src/components/ui/pageMenuScope.contract.test.ts b/frontend/src/components/ui/pageMenuScope.contract.test.ts index b900d7f3..528961e4 100644 --- a/frontend/src/components/ui/pageMenuScope.contract.test.ts +++ b/frontend/src/components/ui/pageMenuScope.contract.test.ts @@ -7,9 +7,9 @@ import { readSources } from "../../lib/rawSources.testkit"; // stats, users, downloads) keep the browser's menu. A page that starts rendering one fails here until // that decision is taken again. const files = readSources(__dirname, [ - "../**/*.tsx", - "!../**/*.test.*", - "!../design-lab/**", + "../../**/*.tsx", + "!../../**/*.test.*", + "!../../design-lab/**", "!./**", ]) as Record; @@ -17,14 +17,14 @@ describe("the pages with a background menu", () => { it("are the five the owner chose", () => { const users = Object.entries(files) .filter(([, code]) => / path.replace(/^\.\.\//, "")) + .map(([path]) => path.replace(/^(\.\.\/)+/, "")) .sort(); expect(users).toEqual([ - "Channels.tsx", - "Feed.tsx", - "NotificationsPanel.tsx", - "Playlists.tsx", - "PlexBrowse.tsx", + "modules/channels/Channels.tsx", + "modules/feed/Feed.tsx", + "modules/notifications/NotificationsPanel.tsx", + "modules/playlists/Playlists.tsx", + "modules/plex/PlexBrowse.tsx", ]); }); }); diff --git a/frontend/src/components/ui/pressableSelectable.contract.test.ts b/frontend/src/components/ui/pressableSelectable.contract.test.ts index e0c2e500..69385f6a 100644 --- a/frontend/src/components/ui/pressableSelectable.contract.test.ts +++ b/frontend/src/components/ui/pressableSelectable.contract.test.ts @@ -10,66 +10,74 @@ import { readSources } from "../../lib/rawSources.testkit"; // someone decides. const files = readSources(__dirname, [ - "../**/*.tsx", - "!../**/*.test.*", - "!../design-lab/**", + "../../**/*.tsx", + "!../../**/*.test.*", + "!../../design-lab/**", ]) as Record; /** file → [how many plain Pressables, why they are not titles]. */ const PLAIN: Record = { - "ApiTokens.tsx": [ + "modules/settings/ApiTokens.tsx": [ 1, "a token kind's tile in the new-token dialog (an option: its text is its label)", ], - "DownloadTray.tsx": [ + "features/downloads/DownloadTray.tsx": [ 2, "the folded pill (its summary is its label); a failure's reason, which opens its details", ], - "FileCard.tsx": [1, "a card's picture (named by aria-label; its title is the caption)"], - "FilesRail.tsx": [2, "the load-error retry; the New-folder command"], - "Home.tsx": [ + "modules/files/FileCard.tsx": [ + 1, + "a card's picture (named by aria-label; its title is the caption)", + ], + "modules/files/FilesRail.tsx": [2, "the load-error retry; the New-folder command"], + "modules/home/Home.tsx": [ 2, "the hero's picture and a shelf tile's picture (each named by aria-label; its title sits outside)", ], - "LanguageSwitcher.tsx": [1, "an option row"], - "NavSidebar.tsx": [3, "the account trigger and its menu rows"], - "PaletteLauncher.tsx": [2, "launcher controls"], - "PlaylistsRail.tsx": [1, "the load-error retry"], - "PlexCollectionEditor.tsx": [1, "a checkbox row in a picker"], - "PlexInfo.tsx": [1, "the rating filter (a number)"], - "PlexPlayerSettings.tsx": [1, "the clock's date-style options"], - "PlexPlaylistAdd.tsx": [1, "a checkbox row in a picker"], - "PlexPlaylistView.tsx": [1, "a show's poster stack (aria-hidden)"], - "PlexSearchBox.tsx": [1, "a listbox option"], - "plexCards.tsx": [1, "a card's picture (named by aria-label; its title is the caption)"], - "Scheduler.tsx": [1, "the interval editor trigger"], - "SettingsPanel.tsx": [1, "a colour swatch"], - "SyncStatus.tsx": [1, "the status pill trigger"], - "TagManager.tsx": [1, "a suggestion in a picker"], - "UploadDialog.tsx": [1, "the drop zone"], - "VideoEditor.tsx": [2, "timeline glyphs"], - "Welcome.tsx": [1, "a screenshot (zoom)"], - "playlistColumns.tsx": [1, "the thumbnail"], - "plexDetailUi.tsx": [2, "a switch row and the poster"], - "plexViews.tsx": [1, "the rating filter (a number)"], - "ui/ActionMenu.tsx": [1, "a menu row"], - "ui/BackToTop.tsx": [1, "a control"], - "ui/CommandPalette.tsx": [1, "a command row"], - "ui/NavRail.tsx": [ + "components/ui/LanguageSwitcher.tsx": [1, "an option row"], + "shell/NavSidebar.tsx": [3, "the account trigger and its menu rows"], + "features/header/PaletteLauncher.tsx": [2, "launcher controls"], + "modules/playlists/PlaylistsRail.tsx": [1, "the load-error retry"], + "modules/plex/PlexCollectionEditor.tsx": [1, "a checkbox row in a picker"], + "modules/plex/PlexInfo.tsx": [1, "the rating filter (a number)"], + "modules/plex/PlexPlayerSettings.tsx": [1, "the clock's date-style options"], + "features/plexCards/PlexPlaylistAdd.tsx": [1, "a checkbox row in a picker"], + "modules/plex/PlexPlaylistView.tsx": [1, "a show's poster stack (aria-hidden)"], + "modules/plex/PlexSearchBox.tsx": [1, "a listbox option"], + "features/plexCards/plexCards.tsx": [ + 1, + "a card's picture (named by aria-label; its title is the caption)", + ], + "modules/scheduler/Scheduler.tsx": [1, "the interval editor trigger"], + "modules/settings/SettingsPanel.tsx": [1, "a colour swatch"], + "modules/channels/SyncStatus.tsx": [1, "the status pill trigger"], + "modules/channels/TagManager.tsx": [1, "a suggestion in a picker"], + "modules/files/UploadDialog.tsx": [1, "the drop zone"], + "features/player/VideoEditor.tsx": [2, "timeline glyphs"], + "shell/Welcome.tsx": [1, "a screenshot (zoom)"], + "modules/playlists/playlistColumns.tsx": [1, "the thumbnail"], + "modules/plex/plexDetailUi.tsx": [2, "a switch row and the poster"], + "modules/plex/plexViews.tsx": [1, "the rating filter (a number)"], + "components/ui/ActionMenu.tsx": [1, "a menu row"], + "components/ui/BackToTop.tsx": [1, "a control"], + "components/ui/CommandPalette.tsx": [1, "a command row"], + "components/ui/NavRail.tsx": [ 2, "the action rows (About, Sign out) and the group row that opens its panel; a page row is a link", ], - "ui/PanelGroup.tsx": [2, "a group's collapse toggle and its label"], - "ui/SidePanel.tsx": [2, "panel controls"], - "ui/tableHeader.tsx": [1, "a column's sort header"], + "components/ui/PanelGroup.tsx": [2, "a group's collapse toggle and its label"], + "components/ui/SidePanel.tsx": [2, "panel controls"], + "components/ui/tableHeader.tsx": [1, "a column's sort header"], }; function scan(): { plain: Record; selectable: number } { const plain: Record = {}; let selectable = 0; for (const [path, code] of Object.entries(files)) { - // Glob keys are relative to this folder: `./x.tsx` is ui/, `../x.tsx` is components/. - const rel = path.startsWith("./") ? `ui/${path.slice(2)}` : path.replace(/^\.\.\//, ""); + // Glob keys are relative to this folder (`./x.tsx` is ui/); the table keys them from src/. + const rel = path.startsWith("./") + ? `components/ui/${path.slice(2)}` + : path.replace(/^(\.\.\/)+/, ""); for (const m of code.matchAll(/ ({ ...(await importOriginal()), useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }), })); -vi.mock("../lib/clipboard", () => ({ copyText: h.copyText })); -vi.mock("../lib/notifications", () => ({ notify: h.notify })); +vi.mock("../../lib/clipboard", () => ({ copyText: h.copyText })); +vi.mock("../../lib/notifications", () => ({ notify: h.notify })); import { useCopyFeedback } from "./useCopyFeedback"; diff --git a/frontend/src/components/useCopyFeedback.ts b/frontend/src/components/ui/useCopyFeedback.ts similarity index 94% rename from frontend/src/components/useCopyFeedback.ts rename to frontend/src/components/ui/useCopyFeedback.ts index 6e05f60e..9c12c0a4 100644 --- a/frontend/src/components/useCopyFeedback.ts +++ b/frontend/src/components/ui/useCopyFeedback.ts @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { copyText } from "../lib/clipboard"; -import { notify } from "../lib/notifications"; +import { copyText } from "../../lib/clipboard"; +import { notify } from "../../lib/notifications"; // How long a Copy button reads "Copied" after it worked. const COPIED_MS = 2000; diff --git a/frontend/src/components/comfortPin.dom.test.ts b/frontend/src/features/comfort/comfortPin.dom.test.ts similarity index 98% rename from frontend/src/components/comfortPin.dom.test.ts rename to frontend/src/features/comfort/comfortPin.dom.test.ts index 79e8f7fc..19100de1 100644 --- a/frontend/src/components/comfortPin.dom.test.ts +++ b/frontend/src/features/comfort/comfortPin.dom.test.ts @@ -8,7 +8,7 @@ // disabled state shows an enabled row that no-ops. import { describe, it, expect } from "vitest"; import { pinTargetForCard, shelfMoveTarget } from "./comfortPin"; -import type { DiscoverCard } from "../lib/api"; +import type { DiscoverCard } from "../../lib/api"; const card = (over: Partial): DiscoverCard => ({ diff --git a/frontend/src/components/comfortPin.langSwitch.dom.test.tsx b/frontend/src/features/comfort/comfortPin.langSwitch.dom.test.tsx similarity index 95% rename from frontend/src/components/comfortPin.langSwitch.dom.test.tsx rename to frontend/src/features/comfort/comfortPin.langSwitch.dom.test.tsx index 568eadba..91389638 100644 --- a/frontend/src/components/comfortPin.langSwitch.dom.test.tsx +++ b/frontend/src/features/comfort/comfortPin.langSwitch.dom.test.tsx @@ -8,8 +8,8 @@ import { describe, it, expect, afterEach, vi } from "vitest"; import type { ReactNode } from "react"; import { renderHook, act, waitFor, cleanup } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { setLanguage } from "../i18n"; -import { api } from "../lib/api"; +import { setLanguage } from "../../i18n"; +import { api } from "../../lib/api"; import { useComfortPin } from "./comfortPin"; // Unmount BEFORE the language goes back: a mounted reader re-renders on the switch, and under a loaded diff --git a/frontend/src/components/comfortPin.tsx b/frontend/src/features/comfort/comfortPin.tsx similarity index 96% rename from frontend/src/components/comfortPin.tsx rename to frontend/src/features/comfort/comfortPin.tsx index ce522587..1ecef7ec 100644 --- a/frontend/src/components/comfortPin.tsx +++ b/frontend/src/features/comfort/comfortPin.tsx @@ -3,12 +3,12 @@ import { useTranslation } from "react-i18next"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2, Pin, PinOff } from "lucide-react"; -import { plexLang } from "../i18n"; -import { api, type DiscoverCard, type PinKind } from "../lib/api"; -import { notifyActionError } from "../lib/actionErrors"; -import { notify } from "../lib/notifications"; -import { qk } from "../lib/queryKeys"; -import { type ActionMenuItem } from "./ui/ActionMenu"; +import { plexLang } from "../../i18n"; +import { api, type DiscoverCard, type PinKind } from "../../lib/api"; +import { notifyActionError } from "../../lib/actionErrors"; +import { notify } from "../../lib/notifications"; +import { qk } from "../../lib/queryKeys"; +import { type ActionMenuItem } from "../../components/ui/ActionMenu"; // "put this on my comfort shelf", from wherever you happen to be looking at the thing. // diff --git a/frontend/src/components/DebugRecorder.dom.test.tsx b/frontend/src/features/diagnostics/DebugRecorder.dom.test.tsx similarity index 95% rename from frontend/src/components/DebugRecorder.dom.test.tsx rename to frontend/src/features/diagnostics/DebugRecorder.dom.test.tsx index 7043659b..5d928c22 100644 --- a/frontend/src/components/DebugRecorder.dom.test.tsx +++ b/frontend/src/features/diagnostics/DebugRecorder.dom.test.tsx @@ -10,8 +10,8 @@ import { useState } from "react"; import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import type { DebugSession, Me } from "../lib/api"; -import { qk } from "../lib/queryKeys"; +import type { DebugSession, Me } from "../../lib/api"; +import { qk } from "../../lib/queryKeys"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); @@ -58,8 +58,8 @@ const { order, session, startDebugSession, stopDebugSession, hold, past } = vi.h }); return { order, session, startDebugSession, stopDebugSession, hold, past }; }); -vi.mock("../lib/api", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("../../lib/api", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, api: { @@ -70,8 +70,8 @@ vi.mock("../lib/api", async (importOriginal) => { }, }; }); -vi.mock("../lib/diagnostics", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("../../lib/diagnostics", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, flushNow: () => { @@ -82,16 +82,21 @@ vi.mock("../lib/diagnostics", async (importOriginal) => { }); import DebugRecorder, { RecorderSettings } from "./DebugRecorder"; -import Modal from "./ui/Modal"; -import Toaster from "./ui/Toaster"; +import Modal from "../../components/ui/Modal"; +import Toaster from "../../components/ui/Toaster"; import { closeComposer, openRecorderStart, setStepPrompt, useComposerSession, -} from "../lib/debugRecorder"; -import { anyBlockingLayer, anyBlockingLayerAbove, pushLayer, removeLayer } from "../lib/layerStack"; -import { events } from "../lib/log"; +} from "../../lib/debugRecorder"; +import { + anyBlockingLayer, + anyBlockingLayerAbove, + pushLayer, + removeLayer, +} from "../../lib/layerStack"; +import { events } from "../../lib/log"; beforeEach(() => { order.length = 0; diff --git a/frontend/src/components/DebugRecorder.tsx b/frontend/src/features/diagnostics/DebugRecorder.tsx similarity index 95% rename from frontend/src/components/DebugRecorder.tsx rename to frontend/src/features/diagnostics/DebugRecorder.tsx index abfeb44d..91e1033f 100644 --- a/frontend/src/components/DebugRecorder.tsx +++ b/frontend/src/features/diagnostics/DebugRecorder.tsx @@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Camera, Flag, Square } from "lucide-react"; -import { api, type DebugSession, type DebugSessionLevel } from "../lib/api"; -import { canCaptureTab } from "../lib/captureTab"; +import { api, type DebugSession, type DebugSessionLevel } from "../../lib/api"; +import { canCaptureTab } from "../../lib/captureTab"; import { closeRecorderStart, onSessionChangeElsewhere, @@ -15,21 +15,21 @@ import { useRecorderStartOpen, useStepCount, useStepPrompt, -} from "../lib/debugRecorder"; -import { formatDateTimeSec, formatDuration } from "../lib/format"; -import { useLayer } from "../lib/layerStack"; -import { notify } from "../lib/notifications"; -import { qk } from "../lib/queryKeys"; -import { dockUnderToasts } from "../lib/toastDock"; -import { useMe } from "../lib/useMe"; -import { Button } from "./ui/Button"; -import Modal, { ModalFooter } from "./ui/Modal"; -import { LoadingState, StateMessage } from "./ui/QueryState"; -import SegmentedToggle from "./ui/SegmentedToggle"; +} from "../../lib/debugRecorder"; +import { formatDateTimeSec, formatDuration } from "../../lib/format"; +import { useLayer } from "../../lib/layerStack"; +import { notify } from "../../lib/notifications"; +import { qk } from "../../lib/queryKeys"; +import { dockUnderToasts } from "../../lib/toastDock"; +import { useMe } from "../../lib/useMe"; +import { Button } from "../../components/ui/Button"; +import Modal, { ModalFooter } from "../../components/ui/Modal"; +import { LoadingState, StateMessage } from "../../components/ui/QueryState"; +import SegmentedToggle from "../../components/ui/SegmentedToggle"; import IssueComposer from "./IssueComposer"; import { useDebugRecording } from "./useDebugRecording"; -import { Input } from "./ui/Input"; -import { SURFACE } from "./ui/surfaces"; +import { Input } from "../../components/ui/Input"; +import { SURFACE } from "../../components/ui/surfaces"; // The debug recorder: Start → reproduce → Stop. // diff --git a/frontend/src/components/HistoryRow.tsx b/frontend/src/features/diagnostics/HistoryRow.tsx similarity index 94% rename from frontend/src/components/HistoryRow.tsx rename to frontend/src/features/diagnostics/HistoryRow.tsx index bf19aa8b..52b583cb 100644 --- a/frontend/src/components/HistoryRow.tsx +++ b/frontend/src/features/diagnostics/HistoryRow.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from "react"; -import { navItemAttrs } from "../lib/listNav"; -import { Pressable } from "./ui/Pressable"; +import { navItemAttrs } from "../../lib/listNav"; +import { Pressable } from "../../components/ui/Pressable"; // One dense row of a history list — a background run, an admin action, a log line (./LogLine draws // its lines through this): its cells on one line, and the row opens in place to its details. One frame diff --git a/frontend/src/components/IssueComposer.dom.test.tsx b/frontend/src/features/diagnostics/IssueComposer.dom.test.tsx similarity index 99% rename from frontend/src/components/IssueComposer.dom.test.tsx rename to frontend/src/features/diagnostics/IssueComposer.dom.test.tsx index 02d9fbfd..563b02d8 100644 --- a/frontend/src/components/IssueComposer.dom.test.tsx +++ b/frontend/src/features/diagnostics/IssueComposer.dom.test.tsx @@ -8,8 +8,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import type { DebugDraft, DebugSession, ServerLogRow } from "../lib/api"; -import { qk } from "../lib/queryKeys"; +import type { DebugDraft, DebugSession, ServerLogRow } from "../../lib/api"; +import { qk } from "../../lib/queryKeys"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); @@ -38,8 +38,8 @@ const api = vi.hoisted(() => ({ // The server's half-failure: the issue is committed, then an upload dies and the call 422s. failSendAfterCreate: false, })); -vi.mock("../lib/api", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("../../lib/api", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, api: { diff --git a/frontend/src/components/IssueComposer.tsx b/frontend/src/features/diagnostics/IssueComposer.tsx similarity index 96% rename from frontend/src/components/IssueComposer.tsx rename to frontend/src/features/diagnostics/IssueComposer.tsx index b10c3ec2..d45a1e15 100644 --- a/frontend/src/components/IssueComposer.tsx +++ b/frontend/src/features/diagnostics/IssueComposer.tsx @@ -3,11 +3,11 @@ import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ExternalLink, FileText, Plus, Server, X } from "lucide-react"; -import { api, type DebugScreenshot, type DebugSession, type ServerLogRow } from "../lib/api"; -import { closeComposer } from "../lib/debugRecorder"; -import { formatBytes, formatDateTimeSec, formatUnit } from "../lib/format"; -import { notify } from "../lib/notifications"; -import { useMe } from "../lib/useMe"; +import { api, type DebugScreenshot, type DebugSession, type ServerLogRow } from "../../lib/api"; +import { closeComposer } from "../../lib/debugRecorder"; +import { formatBytes, formatDateTimeSec, formatUnit } from "../../lib/format"; +import { notify } from "../../lib/notifications"; +import { useMe } from "../../lib/useMe"; import { draftSteps, environment, @@ -15,21 +15,21 @@ import { type DraftForm, type DraftStep, type IssueText, -} from "../lib/issueDraft"; -import { moduleDef } from "../lib/modules"; -import type { Page } from "../lib/urlState"; -import { qk } from "../lib/queryKeys"; -import { focusIsUnclaimed } from "../lib/useFocusKeeper"; +} from "../../lib/issueDraft"; +import { moduleDef } from "../../lib/modules"; +import type { Page } from "../../lib/urlState"; +import { qk } from "../../lib/queryKeys"; +import { focusIsUnclaimed } from "../../lib/useFocusKeeper"; import { LEVEL_TAG, logClock } from "./logLineParts"; -import { Button } from "./ui/Button"; -import { IconButton } from "./ui/IconButton"; -import Modal, { ModalFooter } from "./ui/Modal"; -import { LoadingState, StateMessage } from "./ui/QueryState"; -import SegmentedToggle from "./ui/SegmentedToggle"; -import { Textarea, Input } from "./ui/Input"; -import { Checkbox } from "./ui/NativeControls"; -import { SURFACE } from "./ui/surfaces"; -import RemoteImage from "./ui/RemoteImage"; +import { Button } from "../../components/ui/Button"; +import { IconButton } from "../../components/ui/IconButton"; +import Modal, { ModalFooter } from "../../components/ui/Modal"; +import { LoadingState, StateMessage } from "../../components/ui/QueryState"; +import SegmentedToggle from "../../components/ui/SegmentedToggle"; +import { Textarea, Input } from "../../components/ui/Input"; +import { Checkbox } from "../../components/ui/NativeControls"; +import { SURFACE } from "../../components/ui/surfaces"; +import RemoteImage from "../../components/ui/RemoteImage"; // The issue composer: one dialog — the draft on the left (title, description, the // steps drafted from the session, the environment), what goes with it on the right (the two logs, diff --git a/frontend/src/components/LogLine.tsx b/frontend/src/features/diagnostics/LogLine.tsx similarity index 97% rename from frontend/src/components/LogLine.tsx rename to frontend/src/features/diagnostics/LogLine.tsx index 33b3c9ba..a6cf4712 100644 --- a/frontend/src/components/LogLine.tsx +++ b/frontend/src/features/diagnostics/LogLine.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from "react"; -import type { Level } from "../lib/log"; +import type { Level } from "../../lib/log"; import HistoryRow from "./HistoryRow"; import { LEVEL_TAG, logClock } from "./logLineParts"; diff --git a/frontend/src/components/logLineParts.ts b/frontend/src/features/diagnostics/logLineParts.ts similarity index 95% rename from frontend/src/components/logLineParts.ts rename to frontend/src/features/diagnostics/logLineParts.ts index e2a8f7b6..571e7a6d 100644 --- a/frontend/src/components/logLineParts.ts +++ b/frontend/src/features/diagnostics/logLineParts.ts @@ -1,4 +1,4 @@ -import type { Level } from "../lib/log"; +import type { Level } from "../../lib/log"; // What a log line is drawn with — the level tags and the clock — shared by LogLine (both log // viewers) and the issue composer's review list, so a level reads the same everywhere. Apart from diff --git a/frontend/src/components/useDebugRecording.ts b/frontend/src/features/diagnostics/useDebugRecording.ts similarity index 91% rename from frontend/src/components/useDebugRecording.ts rename to frontend/src/features/diagnostics/useDebugRecording.ts index 071ffcc1..c0a2b914 100644 --- a/frontend/src/components/useDebugRecording.ts +++ b/frontend/src/features/diagnostics/useDebugRecording.ts @@ -2,21 +2,21 @@ import { useCallback } from "react"; import { useIsMutating, useMutation, useQueryClient } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; -import { api, type DebugSession, type DebugSessionLevel } from "../lib/api"; -import { notifyActionError } from "../lib/actionErrors"; -import { captureTab } from "../lib/captureTab"; +import { api, type DebugSession, type DebugSessionLevel } from "../../lib/api"; +import { notifyActionError } from "../../lib/actionErrors"; +import { captureTab } from "../../lib/captureTab"; import { announceSessionChange, closeRecorderStart, nextStep, openComposer, setStepPrompt, -} from "../lib/debugRecorder"; -import { flushNow, liveSession } from "../lib/diagnostics"; -import { log } from "../lib/log"; -import { notify } from "../lib/notifications"; -import { qk } from "../lib/queryKeys"; -import { updateMeCache, useMe } from "../lib/useMe"; +} from "../../lib/debugRecorder"; +import { flushNow, liveSession } from "../../lib/diagnostics"; +import { log } from "../../lib/log"; +import { notify } from "../../lib/notifications"; +import { qk } from "../../lib/queryKeys"; +import { updateMeCache, useMe } from "../../lib/useMe"; /** The debug recording's actions, shared by everything that drives it: the pill, Settings → * Diagnosztika and the command palette. The session itself is `me.debug_session` — the server's diff --git a/frontend/src/components/DownloadButton.tsx b/frontend/src/features/downloads/DownloadButton.tsx similarity index 93% rename from frontend/src/components/DownloadButton.tsx rename to frontend/src/features/downloads/DownloadButton.tsx index f68d5f38..a2cbc10f 100644 --- a/frontend/src/components/DownloadButton.tsx +++ b/frontend/src/features/downloads/DownloadButton.tsx @@ -1,5 +1,5 @@ import { lazy, Suspense, useState } from "react"; -import { qk } from "../lib/queryKeys"; +import { qk } from "../../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useQuery } from "@tanstack/react-query"; import { Download } from "lucide-react"; @@ -7,8 +7,8 @@ import clsx from "clsx"; // Lazy: the download dialog (+ its profile editor) loads only when a card's download button is // clicked, so it never ships in the feed chunk for the majority who just browse. const DownloadDialog = lazy(() => import("./DownloadDialog")); -import { api } from "../lib/api"; -import { IconButton } from "./ui/IconButton"; +import { api } from "../../lib/api"; +import { IconButton } from "../../components/ui/IconButton"; // Self-contained download affordance for a video card / the player. Reflects the video's current state from // the shared per-user download index (one polled query shared across every card). diff --git a/frontend/src/components/DownloadDialog.dom.test.tsx b/frontend/src/features/downloads/DownloadDialog.dom.test.tsx similarity index 91% rename from frontend/src/components/DownloadDialog.dom.test.tsx rename to frontend/src/features/downloads/DownloadDialog.dom.test.tsx index 119e2100..27991fd3 100644 --- a/frontend/src/components/DownloadDialog.dom.test.tsx +++ b/frontend/src/features/downloads/DownloadDialog.dom.test.tsx @@ -7,7 +7,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useState } from "react"; -import { api, type DownloadProfile } from "../lib/api"; +import { api, type DownloadProfile } from "../../lib/api"; const h = vi.hoisted(() => ({ prefs: new Map(), @@ -19,7 +19,7 @@ vi.mock("react-i18next", async (importOriginal) => ({ ...(await importOriginal()), useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }), })); -vi.mock("../lib/usePref", () => ({ +vi.mock("../../lib/usePref", () => ({ usePref: (key: string, initial: T) => { const [value, setValue] = useState(() => h.prefs.has(key) ? (h.prefs.get(key) as T) : initial, @@ -33,11 +33,11 @@ vi.mock("../lib/usePref", () => ({ ]; }, })); -vi.mock("../lib/notifications", async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock("../../lib/notifications", async (importOriginal) => ({ + ...(await importOriginal()), notify: h.notify, })); -vi.mock("../lib/downloadQueue", () => ({ showInTray: h.showInTray })); +vi.mock("../../lib/downloadQueue", () => ({ showInTray: h.showInTray })); import DownloadDialog from "./DownloadDialog"; diff --git a/frontend/src/components/DownloadDialog.tsx b/frontend/src/features/downloads/DownloadDialog.tsx similarity index 84% rename from frontend/src/components/DownloadDialog.tsx rename to frontend/src/features/downloads/DownloadDialog.tsx index 4b7e7d2b..c680672d 100644 --- a/frontend/src/components/DownloadDialog.tsx +++ b/frontend/src/features/downloads/DownloadDialog.tsx @@ -1,22 +1,22 @@ import { lazy, Suspense, useState } from "react"; -import { qk } from "../lib/queryKeys"; +import { qk } from "../../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import Modal, { ModalFooter } from "./ui/Modal"; -import { Button } from "./ui/Button"; -import { api } from "../lib/api"; -import { invalidateDownloads } from "../lib/downloads"; +import Modal, { ModalFooter } from "../../components/ui/Modal"; +import { Button } from "../../components/ui/Button"; +import { api } from "../../lib/api"; +import { invalidateDownloads } from "../../lib/downloads"; // Lazy: the full profile editor only opens from the dialog's "manage presets" affordance. -const ProfileEditor = lazy(() => import("./ProfileEditor")); -import { notify } from "../lib/notifications"; -import { notifyActionError } from "../lib/actionErrors"; -import { showInTray } from "../lib/downloadQueue"; -import { profileName } from "../lib/vocab"; -import { LS } from "../lib/storage"; -import { usePref } from "../lib/usePref"; -import FilterSelect from "./ui/FilterSelect"; -import { Input } from "./ui/Input"; +const ProfileEditor = lazy(() => import("../../modules/settings/ProfileEditor")); +import { notify } from "../../lib/notifications"; +import { notifyActionError } from "../../lib/actionErrors"; +import { showInTray } from "../../lib/downloadQueue"; +import { profileName } from "../../lib/vocab"; +import { LS } from "../../lib/storage"; +import { usePref } from "../../lib/usePref"; +import FilterSelect from "../../components/ui/FilterSelect"; +import { Input } from "../../components/ui/Input"; // Preset picker shown when the user hits Download on a card / in the player, or brings a link (the // Files link box, a paste anywhere). Enqueues via the API; its notice's "View" opens the tray on it. diff --git a/frontend/src/components/DownloadIntake.dom.test.tsx b/frontend/src/features/downloads/DownloadIntake.dom.test.tsx similarity index 95% rename from frontend/src/components/DownloadIntake.dom.test.tsx rename to frontend/src/features/downloads/DownloadIntake.dom.test.tsx index 1ec1c64f..0147eabe 100644 --- a/frontend/src/components/DownloadIntake.dom.test.tsx +++ b/frontend/src/features/downloads/DownloadIntake.dom.test.tsx @@ -21,10 +21,10 @@ vi.mock("./DownloadDialog", () => ({ })); import DownloadIntake from "./DownloadIntake"; -import { LinkBox } from "./LinkBox"; -import { clearPendingDownload, requestLinkBox } from "../lib/downloadIntake"; -import { ensureNavId } from "../lib/history"; -import { pushLayer, removeLayer } from "../lib/layerStack"; +import { LinkBox } from "../../modules/files/LinkBox"; +import { clearPendingDownload, requestLinkBox } from "../../lib/downloadIntake"; +import { ensureNavId } from "../../lib/history"; +import { pushLayer, removeLayer } from "../../lib/layerStack"; afterEach(() => { cleanup(); diff --git a/frontend/src/components/DownloadIntake.tsx b/frontend/src/features/downloads/DownloadIntake.tsx similarity index 97% rename from frontend/src/components/DownloadIntake.tsx rename to frontend/src/features/downloads/DownloadIntake.tsx index 4297c9ae..abccd2b0 100644 --- a/frontend/src/components/DownloadIntake.tsx +++ b/frontend/src/features/downloads/DownloadIntake.tsx @@ -5,7 +5,7 @@ import { pasteIsForThePage, requestDownload, usePendingDownload, -} from "../lib/downloadIntake"; +} from "../../lib/downloadIntake"; const DownloadDialog = lazy(() => import("./DownloadDialog")); diff --git a/frontend/src/components/DownloadTray.dom.test.tsx b/frontend/src/features/downloads/DownloadTray.dom.test.tsx similarity index 94% rename from frontend/src/components/DownloadTray.dom.test.tsx rename to frontend/src/features/downloads/DownloadTray.dom.test.tsx index b9775866..69096443 100644 --- a/frontend/src/components/DownloadTray.dom.test.tsx +++ b/frontend/src/features/downloads/DownloadTray.dom.test.tsx @@ -8,7 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useState } from "react"; -import { api, type QueueJob } from "../lib/api"; +import { api, type QueueJob } from "../../lib/api"; const h = vi.hoisted(() => ({ jobs: [] as unknown[], @@ -28,7 +28,7 @@ vi.mock("react-i18next", async (importOriginal) => ({ `${Array.isArray(key) ? key[0] : key}${o?.count !== undefined ? `:${o.count}` : ""}${o?.title ? `:${o.title}` : ""}`, }), })); -vi.mock("../lib/useLiveQuery", () => ({ +vi.mock("../../lib/useLiveQuery", () => ({ useLiveQuery: (_key: unknown, _fn: unknown, opts: { intervalMs: (d: unknown) => number }) => ( (h.intervalMs = opts.intervalMs), { @@ -42,7 +42,7 @@ vi.mock("../lib/useLiveQuery", () => ({ })); // The prefs as state over a map that outlives a mount — a reload, as far as the tray can tell. How // usePref stores them is its own business (usePref.dom.test). -vi.mock("../lib/usePref", () => ({ +vi.mock("../../lib/usePref", () => ({ usePref: (key: string, initial: T) => { const [value, setValue] = useState(() => h.prefs.has(key) ? (h.prefs.get(key) as T) : initial, @@ -56,21 +56,23 @@ vi.mock("../lib/usePref", () => ({ ]; }, })); -vi.mock("./filesContext", () => ({ useFilesActions: () => ({ openFile: h.openFile }) })); -vi.mock("./confirmContext", () => ({ useConfirm: () => async () => true })); -vi.mock("../lib/downloads", async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock("../../modules/files/filesContext", () => ({ + useFilesActions: () => ({ openFile: h.openFile }), +})); +vi.mock("../../components/ui/confirmContext", () => ({ useConfirm: () => async () => true })); +vi.mock("../../lib/downloads", async (importOriginal) => ({ + ...(await importOriginal()), quickShareLink: h.share, })); -vi.mock("../lib/notifications", async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock("../../lib/notifications", async (importOriginal) => ({ + ...(await importOriginal()), notify: h.notify, })); import DownloadTray from "./DownloadTray"; -import { noteQueue, resetQueueWatch, showInTray } from "../lib/downloadQueue"; -import { pushLayer, removeLayer } from "../lib/layerStack"; -import { qk } from "../lib/queryKeys"; +import { noteQueue, resetQueueWatch, showInTray } from "../../lib/downloadQueue"; +import { pushLayer, removeLayer } from "../../lib/layerStack"; +import { qk } from "../../lib/queryKeys"; beforeEach(() => { resetQueueWatch(); diff --git a/frontend/src/components/DownloadTray.menu.dom.test.tsx b/frontend/src/features/downloads/DownloadTray.menu.dom.test.tsx similarity index 95% rename from frontend/src/components/DownloadTray.menu.dom.test.tsx rename to frontend/src/features/downloads/DownloadTray.menu.dom.test.tsx index a28411e3..7b1caecd 100644 --- a/frontend/src/components/DownloadTray.menu.dom.test.tsx +++ b/frontend/src/features/downloads/DownloadTray.menu.dom.test.tsx @@ -10,8 +10,8 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useState } from "react"; -import { api, type DownloadJob } from "../lib/api"; -import { menuGroups } from "./ui/menuGroups"; +import { api, type DownloadJob } from "../../lib/api"; +import { menuGroups } from "../../components/ui/menuGroups"; const mocks = vi.hoisted(() => ({ jobs: [] as unknown[], @@ -28,8 +28,8 @@ vi.mock("react-i18next", async (importOriginal) => { useTranslation: () => ({ i18n: { language: "en" }, t: (key: string) => key }), }; }); -vi.mock("./confirmContext", () => ({ useConfirm: () => mocks.confirm })); -vi.mock("../lib/useLiveQuery", () => ({ +vi.mock("../../components/ui/confirmContext", () => ({ useConfirm: () => mocks.confirm })); +vi.mock("../../lib/useLiveQuery", () => ({ useLiveQuery: (key: readonly unknown[]) => ({ data: key[0] === "downloads" ? mocks.jobs : undefined, isError: false, @@ -37,10 +37,12 @@ vi.mock("../lib/useLiveQuery", () => ({ refetch: vi.fn(), }), })); -vi.mock("../lib/usePref", () => ({ +vi.mock("../../lib/usePref", () => ({ usePref: (_key: string, initial: T) => useState(initial), })); -vi.mock("./filesContext", () => ({ useFilesActions: () => ({ openFile: vi.fn() }) })); +vi.mock("../../modules/files/filesContext", () => ({ + useFilesActions: () => ({ openFile: vi.fn() }), +})); globalThis.ResizeObserver ??= class { observe() {} diff --git a/frontend/src/components/DownloadTray.tsx b/frontend/src/features/downloads/DownloadTray.tsx similarity index 91% rename from frontend/src/components/DownloadTray.tsx rename to frontend/src/features/downloads/DownloadTray.tsx index bee55acc..abf825b0 100644 --- a/frontend/src/components/DownloadTray.tsx +++ b/frontend/src/features/downloads/DownloadTray.tsx @@ -13,9 +13,9 @@ import { RotateCw, X, } from "lucide-react"; -import type { QueueJob } from "../lib/api"; -import { invalidateDownloads, quickShareLink } from "../lib/downloads"; -import { failCopyKeys, failTitleKey } from "../lib/downloadFailure"; +import type { QueueJob } from "../../lib/api"; +import { invalidateDownloads, quickShareLink } from "../../lib/downloads"; +import { failCopyKeys, failTitleKey } from "../../lib/downloadFailure"; import { IN_FLIGHT, DOWNLOAD_PHASES, @@ -27,31 +27,31 @@ import { useFinished, useRevealing, useUnannounced, -} from "../lib/downloadQueue"; -import { anyBlockingLayer } from "../lib/layerStack"; -import { focusNavItem, navItemAttrs } from "../lib/listNav"; -import { notify } from "../lib/notifications"; -import { qk } from "../lib/queryKeys"; -import { LS } from "../lib/storage"; -import { useLiveQuery } from "../lib/useLiveQuery"; -import { usePref } from "../lib/usePref"; +} from "../../lib/downloadQueue"; +import { anyBlockingLayer } from "../../lib/layerStack"; +import { focusNavItem, navItemAttrs } from "../../lib/listNav"; +import { notify } from "../../lib/notifications"; +import { qk } from "../../lib/queryKeys"; +import { LS } from "../../lib/storage"; +import { useLiveQuery } from "../../lib/useLiveQuery"; +import { usePref } from "../../lib/usePref"; import { ErrorModal } from "./downloadModals"; import { jobTitle } from "./downloadTitle"; -import { sourceLabel } from "./fileFolders"; -import { useFilesActions } from "./filesContext"; +import { sourceLabel } from "../../modules/files/fileFolders"; +import { useFilesActions } from "../../modules/files/filesContext"; import { useDownloadActions } from "./useDownloadActions"; -import { type ActionMenuItem } from "./ui/ActionMenu"; -import { setCornerLift } from "./ui/backToTopState"; -import { Button } from "./ui/Button"; -import { ContextMenu } from "./ui/ContextMenu"; -import { IconButton } from "./ui/IconButton"; -import { menuGroups } from "./ui/menuGroups"; -import { MoreButton } from "./ui/MoreButton"; -import NavGrid from "./ui/NavGrid"; -import { Pressable } from "./ui/Pressable"; -import { CONTENT_ID } from "./ui/landmarks"; -import { SURFACE } from "./ui/surfaces"; -import { useContextMenu } from "./ui/useContextMenu"; +import { type ActionMenuItem } from "../../components/ui/ActionMenu"; +import { setCornerLift } from "../../components/ui/backToTopState"; +import { Button } from "../../components/ui/Button"; +import { ContextMenu } from "../../components/ui/ContextMenu"; +import { IconButton } from "../../components/ui/IconButton"; +import { menuGroups } from "../../components/ui/menuGroups"; +import { MoreButton } from "../../components/ui/MoreButton"; +import NavGrid from "../../components/ui/NavGrid"; +import { Pressable } from "../../components/ui/Pressable"; +import { CONTENT_ID } from "../../components/ui/landmarks"; +import { SURFACE } from "../../components/ui/surfaces"; +import { useContextMenu } from "../../components/ui/useContextMenu"; // The running downloads, app-wide (DESIGN.md "Running downloads"): a tray in the bottom-right corner, // up only while something waits, runs or failed — and what finished in this tab, "Done · Open", diff --git a/frontend/src/components/downloadModals.error.dom.test.tsx b/frontend/src/features/downloads/downloadModals.error.dom.test.tsx similarity index 96% rename from frontend/src/components/downloadModals.error.dom.test.tsx rename to frontend/src/features/downloads/downloadModals.error.dom.test.tsx index 91873dce..20654c66 100644 --- a/frontend/src/components/downloadModals.error.dom.test.tsx +++ b/frontend/src/features/downloads/downloadModals.error.dom.test.tsx @@ -4,7 +4,7 @@ // downloader did not understand (an update on the server cures it). Never for an unclassified one. import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import type { DownloadJob } from "../lib/api"; +import type { DownloadJob } from "../../lib/api"; import { ErrorModal } from "./downloadModals"; vi.mock("react-i18next", async (importOriginal) => ({ diff --git a/frontend/src/components/downloadModals.tsx b/frontend/src/features/downloads/downloadModals.tsx similarity index 94% rename from frontend/src/components/downloadModals.tsx rename to frontend/src/features/downloads/downloadModals.tsx index 5f901257..97deb656 100644 --- a/frontend/src/components/downloadModals.tsx +++ b/frontend/src/features/downloads/downloadModals.tsx @@ -1,14 +1,14 @@ import { useState } from "react"; -import { invalidateDownloads } from "../lib/downloads"; +import { invalidateDownloads } from "../../lib/downloads"; import { useTranslation } from "react-i18next"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Plus, Trash2 } from "lucide-react"; -import Modal, { ModalFooter } from "./ui/Modal"; -import { api, type DownloadJob } from "../lib/api"; -import { Button } from "./ui/Button"; +import Modal, { ModalFooter } from "../../components/ui/Modal"; +import { api, type DownloadJob } from "../../lib/api"; +import { Button } from "../../components/ui/Button"; import { IconBtn } from "./downloadRow"; -import { failCopyKeys, failTitleKey } from "../lib/downloadFailure"; -import { Input } from "./ui/Input"; +import { failCopyKeys, failTitleKey } from "../../lib/downloadFailure"; +import { Input } from "../../components/ui/Input"; // Per-item user modals (self-contained, prop-driven): a library file's details (Files) and a failed // download's reasons (the tray). diff --git a/frontend/src/components/downloadRow.dom.test.tsx b/frontend/src/features/downloads/downloadRow.dom.test.tsx similarity index 98% rename from frontend/src/components/downloadRow.dom.test.tsx rename to frontend/src/features/downloads/downloadRow.dom.test.tsx index f7b17dec..47e7b335 100644 --- a/frontend/src/components/downloadRow.dom.test.tsx +++ b/frontend/src/features/downloads/downloadRow.dom.test.tsx @@ -14,7 +14,7 @@ vi.mock("react-i18next", async (importOriginal) => { }); import { JobThumb } from "./downloadRow"; -import type { DownloadJob } from "../lib/api"; +import type { DownloadJob } from "../../lib/api"; const baseJob: DownloadJob = { id: 1, diff --git a/frontend/src/components/downloadRow.tsx b/frontend/src/features/downloads/downloadRow.tsx similarity index 95% rename from frontend/src/components/downloadRow.tsx rename to frontend/src/features/downloads/downloadRow.tsx index b9969c2f..97685ec7 100644 --- a/frontend/src/components/downloadRow.tsx +++ b/frontend/src/features/downloads/downloadRow.tsx @@ -2,11 +2,11 @@ import type { ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { File, Film, Music } from "lucide-react"; import clsx from "clsx"; -import { DOWNLOAD_PHASES } from "../lib/downloadQueue"; -import { api, type DownloadJob } from "../lib/api"; -import { IconButton } from "./ui/IconButton"; -import { MediaProgress } from "./ui/MediaCard"; -import RemoteImage from "./ui/RemoteImage"; +import { DOWNLOAD_PHASES } from "../../lib/downloadQueue"; +import { api, type DownloadJob } from "../../lib/api"; +import { IconButton } from "../../components/ui/IconButton"; +import { MediaProgress } from "../../components/ui/MediaCard"; +import RemoteImage from "../../components/ui/RemoteImage"; // The small shared primitives of a download (status pill, icon button, the job's picture), all purely // prop-driven: the admin Storage tab shows the status, the Files list and grid the picture, the diff --git a/frontend/src/components/downloadTitle.ts b/frontend/src/features/downloads/downloadTitle.ts similarity index 94% rename from frontend/src/components/downloadTitle.ts rename to frontend/src/features/downloads/downloadTitle.ts index df212595..0246725f 100644 --- a/frontend/src/components/downloadTitle.ts +++ b/frontend/src/features/downloads/downloadTitle.ts @@ -1,4 +1,4 @@ -import type { DownloadJob } from "../lib/api"; +import type { DownloadJob } from "../../lib/api"; // A download job's display title (a user rename wins over the extracted asset title), shared by the // tray, the Files list, the admin Storage tab and the library player. diff --git a/frontend/src/components/useDownloadActions.tsx b/frontend/src/features/downloads/useDownloadActions.tsx similarity index 91% rename from frontend/src/components/useDownloadActions.tsx rename to frontend/src/features/downloads/useDownloadActions.tsx index c7e62e56..436f3b95 100644 --- a/frontend/src/components/useDownloadActions.tsx +++ b/frontend/src/features/downloads/useDownloadActions.tsx @@ -1,14 +1,14 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; import { Ban, Pause, Play, RefreshCw } from "lucide-react"; -import { api, type DownloadJob, type DownloadJobKind } from "../lib/api"; -import { notifyActionError } from "../lib/actionErrors"; -import { invalidateDownloads } from "../lib/downloads"; -import { useConfirm } from "./confirmContext"; -import { useCopyItems } from "./copyItems"; +import { api, type DownloadJob, type DownloadJobKind } from "../../lib/api"; +import { notifyActionError } from "../../lib/actionErrors"; +import { invalidateDownloads } from "../../lib/downloads"; +import { useConfirm } from "../../components/ui/confirmContext"; +import { useCopyItems } from "../../components/ui/copyItems"; import { jobTitle } from "./downloadTitle"; -import { type ActionMenuItem } from "./ui/ActionMenu"; -import { menuGroups } from "./ui/menuGroups"; +import { type ActionMenuItem } from "../../components/ui/ActionMenu"; +import { menuGroups } from "../../components/ui/menuGroups"; // A queued download's ONE action list: the row's "…", its right-click, long-press and Shift+F10. The // row keeps its primary action (pause / resume / retry) and its state (the error and its details) in diff --git a/frontend/src/components/ActiveFilterChips.tsx b/frontend/src/features/filters/ActiveFilterChips.tsx similarity index 92% rename from frontend/src/components/ActiveFilterChips.tsx rename to frontend/src/features/filters/ActiveFilterChips.tsx index 15aeb6b7..34e203e5 100644 --- a/frontend/src/components/ActiveFilterChips.tsx +++ b/frontend/src/features/filters/ActiveFilterChips.tsx @@ -1,16 +1,16 @@ import { useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Filter, X } from "lucide-react"; -import type { ActiveFilter } from "../lib/useActiveFilters"; -import { decideChipLayout } from "../lib/chipOverflow"; -import { chipLaneClass, useChipOverflowFit } from "../lib/useChipOverflowFit"; -import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover"; -import { useAnchoredPopover } from "./ui/anchoredPopoverHooks"; -import { Button } from "./ui/Button"; -import { Chip } from "./ui/Chip"; -import { chipClass } from "./ui/chipStyles"; -import { ChipLabelPending } from "./ui/Skeleton"; -import { SURFACE } from "./ui/surfaces"; +import type { ActiveFilter } from "../../lib/useActiveFilters"; +import { decideChipLayout } from "../../lib/chipOverflow"; +import { chipLaneClass, useChipOverflowFit } from "../../lib/useChipOverflowFit"; +import { PopoverItem, PopoverSurface } from "../../components/ui/AnchoredPopover"; +import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks"; +import { Button } from "../../components/ui/Button"; +import { Chip } from "../../components/ui/Chip"; +import { chipClass } from "../../components/ui/chipStyles"; +import { ChipLabelPending } from "../../components/ui/Skeleton"; +import { SURFACE } from "../../components/ui/surfaces"; // The applied-filter row above the feed: every narrowing currently in force, each removable on its // own. It exists because the filter panel now sits collapsed as a tab — without this you'd have to diff --git a/frontend/src/components/PersonalViewsModal.dom.test.tsx b/frontend/src/features/filters/PersonalViewsModal.dom.test.tsx similarity index 98% rename from frontend/src/components/PersonalViewsModal.dom.test.tsx rename to frontend/src/features/filters/PersonalViewsModal.dom.test.tsx index fa944ae5..7180e1bd 100644 --- a/frontend/src/components/PersonalViewsModal.dom.test.tsx +++ b/frontend/src/features/filters/PersonalViewsModal.dom.test.tsx @@ -63,7 +63,7 @@ const CTX = { toggleDefault: vi.fn(async () => {}), }; -vi.mock("./savedViewsContext", () => ({ +vi.mock("../../modules/feed/savedViewsContext", () => ({ useSavedViews: () => ({ ...CTX, activeViewId: h.activeViewId, @@ -80,8 +80,8 @@ vi.mock("./savedViewsContext", () => ({ })); import PersonalViewsModal from "./PersonalViewsModal"; -import { api } from "../lib/api"; -import { qk } from "../lib/queryKeys"; +import { api } from "../../lib/api"; +import { qk } from "../../lib/queryKeys"; function mount() { const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); diff --git a/frontend/src/components/PersonalViewsModal.tsx b/frontend/src/features/filters/PersonalViewsModal.tsx similarity index 93% rename from frontend/src/components/PersonalViewsModal.tsx rename to frontend/src/features/filters/PersonalViewsModal.tsx index 8976ee66..e1e5e52a 100644 --- a/frontend/src/components/PersonalViewsModal.tsx +++ b/frontend/src/features/filters/PersonalViewsModal.tsx @@ -17,21 +17,26 @@ import { verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { coercePlexFilters, type FeedFilters, type PlexFilters, type SavedView } from "../lib/api"; -import Modal from "./ui/Modal"; -import { Button } from "./ui/Button"; -import { IconButton } from "./ui/IconButton"; -import { useSavedViews, asFilters } from "./savedViewsContext"; -import { useActiveFilters } from "../lib/useActiveFilters"; -import { buildPlexActiveFilters } from "./usePlexActiveFilters"; -import { useUndoable } from "../lib/useUndoable"; -import UndoToolbar from "./ui/UndoToolbar"; -import { useSortableA11y, useSortableItem } from "./ui/sortableA11y"; -import { DragHandle } from "./ui/DragHandle"; -import { OrderButtons } from "./ui/OrderButtons"; -import { Pressable } from "./ui/Pressable"; -import { DismissibleChip } from "./ui/Chip"; -import { Input } from "./ui/Input"; +import { + coercePlexFilters, + type FeedFilters, + type PlexFilters, + type SavedView, +} from "../../lib/api"; +import Modal from "../../components/ui/Modal"; +import { Button } from "../../components/ui/Button"; +import { IconButton } from "../../components/ui/IconButton"; +import { useSavedViews, asFilters } from "../../modules/feed/savedViewsContext"; +import { useActiveFilters } from "../../lib/useActiveFilters"; +import { buildPlexActiveFilters } from "../../modules/plex/usePlexActiveFilters"; +import { useUndoable } from "../../lib/useUndoable"; +import UndoToolbar from "../../components/ui/UndoToolbar"; +import { useSortableA11y, useSortableItem } from "../../components/ui/sortableA11y"; +import { DragHandle } from "../../components/ui/DragHandle"; +import { OrderButtons } from "../../components/ui/OrderButtons"; +import { Pressable } from "../../components/ui/Pressable"; +import { DismissibleChip } from "../../components/ui/Chip"; +import { Input } from "../../components/ui/Input"; // Master–detail manager for saved ("personal") views: the left column lists the views (drag to // reorder, or the selected one's order buttons on the right) plus a "save the current filters" input; diff --git a/frontend/src/components/SavedViewsPopover.dom.test.tsx b/frontend/src/features/filters/SavedViewsPopover.dom.test.tsx similarity index 98% rename from frontend/src/components/SavedViewsPopover.dom.test.tsx rename to frontend/src/features/filters/SavedViewsPopover.dom.test.tsx index 97bf282b..81c76217 100644 --- a/frontend/src/components/SavedViewsPopover.dom.test.tsx +++ b/frontend/src/features/filters/SavedViewsPopover.dom.test.tsx @@ -14,7 +14,7 @@ vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (key: string) => key }) }; }); -vi.mock("./savedViewsContext", () => ({ +vi.mock("../../modules/feed/savedViewsContext", () => ({ useSavedViews: () => ({ views: [], activeViewId: null, apply: vi.fn(), openModal }), })); diff --git a/frontend/src/components/SavedViewsPopover.tsx b/frontend/src/features/filters/SavedViewsPopover.tsx similarity index 87% rename from frontend/src/components/SavedViewsPopover.tsx rename to frontend/src/features/filters/SavedViewsPopover.tsx index 0ba58e3c..8d04147b 100644 --- a/frontend/src/components/SavedViewsPopover.tsx +++ b/frontend/src/features/filters/SavedViewsPopover.tsx @@ -1,13 +1,13 @@ import { useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { Settings2 } from "lucide-react"; -import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover"; -import { closeThenAct, useAnchoredPopover } from "./ui/anchoredPopoverHooks"; -import { useSavedViews } from "./savedViewsContext"; -import { IconButton } from "./ui/IconButton"; -import { MenuCheck } from "./ui/MenuCheck"; -import { SURFACE } from "./ui/surfaces"; -import { SelectTrigger } from "./ui/SelectTrigger"; +import { PopoverItem, PopoverSurface } from "../../components/ui/AnchoredPopover"; +import { closeThenAct, useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks"; +import { useSavedViews } from "../../modules/feed/savedViewsContext"; +import { IconButton } from "../../components/ui/IconButton"; +import { MenuCheck } from "../../components/ui/MenuCheck"; +import { SURFACE } from "../../components/ui/surfaces"; +import { SelectTrigger } from "../../components/ui/SelectTrigger"; // The saved-views quick-picker. The filter-panel header's icon (and the toolbar's view chip) use it as // their trigger: click → a popover list of the user's saved views (apply on click, the active one diff --git a/frontend/src/components/SelectedViewChip.tsx b/frontend/src/features/filters/SelectedViewChip.tsx similarity index 94% rename from frontend/src/components/SelectedViewChip.tsx rename to frontend/src/features/filters/SelectedViewChip.tsx index 4f8ba066..dd89be80 100644 --- a/frontend/src/components/SelectedViewChip.tsx +++ b/frontend/src/features/filters/SelectedViewChip.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { Bookmark } from "lucide-react"; import SavedViewsPopover from "./SavedViewsPopover"; -import { useSavedViews } from "./savedViewsContext"; +import { useSavedViews } from "../../modules/feed/savedViewsContext"; // The active personal view, shown as the leading pill before the active-filter chip row. Styled like // the toolbar's FilterSelect pills; a warn-coloured dot appears when the live filters have diverged diff --git a/frontend/src/components/PaletteLauncher.dom.test.tsx b/frontend/src/features/header/PaletteLauncher.dom.test.tsx similarity index 83% rename from frontend/src/components/PaletteLauncher.dom.test.tsx rename to frontend/src/features/header/PaletteLauncher.dom.test.tsx index 928060b2..24649211 100644 --- a/frontend/src/components/PaletteLauncher.dom.test.tsx +++ b/frontend/src/features/header/PaletteLauncher.dom.test.tsx @@ -11,24 +11,24 @@ vi.mock("react-i18next", async (importOriginal) => ({ })); let page = "home"; -vi.mock("./navigationContext", () => ({ useNavigation: () => ({ page }) })); -vi.mock("./feedFiltersContext", () => ({ +vi.mock("../navigation/navigationContext", () => ({ useNavigation: () => ({ page }) })); +vi.mock("../../modules/feed/feedFiltersContext", () => ({ useFeedFilters: () => ({ q: "", sort: "newest" }), useFeedFiltersActions: () => ({ setFilters: vi.fn() }), })); -vi.mock("./playlistsContext", () => ({ +vi.mock("../../modules/playlists/playlistsContext", () => ({ usePlaylists: () => ({ search: "" }), usePlaylistsActions: () => ({ setSearch: vi.fn() }), })); -vi.mock("./channelsContext", () => ({ +vi.mock("../../modules/channels/channelsContext", () => ({ useChannels: () => ({ search: "" }), useChannelsActions: () => ({ setSearch: vi.fn(), goToFullHistory: vi.fn() }), })); -vi.mock("./SyncStatus", () => ({ default: () => null })); +vi.mock("../../modules/channels/SyncStatus", () => ({ default: () => null })); -import Header from "./Header"; -import { closePalette, usePaletteOpen } from "../lib/keymap"; -import type { Me } from "../lib/api"; +import Header from "../../shell/Header"; +import { closePalette, usePaletteOpen } from "../../lib/keymap"; +import type { Me } from "../../lib/api"; afterEach(() => { cleanup(); diff --git a/frontend/src/components/PaletteLauncher.tsx b/frontend/src/features/header/PaletteLauncher.tsx similarity index 93% rename from frontend/src/components/PaletteLauncher.tsx rename to frontend/src/features/header/PaletteLauncher.tsx index b486e42a..a6c4e804 100644 --- a/frontend/src/components/PaletteLauncher.tsx +++ b/frontend/src/features/header/PaletteLauncher.tsx @@ -1,10 +1,10 @@ import { useTranslation } from "react-i18next"; import { Command } from "lucide-react"; -import { SHORTCUTS, formatChord, openPalette } from "../lib/keymap"; -import { Pressable } from "./ui/Pressable"; +import { SHORTCUTS, formatChord, openPalette } from "../../lib/keymap"; +import { Pressable } from "../../components/ui/Pressable"; import clsx from "clsx"; -import { fieldClass } from "./ui/fieldStyles"; +import { fieldClass } from "../../components/ui/fieldStyles"; // The visible way into the command palette (and, from there, the shortcut sheet). Ctrl/Cmd+K and // `h` had no entry point anywhere but a palette row — the one in-app pointer to the cheat sheet sat diff --git a/frontend/src/components/SearchBar.dom.test.tsx b/frontend/src/features/header/SearchBar.dom.test.tsx similarity index 100% rename from frontend/src/components/SearchBar.dom.test.tsx rename to frontend/src/features/header/SearchBar.dom.test.tsx diff --git a/frontend/src/components/SearchBar.keys.dom.test.tsx b/frontend/src/features/header/SearchBar.keys.dom.test.tsx similarity index 95% rename from frontend/src/components/SearchBar.keys.dom.test.tsx rename to frontend/src/features/header/SearchBar.keys.dom.test.tsx index 1a1b5914..6b88743b 100644 --- a/frontend/src/components/SearchBar.keys.dom.test.tsx +++ b/frontend/src/features/header/SearchBar.keys.dom.test.tsx @@ -5,9 +5,9 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { act, cleanup, render, screen } from "@testing-library/react"; import SearchBar from "./SearchBar"; -import KeyboardHelp from "./ui/KeyboardHelp"; -import { closeKeyboardHelp, toggleKeyboardHelp } from "../lib/keymap"; -import { pushLayer, removeLayer } from "../lib/layerStack"; +import KeyboardHelp from "../../components/ui/KeyboardHelp"; +import { closeKeyboardHelp, toggleKeyboardHelp } from "../../lib/keymap"; +import { pushLayer, removeLayer } from "../../lib/layerStack"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/features/header/SearchBar.tsx similarity index 95% rename from frontend/src/components/SearchBar.tsx rename to frontend/src/features/header/SearchBar.tsx index c617b389..3a295325 100644 --- a/frontend/src/components/SearchBar.tsx +++ b/frontend/src/features/header/SearchBar.tsx @@ -8,12 +8,12 @@ import { } from "react"; import { useTranslation } from "react-i18next"; import { Search, X } from "lucide-react"; -import { useSearchFocusKey } from "../lib/keymap"; +import { useSearchFocusKey } from "../../lib/keymap"; import { PaletteKeycap } from "./PaletteLauncher"; -import { Button } from "./ui/Button"; -import { IconButton } from "./ui/IconButton"; -import { InnerInput } from "./ui/Input"; -import { fieldClass } from "./ui/fieldStyles"; +import { Button } from "../../components/ui/Button"; +import { IconButton } from "../../components/ui/IconButton"; +import { InnerInput } from "../../components/ui/Input"; +import { fieldClass } from "../../components/ui/fieldStyles"; // The floating search pill in the top header. One component for every module that searches // (feed, Plex, channels, playlists): a 40px glass pill with the A4 field edge (the page's one hero diff --git a/frontend/src/components/NavigationProvider.dom.test.tsx b/frontend/src/features/navigation/NavigationProvider.dom.test.tsx similarity index 98% rename from frontend/src/components/NavigationProvider.dom.test.tsx rename to frontend/src/features/navigation/NavigationProvider.dom.test.tsx index 61b6b5ea..55684824 100644 --- a/frontend/src/components/NavigationProvider.dom.test.tsx +++ b/frontend/src/features/navigation/NavigationProvider.dom.test.tsx @@ -27,12 +27,12 @@ vi.mock("react-i18next", async (importOriginal) => { import { NavigationProvider } from "./NavigationProvider"; import { useNavigation, useNavigationActions } from "./navigationContext"; -import { useBackToClose } from "../lib/history"; -import { PageHeading } from "./ui/Headings"; +import { useBackToClose } from "../../lib/history"; +import { PageHeading } from "../../components/ui/Headings"; -import { qk } from "../lib/queryKeys"; -import { claimLocalData, getAccountRaw, LS, setAccountRaw } from "../lib/storage"; -import { api } from "../lib/api"; +import { qk } from "../../lib/queryKeys"; +import { claimLocalData, getAccountRaw, LS, setAccountRaw } from "../../lib/storage"; +import { api } from "../../lib/api"; import { PAGE_KEY } from "./navigationInit"; /** The Navigation API's view of the tab: `below` = whether each entry under the current one belongs diff --git a/frontend/src/components/NavigationProvider.tsx b/frontend/src/features/navigation/NavigationProvider.tsx similarity index 96% rename from frontend/src/components/NavigationProvider.tsx rename to frontend/src/features/navigation/NavigationProvider.tsx index 8075b0ab..439e433c 100644 --- a/frontend/src/components/NavigationProvider.tsx +++ b/frontend/src/features/navigation/NavigationProvider.tsx @@ -1,20 +1,20 @@ import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; -import { useLatest } from "../lib/useLatest"; +import { useLatest } from "../../lib/useLatest"; import { useTranslation } from "react-i18next"; -import { useMe } from "../lib/useMe"; -import { resolvePage, retiredParams, urlWithParams, type Page } from "../lib/urlState"; -import { ACTIVITY_KEYS } from "../lib/activityUrlState"; -import { pageTitleKey } from "../lib/pageMeta"; -import { useDocumentObjectTitle } from "../lib/documentTitle"; -import { getAccountRaw, setAccountRaw } from "../lib/storage"; +import { useMe } from "../../lib/useMe"; +import { resolvePage, retiredParams, urlWithParams, type Page } from "../../lib/urlState"; +import { ACTIVITY_KEYS } from "../../lib/activityUrlState"; +import { pageTitleKey } from "../../lib/pageMeta"; +import { useDocumentObjectTitle } from "../../lib/documentTitle"; +import { getAccountRaw, setAccountRaw } from "../../lib/storage"; import { backStaysInDocument, closeOverlaysThenNavigate, nextNavId, notifyNav, rememberNavPosition, -} from "../lib/history"; -import { reachablePage } from "../lib/modules"; +} from "../../lib/history"; +import { reachablePage } from "../../lib/modules"; import { channelViewFrom, loadInitialPage, @@ -23,7 +23,7 @@ import { START_ON_HOME_PREF, type ChannelView, } from "./navigationInit"; -import { usePref } from "../lib/usePref"; +import { usePref } from "../../lib/usePref"; import { NavigationActionsContext, NavigationStateContext, diff --git a/frontend/src/components/navigation.dom.test.ts b/frontend/src/features/navigation/navigation.dom.test.ts similarity index 96% rename from frontend/src/components/navigation.dom.test.ts rename to frontend/src/features/navigation/navigation.dom.test.ts index 721adbe7..4470b574 100644 --- a/frontend/src/components/navigation.dom.test.ts +++ b/frontend/src/features/navigation/navigation.dom.test.ts @@ -11,13 +11,13 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; const { stored } = vi.hoisted(() => ({ stored: {} as Record })); -vi.mock("../lib/storage", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("../../lib/storage", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, getAccountRaw: (k: string) => stored[k] ?? null }; }); import { loadInitialPage, channelViewFrom } from "./navigationInit"; -import { LS } from "../lib/storage"; +import { LS } from "../../lib/storage"; const setUrl = (search: string) => window.history.replaceState(null, "", search || "/"); diff --git a/frontend/src/components/navigationContext.ts b/frontend/src/features/navigation/navigationContext.ts similarity index 98% rename from frontend/src/components/navigationContext.ts rename to frontend/src/features/navigation/navigationContext.ts index 5c3b1684..b033ae96 100644 --- a/frontend/src/components/navigationContext.ts +++ b/frontend/src/features/navigation/navigationContext.ts @@ -1,5 +1,5 @@ import { createContext, useContext } from "react"; -import type { Page } from "../lib/urlState"; +import type { Page } from "../../lib/urlState"; import type { ChannelView } from "./navigationInit"; // The navigation contexts + their hooks, split out of NavigationProvider so that file exports only diff --git a/frontend/src/components/navigationInit.ts b/frontend/src/features/navigation/navigationInit.ts similarity index 95% rename from frontend/src/components/navigationInit.ts rename to frontend/src/features/navigation/navigationInit.ts index d820940e..6473857c 100644 --- a/frontend/src/components/navigationInit.ts +++ b/frontend/src/features/navigation/navigationInit.ts @@ -2,9 +2,9 @@ // provider file keeps react-refresh happy (component-only exports). `loadInitialPage` picks the landing // module from a bookmark's params; `channelViewFrom` is the shared _chan/?chan resolver behind BOTH the // channelView seed and the onPop handler. -import { hasFilterParams, readPage, resolvePage, type Page } from "../lib/urlState"; -import { getAccountRaw, LS } from "../lib/storage"; -import type { UsePrefOptions } from "../lib/usePref"; +import { hasFilterParams, readPage, resolvePage, type Page } from "../../lib/urlState"; +import { getAccountRaw, LS } from "../../lib/storage"; +import type { UsePrefOptions } from "../../lib/usePref"; export const PAGE_KEY = LS.page; /** The landing preference — start on Home, or on the module left open — an account preference diff --git a/frontend/src/components/ClipPanel.dom.test.tsx b/frontend/src/features/player/ClipPanel.dom.test.tsx similarity index 94% rename from frontend/src/components/ClipPanel.dom.test.tsx rename to frontend/src/features/player/ClipPanel.dom.test.tsx index 0665b4cb..5fd20636 100644 --- a/frontend/src/components/ClipPanel.dom.test.tsx +++ b/frontend/src/features/player/ClipPanel.dom.test.tsx @@ -7,8 +7,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useState } from "react"; -import type { ClipRequest } from "../lib/api"; -import type { ClipRange } from "../lib/clipRange"; +import type { ClipRequest } from "../../lib/api"; +import type { ClipRange } from "../../lib/clipRange"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); @@ -16,20 +16,20 @@ vi.mock("react-i18next", async (importOriginal) => { }); const enqueueClip = vi.fn<(body: ClipRequest) => Promise>(); -vi.mock("../lib/api", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("../../lib/api", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, api: { ...actual.api, enqueueClip: (b: ClipRequest) => enqueueClip(b) } }; }); const notify = vi.fn(); -vi.mock("../lib/notifications", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("../../lib/notifications", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, notify: (n: unknown) => notify(n) }; }); const showInTray = vi.fn(); -vi.mock("../lib/downloadQueue", () => ({ showInTray: (id: number) => showInTray(id) })); +vi.mock("../../lib/downloadQueue", () => ({ showInTray: (id: number) => showInTray(id) })); const notifyActionError = vi.fn(); -vi.mock("../lib/actionErrors", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("../../lib/actionErrors", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, notifyActionError: (e: unknown, t: string) => notifyActionError(e, t) }; }); diff --git a/frontend/src/components/ClipPanel.tsx b/frontend/src/features/player/ClipPanel.tsx similarity index 93% rename from frontend/src/components/ClipPanel.tsx rename to frontend/src/features/player/ClipPanel.tsx index f92194be..9e1ee6cd 100644 --- a/frontend/src/components/ClipPanel.tsx +++ b/frontend/src/features/player/ClipPanel.tsx @@ -2,24 +2,24 @@ import { useEffect, useEffectEvent, useId, useRef, useState, type SubmitEvent } import { useTranslation } from "react-i18next"; import { useQueryClient } from "@tanstack/react-query"; import { Crop, Crosshair, Maximize, Minus, Plus, Repeat, Scissors, X } from "lucide-react"; -import { api, type ClipRequest } from "../lib/api"; -import { DISPLAY_NAME_MAX } from "../lib/api/downloads"; -import { invalidateDownloads } from "../lib/downloads"; -import { notify } from "../lib/notifications"; -import { notifyActionError } from "../lib/actionErrors"; -import { showInTray } from "../lib/downloadQueue"; -import { log } from "../lib/log"; -import { formatTimecode } from "../lib/timecode"; -import { handleBounds, markIn, markOut, zoomWindow, type ClipRange } from "../lib/clipRange"; -import { fitRatio, fracRatio, WHOLE, type Frac } from "../lib/cropGeometry"; -import { RangeTrack, RulerTicks, type RangeWindow } from "./ui/RangeTrack"; -import { CropFrame } from "./ui/CropFrame"; -import SegmentedToggle from "./ui/SegmentedToggle"; -import { Button } from "./ui/Button"; -import { IconButton } from "./ui/IconButton"; -import { Input } from "./ui/Input"; -import { TimecodeError, TimecodeField } from "./ui/TimecodeField"; -import { useTimecodeField, type TimecodeDraft } from "./ui/useTimecodeField"; +import { api, type ClipRequest } from "../../lib/api"; +import { DISPLAY_NAME_MAX } from "../../lib/api/downloads"; +import { invalidateDownloads } from "../../lib/downloads"; +import { notify } from "../../lib/notifications"; +import { notifyActionError } from "../../lib/actionErrors"; +import { showInTray } from "../../lib/downloadQueue"; +import { log } from "../../lib/log"; +import { formatTimecode } from "../../lib/timecode"; +import { handleBounds, markIn, markOut, zoomWindow, type ClipRange } from "../../lib/clipRange"; +import { fitRatio, fracRatio, WHOLE, type Frac } from "../../lib/cropGeometry"; +import { RangeTrack, RulerTicks, type RangeWindow } from "../../components/ui/RangeTrack"; +import { CropFrame } from "../../components/ui/CropFrame"; +import SegmentedToggle from "../../components/ui/SegmentedToggle"; +import { Button } from "../../components/ui/Button"; +import { IconButton } from "../../components/ui/IconButton"; +import { Input } from "../../components/ui/Input"; +import { TimecodeError, TimecodeField } from "../../components/ui/TimecodeField"; +import { useTimecodeField, type TimecodeDraft } from "../../components/ui/useTimecodeField"; // The clip panel: marks a range of what is playing and cuts it into a download of its own. It lives // IN the player (the YouTube card under the video, the Plex player's control band), so the video diff --git a/frontend/src/components/PlayerModal.dom.test.tsx b/frontend/src/features/player/PlayerModal.dom.test.tsx similarity index 98% rename from frontend/src/components/PlayerModal.dom.test.tsx rename to frontend/src/features/player/PlayerModal.dom.test.tsx index 40d61133..701cecc0 100644 --- a/frontend/src/components/PlayerModal.dom.test.tsx +++ b/frontend/src/features/player/PlayerModal.dom.test.tsx @@ -23,16 +23,18 @@ vi.mock("react-i18next", async (importOriginal) => { }), }; }); -vi.mock("./navigationContext", () => ({ useNavigationActions: () => ({ openChannel: vi.fn() }) })); +vi.mock("../navigation/navigationContext", () => ({ + useNavigationActions: () => ({ openChannel: vi.fn() }), +})); // Their own queries and popovers are not what is under test here. -vi.mock("./AddToPlaylist", () => ({ default: () => null })); -vi.mock("./DownloadButton", () => ({ default: () => null })); +vi.mock("../video/AddToPlaylist", () => ({ default: () => null })); +vi.mock("../downloads/DownloadButton", () => ({ default: () => null })); import PlayerModal from "./PlayerModal"; -import { ConfirmProvider } from "./ConfirmProvider"; -import { useConfirm } from "./confirmContext"; -import { api, type Video } from "../lib/api"; -import { clearAll, notify } from "../lib/notifications"; +import { ConfirmProvider } from "../../components/ui/ConfirmProvider"; +import { useConfirm } from "../../components/ui/confirmContext"; +import { api, type Video } from "../../lib/api"; +import { clearAll, notify } from "../../lib/notifications"; beforeAll(() => { class Stub { diff --git a/frontend/src/components/PlayerModal.tsx b/frontend/src/features/player/PlayerModal.tsx similarity index 97% rename from frontend/src/components/PlayerModal.tsx rename to frontend/src/features/player/PlayerModal.tsx index da8dcd8d..b0afdd7c 100644 --- a/frontend/src/components/PlayerModal.tsx +++ b/frontend/src/features/player/PlayerModal.tsx @@ -10,12 +10,12 @@ import { } from "react"; import { useTranslation } from "react-i18next"; import { createPortal } from "react-dom"; -import { useNavigationActions } from "./navigationContext"; +import { useNavigationActions } from "../navigation/navigationContext"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { qk } from "../lib/queryKeys"; -import { usePref } from "../lib/usePref"; +import { qk } from "../../lib/queryKeys"; +import { usePref } from "../../lib/usePref"; import { PlayerDescription, PlayerTransportControls } from "./playerModalControls"; -import { AUTO_MODES, LOOP_MODES, type AutoMode, type LoopMode } from "../lib/playerModes"; +import { AUTO_MODES, LOOP_MODES, type AutoMode, type LoopMode } from "../../lib/playerModes"; import { AlertTriangle, ArrowLeft, @@ -37,52 +37,52 @@ import { VolumeX, X, } from "lucide-react"; -import Avatar from "./ui/Avatar"; -import { useLatest, useStableCallback } from "../lib/useLatest"; -import AddToPlaylist from "./AddToPlaylist"; -import { PopoverSurface } from "./ui/AnchoredPopover"; -import { useAnchoredPopover } from "./ui/anchoredPopoverHooks"; +import Avatar from "../../components/ui/Avatar"; +import { useLatest, useStableCallback } from "../../lib/useLatest"; +import AddToPlaylist from "../video/AddToPlaylist"; +import { PopoverSurface } from "../../components/ui/AnchoredPopover"; +import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks"; import { pushLayer, removeLayer, anyBlockingLayerAbove, anyLayerAbove, escapeHandled, -} from "../lib/layerStack"; -import { trapTab, useDialogChrome } from "./ui/dialogFocus"; -import DownloadButton from "./DownloadButton"; +} from "../../lib/layerStack"; +import { trapTab, useDialogChrome } from "../../components/ui/dialogFocus"; +import DownloadButton from "../downloads/DownloadButton"; import ClipPanel, { ClipCropFrame, type ClipCrop } from "./ClipPanel"; -import { markInAt, markOutAt, openAt, type ClipRange } from "../lib/clipRange"; -import { api, type Video, type VideoStatus } from "../lib/api"; -import type { YTPlayer } from "../lib/youtube"; +import { markInAt, markOutAt, openAt, type ClipRange } from "../../lib/clipRange"; +import { api, type Video, type VideoStatus } from "../../lib/api"; +import type { YTPlayer } from "../../lib/youtube"; import { channelYouTubeUrl, formatDate, formatDuration, formatCompact, relativeTime, -} from "../lib/format"; -import { canFullscreenElements, fullscreenElement } from "../lib/fullscreen"; -import { dispatch, isActivationTarget, useKeyScope } from "../lib/keymap"; -import { useBackToClose } from "../lib/history"; -import ChannelNameLink from "./ChannelNameLink"; -import Toaster from "./ui/Toaster"; -import { useOverlayOrder } from "./ui/overlayOrder"; -import { afterRemoval } from "../lib/playerQueue"; +} from "../../lib/format"; +import { canFullscreenElements, fullscreenElement } from "../../lib/fullscreen"; +import { dispatch, isActivationTarget, useKeyScope } from "../../lib/keymap"; +import { useBackToClose } from "../../lib/history"; +import ChannelNameLink from "../video/ChannelNameLink"; +import Toaster from "../../components/ui/Toaster"; +import { useOverlayOrder } from "../../components/ui/overlayOrder"; +import { afterRemoval } from "../../lib/playerQueue"; import { useNativeControlsYield, useYtBigView, useYtProgress, useYtVolume, YT_API_LOAD_FAILED, -} from "../lib/playerModalHooks"; -import { useScrollFade } from "../lib/useScrollFade"; -import { Button } from "./ui/Button"; -import { IconButton } from "./ui/IconButton"; -import { SURFACE } from "./ui/surfaces"; -import { IconLink, LinkButton } from "./ui/LinkButton"; -import { SectionLabel } from "./ui/Headings"; -import { useMediaQuery } from "../lib/useMediaQuery"; +} from "../../lib/playerModalHooks"; +import { useScrollFade } from "../../lib/useScrollFade"; +import { Button } from "../../components/ui/Button"; +import { IconButton } from "../../components/ui/IconButton"; +import { SURFACE } from "../../components/ui/surfaces"; +import { IconLink, LinkButton } from "../../components/ui/LinkButton"; +import { SectionLabel } from "../../components/ui/Headings"; +import { useMediaQuery } from "../../lib/useMediaQuery"; // Experiment (branch experiment/inline-player): play the video in-app via the // YouTube IFrame Player API (not a bare embed) so we can read playback position diff --git a/frontend/src/components/VideoEditor.partial.dom.test.tsx b/frontend/src/features/player/VideoEditor.partial.dom.test.tsx similarity index 97% rename from frontend/src/components/VideoEditor.partial.dom.test.tsx rename to frontend/src/features/player/VideoEditor.partial.dom.test.tsx index 5ea98ab4..e694fd7c 100644 --- a/frontend/src/components/VideoEditor.partial.dom.test.tsx +++ b/frontend/src/features/player/VideoEditor.partial.dom.test.tsx @@ -14,14 +14,14 @@ vi.mock("react-i18next", async (importOriginal) => ({ useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }), })); const { notify } = vi.hoisted(() => ({ notify: vi.fn() })); -vi.mock("../lib/notifications", async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock("../../lib/notifications", async (importOriginal) => ({ + ...(await importOriginal()), notify, })); import VideoEditor from "./VideoEditor"; -import { api, HttpError, type DownloadJob } from "../lib/api"; -import { markReported } from "../lib/api/core"; +import { api, HttpError, type DownloadJob } from "../../lib/api"; +import { markReported } from "../../lib/api/core"; beforeAll(() => { class Stub { diff --git a/frontend/src/components/VideoEditor.tsx b/frontend/src/features/player/VideoEditor.tsx similarity index 95% rename from frontend/src/components/VideoEditor.tsx rename to frontend/src/features/player/VideoEditor.tsx index a52bac94..b42441c2 100644 --- a/frontend/src/components/VideoEditor.tsx +++ b/frontend/src/features/player/VideoEditor.tsx @@ -1,27 +1,27 @@ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; -import { invalidateDownloads } from "../lib/downloads"; -import { qk } from "../lib/queryKeys"; +import { invalidateDownloads } from "../../lib/downloads"; +import { qk } from "../../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Crop, Eye, EyeOff, Pause, Play, Scissors, SplitSquareHorizontal, X } from "lucide-react"; import clsx from "clsx"; -import Modal, { ModalFooter } from "./ui/Modal"; -import { notify } from "../lib/notifications"; -import { api, type DownloadJob, type EditSpec } from "../lib/api"; -import { cropToSourcePx, type Frac } from "../lib/cropGeometry"; -import { CropFrame } from "./ui/CropFrame"; -import { Button } from "./ui/Button"; -import { failureReason, notifyActionError } from "../lib/actionErrors"; -import { markReported } from "../lib/api/core"; -import { IconButton } from "./ui/IconButton"; -import { Pressable } from "./ui/Pressable"; -import { Input } from "./ui/Input"; -import { TimecodeError, TimecodeField } from "./ui/TimecodeField"; -import { useTimecodeField } from "./ui/useTimecodeField"; -import { SURFACE } from "./ui/surfaces"; -import { RangeTrack } from "./ui/RangeTrack"; -import { trackLeft } from "./ui/trackPosition"; -import { formatTimecode } from "../lib/timecode"; +import Modal, { ModalFooter } from "../../components/ui/Modal"; +import { notify } from "../../lib/notifications"; +import { api, type DownloadJob, type EditSpec } from "../../lib/api"; +import { cropToSourcePx, type Frac } from "../../lib/cropGeometry"; +import { CropFrame } from "../../components/ui/CropFrame"; +import { Button } from "../../components/ui/Button"; +import { failureReason, notifyActionError } from "../../lib/actionErrors"; +import { markReported } from "../../lib/api/core"; +import { IconButton } from "../../components/ui/IconButton"; +import { Pressable } from "../../components/ui/Pressable"; +import { Input } from "../../components/ui/Input"; +import { TimecodeError, TimecodeField } from "../../components/ui/TimecodeField"; +import { useTimecodeField } from "../../components/ui/useTimecodeField"; +import { SURFACE } from "../../components/ui/surfaces"; +import { RangeTrack } from "../../components/ui/RangeTrack"; +import { trackLeft } from "../../components/ui/trackPosition"; +import { formatTimecode } from "../../lib/timecode"; type Seg = { id: number; start: number; end: number; keep: boolean }; diff --git a/frontend/src/components/descriptionLinks.dom.test.tsx b/frontend/src/features/player/descriptionLinks.dom.test.tsx similarity index 100% rename from frontend/src/components/descriptionLinks.dom.test.tsx rename to frontend/src/features/player/descriptionLinks.dom.test.tsx diff --git a/frontend/src/components/descriptionLinks.tsx b/frontend/src/features/player/descriptionLinks.tsx similarity index 99% rename from frontend/src/components/descriptionLinks.tsx rename to frontend/src/features/player/descriptionLinks.tsx index 31973922..b22ce848 100644 --- a/frontend/src/components/descriptionLinks.tsx +++ b/frontend/src/features/player/descriptionLinks.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from "react"; import type { TFunction } from "i18next"; -import { Pressable } from "./ui/Pressable"; +import { Pressable } from "../../components/ui/Pressable"; // Turn a video description into clickable nodes: // - bare timestamps (mm:ss / hh:mm:ss) → seek the inline player diff --git a/frontend/src/components/playerModalControls.tsx b/frontend/src/features/player/playerModalControls.tsx similarity index 95% rename from frontend/src/components/playerModalControls.tsx rename to frontend/src/features/player/playerModalControls.tsx index 1ad217ab..3dba6a2c 100644 --- a/frontend/src/components/playerModalControls.tsx +++ b/frontend/src/features/player/playerModalControls.tsx @@ -1,11 +1,11 @@ import type { CSSProperties, Ref } from "react"; import { useTranslation } from "react-i18next"; import { Repeat, Repeat1, Shuffle, SkipBack, SkipForward } from "lucide-react"; -import type { VideoDetail } from "../lib/api"; -import type { AutoMode, LoopMode } from "../lib/playerModes"; -import { Button } from "./ui/Button"; -import { StateMessage } from "./ui/QueryState"; -import { ClampedText } from "./ui/ClampedText"; +import type { VideoDetail } from "../../lib/api"; +import type { AutoMode, LoopMode } from "../../lib/playerModes"; +import { Button } from "../../components/ui/Button"; +import { StateMessage } from "../../components/ui/QueryState"; +import { ClampedText } from "../../components/ui/ClampedText"; import { renderDescription } from "./descriptionLinks"; // Presentational controls for the YouTube modal player (PlayerModal), split out of the god component — diff --git a/frontend/src/components/PlexCollectionAdd.tsx b/frontend/src/features/plexCards/PlexCollectionAdd.tsx similarity index 91% rename from frontend/src/components/PlexCollectionAdd.tsx rename to frontend/src/features/plexCards/PlexCollectionAdd.tsx index 1ace05cb..992a527f 100644 --- a/frontend/src/components/PlexCollectionAdd.tsx +++ b/frontend/src/features/plexCards/PlexCollectionAdd.tsx @@ -1,13 +1,13 @@ import { useLayoutEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { qk } from "../lib/queryKeys"; -import { api, HttpError, type PlexCollection } from "../lib/api"; -import { notifyActionError } from "../lib/actionErrors"; -import { PopoverSurface } from "./ui/AnchoredPopover"; -import { useAnchoredPopover } from "./ui/anchoredPopoverHooks"; -import { MembershipMenu } from "./ui/MembershipMenu"; -import { SURFACE } from "./ui/surfaces"; +import { qk } from "../../lib/queryKeys"; +import { api, HttpError, type PlexCollection } from "../../lib/api"; +import { notifyActionError } from "../../lib/actionErrors"; +import { PopoverSurface } from "../../components/ui/AnchoredPopover"; +import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks"; +import { MembershipMenu } from "../../components/ui/MembershipMenu"; +import { SURFACE } from "../../components/ui/surfaces"; // "Add to collection…" from a movie or show card's menu: a panel beside the card that ticks the title // into or out of its library's manual collections, and names a new one — created with this title in diff --git a/frontend/src/components/PlexPlaylistAdd.tsx b/frontend/src/features/plexCards/PlexPlaylistAdd.tsx similarity index 92% rename from frontend/src/components/PlexPlaylistAdd.tsx rename to frontend/src/features/plexCards/PlexPlaylistAdd.tsx index f9a2ebc6..c7b49d73 100644 --- a/frontend/src/components/PlexPlaylistAdd.tsx +++ b/frontend/src/features/plexCards/PlexPlaylistAdd.tsx @@ -1,17 +1,17 @@ import { useState } from "react"; -import { qk } from "../lib/queryKeys"; +import { qk } from "../../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Plus } from "lucide-react"; -import { MenuCheck } from "./ui/MenuCheck"; -import { api, type PlexPlaylist } from "../lib/api"; -import { notifyActionError } from "../lib/actionErrors"; -import { useScrollFade } from "../lib/useScrollFade"; -import { Button } from "./ui/Button"; -import Modal from "./ui/Modal"; -import { Pressable } from "./ui/Pressable"; -import { Input } from "./ui/Input"; -import { SURFACE } from "./ui/surfaces"; +import { MenuCheck } from "../../components/ui/MenuCheck"; +import { api, type PlexPlaylist } from "../../lib/api"; +import { notifyActionError } from "../../lib/actionErrors"; +import { useScrollFade } from "../../lib/useScrollFade"; +import { Button } from "../../components/ui/Button"; +import Modal from "../../components/ui/Modal"; +import { Pressable } from "../../components/ui/Pressable"; +import { Input } from "../../components/ui/Input"; +import { SURFACE } from "../../components/ui/surfaces"; // What we're adding: a single leaf (movie/episode) or a whole group (a season or an entire show, as a // set of episode rating_keys). Groups get a tri-state (none / some / all) per playlist. diff --git a/frontend/src/components/plexCards.dom.test.tsx b/frontend/src/features/plexCards/plexCards.dom.test.tsx similarity index 99% rename from frontend/src/components/plexCards.dom.test.tsx rename to frontend/src/features/plexCards/plexCards.dom.test.tsx index a2a1cc7f..d5216126 100644 --- a/frontend/src/components/plexCards.dom.test.tsx +++ b/frontend/src/features/plexCards/plexCards.dom.test.tsx @@ -22,8 +22,8 @@ vi.mock("react-i18next", async (importOriginal) => { }); import { PlexPosterCard, SeasonCard, EpisodeCard } from "./plexCards"; -import { iconButtonClass } from "./ui/buttonStyles"; -import { api, type PlexCard, type PlexSeasonDetail } from "../lib/api"; +import { iconButtonClass } from "../../components/ui/buttonStyles"; +import { api, type PlexCard, type PlexSeasonDetail } from "../../lib/api"; // A show card asks the server which episode it would play once its preview is up: a query client. const render = (ui: ReactElement) => diff --git a/frontend/src/components/plexCards.lang.dom.test.tsx b/frontend/src/features/plexCards/plexCards.lang.dom.test.tsx similarity index 97% rename from frontend/src/components/plexCards.lang.dom.test.tsx rename to frontend/src/features/plexCards/plexCards.lang.dom.test.tsx index 219603ed..62d7fc95 100644 --- a/frontend/src/components/plexCards.lang.dom.test.tsx +++ b/frontend/src/features/plexCards/plexCards.lang.dom.test.tsx @@ -17,7 +17,7 @@ vi.mock("react-i18next", async (importOriginal) => { }); import { PlexPosterCard, SeasonCard } from "./plexCards"; -import type { PlexCard, PlexSeasonDetail } from "../lib/api"; +import type { PlexCard, PlexSeasonDetail } from "../../lib/api"; // A show card asks the server which episode it would play once its preview is up: a query client. const render = (ui: ReactElement) => diff --git a/frontend/src/components/plexCards.langSwitch.dom.test.tsx b/frontend/src/features/plexCards/plexCards.langSwitch.dom.test.tsx similarity index 94% rename from frontend/src/components/plexCards.langSwitch.dom.test.tsx rename to frontend/src/features/plexCards/plexCards.langSwitch.dom.test.tsx index f493d87e..d3280069 100644 --- a/frontend/src/components/plexCards.langSwitch.dom.test.tsx +++ b/frontend/src/features/plexCards/plexCards.langSwitch.dom.test.tsx @@ -7,9 +7,9 @@ import { describe, it, expect, afterEach, vi } from "vitest"; import { render as rtlRender, screen, cleanup, act } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { ReactElement } from "react"; -import { setLanguage } from "../i18n"; +import { setLanguage } from "../../i18n"; import { PlexPosterCard } from "./plexCards"; -import type { PlexCard } from "../lib/api"; +import type { PlexCard } from "../../lib/api"; // A show card asks the server which episode it would play once its preview is up: a query client. const render = (ui: ReactElement) => diff --git a/frontend/src/components/plexCards.tsx b/frontend/src/features/plexCards/plexCards.tsx similarity index 95% rename from frontend/src/components/plexCards.tsx rename to frontend/src/features/plexCards/plexCards.tsx index 7286c885..0127f0df 100644 --- a/frontend/src/components/plexCards.tsx +++ b/frontend/src/features/plexCards/plexCards.tsx @@ -3,21 +3,21 @@ import { CheckCircle2, Circle, Info, ListPlus, Star } from "lucide-react"; import clsx from "clsx"; import { type ReactNode, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; -import { api } from "../lib/api"; -import { qk } from "../lib/queryKeys"; -import { plexLang } from "../i18n"; -import { formatDecimal, formatRuntime } from "../lib/format"; -import { Carousel } from "./ui/Carousel"; -import type { PlexCard, PlexCastMember, PlexFilters, PlexSeasonDetail } from "../lib/api"; -import { episodeLine, episodeName, episodeNumbers, seasonTitle } from "../lib/plexDetail"; -import { navItemAttrs } from "../lib/listNav"; -import { endsTextSelection, movedPastSlop } from "../lib/textSelection"; +import { api } from "../../lib/api"; +import { qk } from "../../lib/queryKeys"; +import { plexLang } from "../../i18n"; +import { formatDecimal, formatRuntime } from "../../lib/format"; +import { Carousel } from "../../components/ui/Carousel"; +import type { PlexCard, PlexCastMember, PlexFilters, PlexSeasonDetail } from "../../lib/api"; +import { episodeLine, episodeName, episodeNumbers, seasonTitle } from "../../lib/plexDetail"; +import { navItemAttrs } from "../../lib/listNav"; +import { endsTextSelection, movedPastSlop } from "../../lib/textSelection"; import { plexAddLabelKey, plexStarted } from "./usePlexCardActions"; -import { IconButton } from "./ui/IconButton"; -import { MoreButton } from "./ui/MoreButton"; -import { Pressable } from "./ui/Pressable"; -import { HOVER_YIELD } from "./ui/reveal"; -import { MediaResumeButtons } from "./MediaResumeButtons"; +import { IconButton } from "../../components/ui/IconButton"; +import { MoreButton } from "../../components/ui/MoreButton"; +import { Pressable } from "../../components/ui/Pressable"; +import { HOVER_YIELD } from "../../components/ui/reveal"; +import { MediaResumeButtons } from "../video/MediaResumeButtons"; import { MEDIA_BADGE, MEDIA_BAR, @@ -27,13 +27,13 @@ import { MEDIA_PICTURE, mediaPictureBox, mediaTitleClass, -} from "./ui/mediaCardStyles"; -import { MediaCard, MediaProgress, MediaPlayGlyph } from "./ui/MediaCard"; -import { MediaPreview } from "./ui/MediaPreview"; -import { useMediaPreview } from "./ui/useMediaPreview"; -import { SURFACE } from "./ui/surfaces"; -import { cardImage } from "../lib/plexImage"; -import RemoteImage from "./ui/RemoteImage"; +} from "../../components/ui/mediaCardStyles"; +import { MediaCard, MediaProgress, MediaPlayGlyph } from "../../components/ui/MediaCard"; +import { MediaPreview } from "../../components/ui/MediaPreview"; +import { useMediaPreview } from "../../components/ui/useMediaPreview"; +import { SURFACE } from "../../components/ui/surfaces"; +import { cardImage } from "../../lib/plexImage"; +import RemoteImage from "../../components/ui/RemoteImage"; // Presentational Plex cards + strips, split out of PlexBrowse (prop-driven, no shared state; the one // read is a show card asking which episode it would play, once its preview is up). Rendered by the diff --git a/frontend/src/components/usePlexCardActions.tsx b/frontend/src/features/plexCards/usePlexCardActions.tsx similarity index 96% rename from frontend/src/components/usePlexCardActions.tsx rename to frontend/src/features/plexCards/usePlexCardActions.tsx index 88d19b13..c32284a5 100644 --- a/frontend/src/components/usePlexCardActions.tsx +++ b/frontend/src/features/plexCards/usePlexCardActions.tsx @@ -3,13 +3,13 @@ import { useTranslation } from "react-i18next"; import { useQueryClient } from "@tanstack/react-query"; import { Check, Info, Layers, ListPlus, Play, SkipBack, Tv2, Undo2 } from "lucide-react"; -import { type PlexCard } from "../lib/api"; -import { qk } from "../lib/queryKeys"; +import { type PlexCard } from "../../lib/api"; +import { qk } from "../../lib/queryKeys"; import PlexCollectionAddAt, { type PlexCollectionTarget } from "./PlexCollectionAdd"; -import { pinPlex, useComfortPin } from "./comfortPin"; -import { useCopyItems } from "./copyItems"; +import { pinPlex, useComfortPin } from "../comfort/comfortPin"; +import { useCopyItems } from "../../components/ui/copyItems"; import PlexPlaylistAdd, { type PlexAddTarget } from "./PlexPlaylistAdd"; -import { type ActionMenuItem } from "./ui/ActionMenu"; +import { type ActionMenuItem } from "../../components/ui/ActionMenu"; // A Plex title's ONE action list (docs/DESIGN.md "Action placement"): the right-click, the // long-press, Shift+F10 and the card's "…" open a menu built here, so every poster grid and rail diff --git a/frontend/src/components/PrefsProvider.dom.test.tsx b/frontend/src/features/prefs/PrefsProvider.dom.test.tsx similarity index 95% rename from frontend/src/components/PrefsProvider.dom.test.tsx rename to frontend/src/features/prefs/PrefsProvider.dom.test.tsx index 4c5d6d5d..e4c2c8c2 100644 --- a/frontend/src/components/PrefsProvider.dom.test.tsx +++ b/frontend/src/features/prefs/PrefsProvider.dom.test.tsx @@ -10,9 +10,9 @@ import { act, cleanup, render } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { PrefsProvider } from "./PrefsProvider"; import { usePrefs } from "./prefsContext"; -import { api } from "../lib/api"; -import { claimLocalData, LS } from "../lib/storage"; -import { qk } from "../lib/queryKeys"; +import { api } from "../../lib/api"; +import { claimLocalData, LS } from "../../lib/storage"; +import { qk } from "../../lib/queryKeys"; const seen: { perf: boolean; hints: boolean }[] = []; const controls: ReturnType[] = []; diff --git a/frontend/src/components/PrefsProvider.tsx b/frontend/src/features/prefs/PrefsProvider.tsx similarity index 93% rename from frontend/src/components/PrefsProvider.tsx rename to frontend/src/features/prefs/PrefsProvider.tsx index 2b374c8d..109f70e4 100644 --- a/frontend/src/components/PrefsProvider.tsx +++ b/frontend/src/features/prefs/PrefsProvider.tsx @@ -7,20 +7,24 @@ import { useState, type ReactNode, } from "react"; -import { qk } from "../lib/queryKeys"; +import { qk } from "../../lib/queryKeys"; import { useQueryClient } from "@tanstack/react-query"; -import { api, type Me } from "../lib/api"; +import { api, type Me } from "../../lib/api"; import { applyTheme, loadLocalTheme, normalizeTheme, saveLocalTheme, type ThemePrefs, -} from "../lib/theme"; -import { configureNotifications, getNotifSettings, type NotifSettings } from "../lib/notifications"; -import { hintsEnabled, setHintsEnabled } from "../lib/hints"; -import { getAccountRaw, LS, setAccountRaw } from "../lib/storage"; -import type { PrefsController } from "./SettingsPanel"; +} from "../../lib/theme"; +import { + configureNotifications, + getNotifSettings, + type NotifSettings, +} from "../../lib/notifications"; +import { hintsEnabled, setHintsEnabled } from "../../lib/hints"; +import { getAccountRaw, LS, setAccountRaw } from "../../lib/storage"; +import type { PrefsController } from "../../modules/settings/SettingsPanel"; import { PrefsContext } from "./prefsContext"; const PERF_KEY = LS.perfMode; diff --git a/frontend/src/components/prefsContext.ts b/frontend/src/features/prefs/prefsContext.ts similarity index 75% rename from frontend/src/components/prefsContext.ts rename to frontend/src/features/prefs/prefsContext.ts index 4849aa05..adea2d68 100644 --- a/frontend/src/components/prefsContext.ts +++ b/frontend/src/features/prefs/prefsContext.ts @@ -1,7 +1,7 @@ import { createContext, useContext } from "react"; -import { DEFAULT_THEME } from "../lib/theme"; -import { getNotifSettings } from "../lib/notifications"; -import type { PrefsController } from "./SettingsPanel"; +import { DEFAULT_THEME } from "../../lib/theme"; +import { getNotifSettings } from "../../lib/notifications"; +import type { PrefsController } from "../../modules/settings/SettingsPanel"; // The Settings-page preferences context + hook, split out of PrefsProvider so that file exports only // components (Fast Refresh). diff --git a/frontend/src/components/AddToPlaylist.tsx b/frontend/src/features/video/AddToPlaylist.tsx similarity index 90% rename from frontend/src/components/AddToPlaylist.tsx rename to frontend/src/features/video/AddToPlaylist.tsx index 8ad527a0..7f88b1e0 100644 --- a/frontend/src/components/AddToPlaylist.tsx +++ b/frontend/src/features/video/AddToPlaylist.tsx @@ -1,17 +1,17 @@ import { useLayoutEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { qk } from "../lib/queryKeys"; +import { qk } from "../../lib/queryKeys"; import { ListPlus } from "lucide-react"; -import { YouTubeIcon } from "../lib/brandIcons"; -import { api, type Playlist } from "../lib/api"; -import { notifyActionError } from "../lib/actionErrors"; -import { playlistName } from "../lib/playlistName"; -import { PopoverSurface } from "./ui/AnchoredPopover"; -import { useAnchoredPopover } from "./ui/anchoredPopoverHooks"; -import { IconButton } from "./ui/IconButton"; -import { MembershipMenu } from "./ui/MembershipMenu"; -import { SURFACE } from "./ui/surfaces"; +import { YouTubeIcon } from "../../lib/brandIcons"; +import { api, type Playlist } from "../../lib/api"; +import { notifyActionError } from "../../lib/actionErrors"; +import { playlistName } from "../../lib/playlistName"; +import { PopoverSurface } from "../../components/ui/AnchoredPopover"; +import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks"; +import { IconButton } from "../../components/ui/IconButton"; +import { MembershipMenu } from "../../components/ui/MembershipMenu"; +import { SURFACE } from "../../components/ui/surfaces"; // A small anchored menu for toggling a video's membership in the user's playlists, with inline // "new playlist" creation. Its body is the shared MembershipMenu (as the channel tag editor's). diff --git a/frontend/src/components/ChannelNameLink.dom.test.tsx b/frontend/src/features/video/ChannelNameLink.dom.test.tsx similarity index 100% rename from frontend/src/components/ChannelNameLink.dom.test.tsx rename to frontend/src/features/video/ChannelNameLink.dom.test.tsx diff --git a/frontend/src/components/ChannelNameLink.tsx b/frontend/src/features/video/ChannelNameLink.tsx similarity index 95% rename from frontend/src/components/ChannelNameLink.tsx rename to frontend/src/features/video/ChannelNameLink.tsx index 2be89068..1d129e5e 100644 --- a/frontend/src/components/ChannelNameLink.tsx +++ b/frontend/src/features/video/ChannelNameLink.tsx @@ -1,7 +1,7 @@ import { forwardRef, type AnchorHTMLAttributes } from "react"; import { useTranslation } from "react-i18next"; -import { isPlainLeftClick } from "../lib/nav"; -import { urlWithParams } from "../lib/urlState"; +import { isPlainLeftClick } from "../../lib/nav"; +import { urlWithParams } from "../../lib/urlState"; // A channel name that is a REAL link. The player showed it as a `