From 5bfcf5d83af7a43eb0ddb3d5359a40c1afccd07b Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 5 Sep 2026 16:58:29 +0200 Subject: [PATCH] refactor(e10): move the platform components into components/ui/ and enforce the boundary The app HAS a platform layer -- AnchoredPopover (22 importers, with arrows/typeahead/Escape/anchor-flip), QueryState (21), Modal (17), PageShell, DataTable, Tabs, Tooltip and ~26 more -- but it sat flat among ~160 feature components with nothing marking it as one. Several of those files describe themselves as 'the one place' or 'canonical' and were still bypassed by hand-rolled markup, because a layer nobody can see is a layer nobody uses; three separate inventory passes of this codebase misread it the same way. Moves the 33 domain-free modules into components/ui/, rewrites the imports in 89 files, and makes the boundary a lint rule rather than a convention: inside ui/ a one-level-up import (a feature component) and any lib/api slice are errors. The rule was verified to fire on both and to leave generic lib/ helpers alone. README.md states which side of the line a component belongs on and why. No behaviour change: 3x tsc, eslint, knip, prettier and 687 vitest tests all green; no backend file touched. --- frontend/eslint.config.js | 29 ++++++++++++ frontend/src/App.tsx | 8 ++-- frontend/src/components/About.tsx | 2 +- frontend/src/components/AboutCell.tsx | 2 +- frontend/src/components/AccountPanel.tsx | 4 +- frontend/src/components/ActiveFilterChips.tsx | 2 +- frontend/src/components/AddToPlaylist.tsx | 2 +- frontend/src/components/AdminUsers.tsx | 12 ++--- frontend/src/components/AuditLog.tsx | 4 +- frontend/src/components/ChannelDiscovery.tsx | 12 ++--- frontend/src/components/ChannelLayoutGrid.tsx | 4 +- frontend/src/components/ChannelLink.tsx | 4 +- frontend/src/components/ChannelPage.tsx | 8 ++-- frontend/src/components/ChannelPicker.tsx | 2 +- .../src/components/ChannelStatsPopover.tsx | 2 +- frontend/src/components/ChannelTags.tsx | 2 +- frontend/src/components/Channels.tsx | 20 ++++----- frontend/src/components/ChannelsProvider.tsx | 4 +- frontend/src/components/ChatDock.tsx | 4 +- frontend/src/components/ChatThread.tsx | 2 +- frontend/src/components/ConfigPanel.tsx | 10 ++--- frontend/src/components/ConfirmProvider.tsx | 2 +- frontend/src/components/DateFilterSelect.tsx | 2 +- frontend/src/components/DemoBanner.tsx | 2 +- frontend/src/components/DownloadCenter.tsx | 6 +-- frontend/src/components/DownloadDialog.tsx | 2 +- frontend/src/components/ErrorDialog.tsx | 2 +- frontend/src/components/Feed.tsx | 10 ++--- frontend/src/components/FeedScopeToggle.tsx | 2 +- frontend/src/components/FilterRail.tsx | 4 +- frontend/src/components/GlassTuner.tsx | 2 +- frontend/src/components/LanguageSwitcher.tsx | 2 +- frontend/src/components/LibraryPlayer.tsx | 2 +- frontend/src/components/Messages.tsx | 4 +- frontend/src/components/NavSidebar.tsx | 6 +-- .../src/components/NotificationsPanel.tsx | 6 +-- frontend/src/components/OnboardingWizard.tsx | 2 +- .../src/components/PersonalViewsModal.tsx | 4 +- frontend/src/components/PlayerModal.tsx | 4 +- frontend/src/components/Playlists.tsx | 6 +-- frontend/src/components/PlaylistsRail.tsx | 4 +- frontend/src/components/PlexBrowse.tsx | 10 ++--- .../src/components/PlexCollectionEditor.tsx | 2 +- .../src/components/PlexCollectionView.tsx | 2 +- frontend/src/components/PlexDiscover.tsx | 4 +- frontend/src/components/PlexInfo.tsx | 2 +- frontend/src/components/PlexLibraryPicker.tsx | 2 +- frontend/src/components/PlexPlaylistAdd.tsx | 2 +- frontend/src/components/PlexPlaylistView.tsx | 2 +- frontend/src/components/PlexScopeToggle.tsx | 2 +- frontend/src/components/PlexSearchBox.tsx | 2 +- frontend/src/components/PlexSidebar.tsx | 8 ++-- frontend/src/components/PlexViewToggle.tsx | 2 +- frontend/src/components/ProfileEditor.tsx | 2 +- frontend/src/components/ReleaseNotes.tsx | 2 +- frontend/src/components/SavedViewsPopover.tsx | 2 +- frontend/src/components/Scheduler.tsx | 4 +- frontend/src/components/SettingsPanel.tsx | 8 ++-- frontend/src/components/ShareDialog.tsx | 2 +- frontend/src/components/Sidebar.tsx | 4 +- frontend/src/components/Stats.tsx | 8 ++-- frontend/src/components/SyncStatus.tsx | 2 +- frontend/src/components/TagFilterChips.tsx | 4 +- frontend/src/components/TagManager.tsx | 4 +- frontend/src/components/UploadDialog.tsx | 2 +- frontend/src/components/VerifyBanner.tsx | 2 +- frontend/src/components/VersionBanner.tsx | 2 +- frontend/src/components/VideoCard.tsx | 2 +- frontend/src/components/VideoEditor.tsx | 2 +- frontend/src/components/VideoInfoCard.tsx | 4 +- frontend/src/components/ViewSwitcher.tsx | 2 +- frontend/src/components/VirtualFeed.tsx | 2 +- frontend/src/components/YearFilter.tsx | 6 +-- .../src/components/adminInvites.dom.test.tsx | 2 +- .../adminPendingAccess.dom.test.tsx | 2 +- frontend/src/components/auditColumns.tsx | 2 +- frontend/src/components/channelColumns.tsx | 6 +-- frontend/src/components/downloadAdmin.tsx | 2 +- frontend/src/components/downloadModals.tsx | 2 +- frontend/src/components/playlistColumns.tsx | 2 +- frontend/src/components/plexBrowseLists.tsx | 2 +- frontend/src/components/plexCards.tsx | 2 +- frontend/src/components/plexViews.tsx | 4 +- .../{ => ui}/ActionMenu.dom.test.tsx | 0 .../src/components/{ => ui}/ActionMenu.tsx | 0 .../{ => ui}/AnchoredPopover.dom.test.tsx | 0 .../components/{ => ui}/AnchoredPopover.tsx | 4 +- frontend/src/components/{ => ui}/Avatar.tsx | 0 .../src/components/{ => ui}/BackToTop.tsx | 0 frontend/src/components/{ => ui}/Banner.tsx | 0 .../components/{ => ui}/Carousel.dom.test.tsx | 0 frontend/src/components/{ => ui}/Carousel.tsx | 2 +- .../components/{ => ui}/ColumnSortControl.tsx | 4 +- .../components/{ => ui}/Combobox.dom.test.tsx | 0 frontend/src/components/{ => ui}/Combobox.tsx | 2 +- .../src/components/{ => ui}/CountBadge.tsx | 0 .../DataTable.fixedLayout.dom.test.tsx | 0 .../src/components/{ => ui}/DataTable.tsx | 2 +- .../{ => ui}/FilterChipGroup.dom.test.tsx | 0 .../components/{ => ui}/FilterChipGroup.tsx | 0 .../{ => ui}/FilterSelect.dom.test.tsx | 0 .../src/components/{ => ui}/FilterSelect.tsx | 0 .../src/components/{ => ui}/InlineNotice.tsx | 0 .../{ => ui}/ItemCount.dom.test.tsx | 2 +- .../src/components/{ => ui}/ItemCount.tsx | 0 .../components/{ => ui}/Modal.dom.test.tsx | 2 +- frontend/src/components/{ => ui}/Modal.tsx | 6 +-- frontend/src/components/{ => ui}/Overlay.tsx | 0 .../{ => ui}/PageScroller.dom.test.tsx | 2 +- .../src/components/{ => ui}/PageScroller.tsx | 4 +- .../{ => ui}/PageShell.dom.test.tsx | 0 .../src/components/{ => ui}/PageShell.tsx | 0 frontend/src/components/{ => ui}/Pager.tsx | 0 .../src/components/{ => ui}/PanelGroup.tsx | 0 .../src/components/{ => ui}/PanelGroups.tsx | 2 +- .../{ => ui}/PriorityStepper.dom.test.tsx | 0 .../components/{ => ui}/PriorityStepper.tsx | 0 .../src/components/{ => ui}/QueryState.tsx | 0 frontend/src/components/ui/README.md | 44 +++++++++++++++++++ .../{ => ui}/SegmentedToggle.dom.test.tsx | 0 .../components/{ => ui}/SegmentedToggle.tsx | 0 .../src/components/{ => ui}/SidePanel.tsx | 4 +- .../{ => ui}/SortControl.dom.test.tsx | 2 +- .../src/components/{ => ui}/SortControl.tsx | 2 +- frontend/src/components/{ => ui}/Tabs.tsx | 2 +- frontend/src/components/{ => ui}/Toaster.tsx | 2 +- frontend/src/components/{ => ui}/Tooltip.tsx | 2 +- .../src/components/{ => ui}/UndoToolbar.tsx | 0 .../src/components/{ => ui}/VirtualGrid.tsx | 0 frontend/src/components/ui/form.tsx | 2 +- .../src/components/{ => ui}/readAhead.test.ts | 0 frontend/src/components/{ => ui}/readAhead.ts | 0 .../src/components/{ => ui}/tableHeader.tsx | 4 +- frontend/src/lib/channelsUrlState.dom.test.ts | 2 +- frontend/src/lib/channelsUrlState.ts | 2 +- frontend/src/lib/playlistView.test.ts | 2 +- frontend/src/lib/sortOptions.ts | 2 +- frontend/src/lib/useFilterState.ts | 2 +- 138 files changed, 262 insertions(+), 189 deletions(-) rename frontend/src/components/{ => ui}/ActionMenu.dom.test.tsx (100%) rename frontend/src/components/{ => ui}/ActionMenu.tsx (100%) rename frontend/src/components/{ => ui}/AnchoredPopover.dom.test.tsx (100%) rename frontend/src/components/{ => ui}/AnchoredPopover.tsx (99%) rename frontend/src/components/{ => ui}/Avatar.tsx (100%) rename frontend/src/components/{ => ui}/BackToTop.tsx (100%) rename frontend/src/components/{ => ui}/Banner.tsx (100%) rename frontend/src/components/{ => ui}/Carousel.dom.test.tsx (100%) rename frontend/src/components/{ => ui}/Carousel.tsx (98%) rename frontend/src/components/{ => ui}/ColumnSortControl.tsx (90%) rename frontend/src/components/{ => ui}/Combobox.dom.test.tsx (100%) rename frontend/src/components/{ => ui}/Combobox.tsx (99%) rename frontend/src/components/{ => ui}/CountBadge.tsx (100%) rename frontend/src/components/{ => ui}/DataTable.fixedLayout.dom.test.tsx (100%) rename frontend/src/components/{ => ui}/DataTable.tsx (99%) rename frontend/src/components/{ => ui}/FilterChipGroup.dom.test.tsx (100%) rename frontend/src/components/{ => ui}/FilterChipGroup.tsx (100%) rename frontend/src/components/{ => ui}/FilterSelect.dom.test.tsx (100%) rename frontend/src/components/{ => ui}/FilterSelect.tsx (100%) rename frontend/src/components/{ => ui}/InlineNotice.tsx (100%) rename frontend/src/components/{ => ui}/ItemCount.dom.test.tsx (96%) rename frontend/src/components/{ => ui}/ItemCount.tsx (100%) rename frontend/src/components/{ => ui}/Modal.dom.test.tsx (98%) rename frontend/src/components/{ => ui}/Modal.tsx (97%) rename frontend/src/components/{ => ui}/Overlay.tsx (100%) rename frontend/src/components/{ => ui}/PageScroller.dom.test.tsx (98%) rename frontend/src/components/{ => ui}/PageScroller.tsx (98%) rename frontend/src/components/{ => ui}/PageShell.dom.test.tsx (100%) rename frontend/src/components/{ => ui}/PageShell.tsx (100%) rename frontend/src/components/{ => ui}/Pager.tsx (100%) rename frontend/src/components/{ => ui}/PanelGroup.tsx (100%) rename frontend/src/components/{ => ui}/PanelGroups.tsx (98%) rename frontend/src/components/{ => ui}/PriorityStepper.dom.test.tsx (100%) rename frontend/src/components/{ => ui}/PriorityStepper.tsx (100%) rename frontend/src/components/{ => ui}/QueryState.tsx (100%) create mode 100644 frontend/src/components/ui/README.md rename frontend/src/components/{ => ui}/SegmentedToggle.dom.test.tsx (100%) rename frontend/src/components/{ => ui}/SegmentedToggle.tsx (100%) rename frontend/src/components/{ => ui}/SidePanel.tsx (98%) rename frontend/src/components/{ => ui}/SortControl.dom.test.tsx (98%) rename frontend/src/components/{ => ui}/SortControl.tsx (98%) rename frontend/src/components/{ => ui}/Tabs.tsx (97%) rename frontend/src/components/{ => ui}/Toaster.tsx (99%) rename frontend/src/components/{ => ui}/Tooltip.tsx (97%) rename frontend/src/components/{ => ui}/UndoToolbar.tsx (100%) rename frontend/src/components/{ => ui}/VirtualGrid.tsx (100%) rename frontend/src/components/{ => ui}/readAhead.test.ts (100%) rename frontend/src/components/{ => ui}/readAhead.ts (100%) rename frontend/src/components/{ => ui}/tableHeader.tsx (99%) diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index c8f32bc6..88a51850 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -67,5 +67,34 @@ export default tseslint.config( ], }, }, + { + // E10 S1: `components/ui/` is the PLATFORM layer, and the rule that makes it a layer rather than + // a folder is enforced here, not in prose (see components/ui/README.md). A platform component is + // domain-free: it may import sibling primitives and generic `lib/` helpers, never a feature + // component (`../Feed`) and never a domain API slice. Without this rule the boundary erodes on + // the first "just this once" import and the directory silently becomes another components/. + files: ["src/components/ui/**/*.{ts,tsx}"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + // `../X` (exactly one level up = a feature component). A `regex` rather than a glob: + // eslint matches `patterns` gitignore-style, where `../*` also swallows `../../lib/x`. + regex: "^\.\./(?!\.\./)", + message: + "components/ui/ is domain-free: a platform component may not import a feature component. Invert the dependency (take a prop/render-prop) or keep the component outside ui/.", + }, + { + regex: "^\.\./\.\./lib/api/", + message: + "components/ui/ is domain-free: a platform component may not call a domain API slice. Take the data as props.", + }, + ], + }, + ], + }, + }, prettier, ); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4534ef55..a61db11b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -29,12 +29,12 @@ import Welcome from "./components/Welcome"; import SetupWizard from "./components/SetupWizard"; import Header from "./components/Header"; import NavSidebar from "./components/NavSidebar"; -import BackToTop from "./components/BackToTop"; -import PageShell from "./components/PageShell"; +import BackToTop from "./components/ui/BackToTop"; +import PageShell from "./components/ui/PageShell"; import GlassTuner from "./components/GlassTuner"; import ChatDock from "./components/ChatDock"; -import Toaster from "./components/Toaster"; -import { StateMessage } from "./components/QueryState"; +import Toaster from "./components/ui/Toaster"; +import { StateMessage } from "./components/ui/QueryState"; import ErrorDialog from "./components/ErrorDialog"; import VersionBanner from "./components/VersionBanner"; import DemoBanner from "./components/DemoBanner"; diff --git a/frontend/src/components/About.tsx b/frontend/src/components/About.tsx index 46d2888c..1b28abeb 100644 --- a/frontend/src/components/About.tsx +++ b/frontend/src/components/About.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import { Info, Sparkles } from "lucide-react"; import { api } from "../lib/api"; import { FRONTEND_VERSION, FRONTEND_SHA, FRONTEND_BUILD_DATE } from "../lib/version"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; function fmtDate(d?: string | null): string { if (!d) return "—"; diff --git a/frontend/src/components/AboutCell.tsx b/frontend/src/components/AboutCell.tsx index e26bc63d..86d9ddf4 100644 --- a/frontend/src/components/AboutCell.tsx +++ b/frontend/src/components/AboutCell.tsx @@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import { qk } from "../lib/queryKeys"; import { api } from "../lib/api"; import ChannelAboutContent from "./ChannelAbout"; -import { PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; +import { PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; /** * A channel-description cell: one truncated line in the table, revealing the FULL channel "About" diff --git a/frontend/src/components/AccountPanel.tsx b/frontend/src/components/AccountPanel.tsx index a7e25382..4b1377ca 100644 --- a/frontend/src/components/AccountPanel.tsx +++ b/frontend/src/components/AccountPanel.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Check, Clock, Copy, Globe, KeyRound, Mail, Send, ShieldAlert } from "lucide-react"; -import Modal from "./Modal"; -import { LoadingState, StateMessage } from "./QueryState"; +import Modal from "./ui/Modal"; +import { LoadingState, StateMessage } from "./ui/QueryState"; import { api } from "../lib/api"; import { qk } from "../lib/queryKeys"; import { notify } from "../lib/notifications"; diff --git a/frontend/src/components/ActiveFilterChips.tsx b/frontend/src/components/ActiveFilterChips.tsx index 28e42875..e335dabf 100644 --- a/frontend/src/components/ActiveFilterChips.tsx +++ b/frontend/src/components/ActiveFilterChips.tsx @@ -4,7 +4,7 @@ import { Filter, X } from "lucide-react"; import type { ActiveFilter } from "../lib/useActiveFilters"; import { decideChipLayout } from "../lib/chipOverflow"; import { useChipOverflowFit } from "../lib/useChipOverflowFit"; -import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; +import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; // 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/AddToPlaylist.tsx b/frontend/src/components/AddToPlaylist.tsx index b7fb452d..716279ef 100644 --- a/frontend/src/components/AddToPlaylist.tsx +++ b/frontend/src/components/AddToPlaylist.tsx @@ -6,7 +6,7 @@ import { Check, ListPlus, Plus, Youtube } from "lucide-react"; import { api, type Playlist } from "../lib/api"; import { useScrollFade } from "../lib/useScrollFade"; import { playlistName } from "../lib/playlistName"; -import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; +import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; // A small anchored menu for toggling a video's membership in the user's playlists, with inline // "new playlist" creation. Same shape as the channel tag editor (R8 S1's pilot): radio-style diff --git a/frontend/src/components/AdminUsers.tsx b/frontend/src/components/AdminUsers.tsx index d61b2737..ab70e155 100644 --- a/frontend/src/components/AdminUsers.tsx +++ b/frontend/src/components/AdminUsers.tsx @@ -26,16 +26,16 @@ import { import { notify } from "../lib/notifications"; import { invalidateUserAccess, isEmailOff } from "../lib/adminUsers"; import { ADMIN_USERS_TAB_KEY } from "../lib/adminUsersTab"; -import Tooltip from "./Tooltip"; -import InlineNotice from "./InlineNotice"; +import Tooltip from "./ui/Tooltip"; +import InlineNotice from "./ui/InlineNotice"; import { Section } from "./ui/form"; -import SegmentedToggle from "./SegmentedToggle"; +import SegmentedToggle from "./ui/SegmentedToggle"; import { LANGUAGES, type LangCode } from "../i18n"; -import { LoadingState, StateMessage } from "./QueryState"; +import { LoadingState, StateMessage } from "./ui/QueryState"; import AccountPanel from "./AccountPanel"; import { useConfirm } from "./ConfirmProvider"; -import Tabs, { usePersistedTab } from "./Tabs"; -import { PageToolbar } from "./PageShell"; +import Tabs, { usePersistedTab } from "./ui/Tabs"; +import { PageToolbar } from "./ui/PageShell"; import { useMe } from "../lib/useMe"; // Admin-only user & access management: roles, access requests (the Invite whitelist), and the diff --git a/frontend/src/components/AuditLog.tsx b/frontend/src/components/AuditLog.tsx index f19b0457..0631b678 100644 --- a/frontend/src/components/AuditLog.tsx +++ b/frontend/src/components/AuditLog.tsx @@ -8,8 +8,8 @@ import { notify } from "../lib/notifications"; import { LS } from "../lib/storage"; import { Section } from "./ui/form"; import { useConfirm } from "./ConfirmProvider"; -import DataTable from "./DataTable"; -import { LoadingState, StateMessage } from "./QueryState"; +import DataTable from "./ui/DataTable"; +import { LoadingState, StateMessage } from "./ui/QueryState"; import { auditColumns } from "./auditColumns"; // Admin-only audit trail: an append-only who/what/when log of admin actions + scheduler run diff --git a/frontend/src/components/ChannelDiscovery.tsx b/frontend/src/components/ChannelDiscovery.tsx index caa64822..5798f0c1 100644 --- a/frontend/src/components/ChannelDiscovery.tsx +++ b/frontend/src/components/ChannelDiscovery.tsx @@ -12,15 +12,15 @@ import { useCardPager } from "../lib/useCardPager"; import { subsColumn } from "./channelColumns"; import { notify } from "../lib/notifications"; import { notifyYouTubeActionError } from "../lib/youtubeErrors"; -import Tooltip from "./Tooltip"; +import Tooltip from "./ui/Tooltip"; import ChannelLink from "./ChannelLink"; import AboutCell from "./AboutCell"; -import DataTable, { type Column } from "./DataTable"; +import DataTable, { type Column } from "./ui/DataTable"; import ChannelLayoutGrid from "./ChannelLayoutGrid"; -import { LoadingState, StateMessage } from "./QueryState"; -import ColumnSortControl from "./ColumnSortControl"; -import Pager from "./Pager"; -import { PageToolbar } from "./PageShell"; +import { LoadingState, StateMessage } from "./ui/QueryState"; +import ColumnSortControl from "./ui/ColumnSortControl"; +import Pager from "./ui/Pager"; +import { PageToolbar } from "./ui/PageShell"; import { useConfirm } from "./ConfirmProvider"; import { useChannelLayout } from "./ChannelsProvider"; diff --git a/frontend/src/components/ChannelLayoutGrid.tsx b/frontend/src/components/ChannelLayoutGrid.tsx index 76c988de..d15c6ffb 100644 --- a/frontend/src/components/ChannelLayoutGrid.tsx +++ b/frontend/src/components/ChannelLayoutGrid.tsx @@ -1,7 +1,7 @@ import { Fragment } from "react"; import { useTranslation } from "react-i18next"; -import { type Column } from "./DataTable"; -import VirtualGrid from "./VirtualGrid"; +import { type Column } from "./ui/DataTable"; +import VirtualGrid from "./ui/VirtualGrid"; import { CHANNEL_LAYOUT_SPEC } from "../lib/channelLayout"; // The channel manager's `cards` layout (E4 S3), built by reusing the SAME `Column` render closures diff --git a/frontend/src/components/ChannelLink.tsx b/frontend/src/components/ChannelLink.tsx index 2fc67126..a422dbea 100644 --- a/frontend/src/components/ChannelLink.tsx +++ b/frontend/src/components/ChannelLink.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { ExternalLink } from "lucide-react"; -import Avatar from "./Avatar"; -import Tooltip from "./Tooltip"; +import Avatar from "./ui/Avatar"; +import Tooltip from "./ui/Tooltip"; import { channelYouTubeUrl } from "../lib/format"; // Avatar + channel name + "open on YouTube" link, shared by the Channel manager and the diff --git a/frontend/src/components/ChannelPage.tsx b/frontend/src/components/ChannelPage.tsx index 682388d3..2932ca7d 100644 --- a/frontend/src/components/ChannelPage.tsx +++ b/frontend/src/components/ChannelPage.tsx @@ -5,7 +5,7 @@ import { useLiveQuery } from "../lib/useLiveQuery"; import { channelDetailPollMs } from "../lib/channelDetailPoll"; import { qk } from "../lib/queryKeys"; import { useNavigationActions } from "./NavigationProvider"; -import { PriorityStepper } from "./PriorityStepper"; +import { PriorityStepper } from "./ui/PriorityStepper"; import { useChannelPriority } from "../lib/useChannelPriority"; import { ArrowLeft, @@ -20,11 +20,11 @@ import { RefreshCw, SlidersHorizontal, } from "lucide-react"; -import Avatar from "./Avatar"; -import Tabs from "./Tabs"; +import Avatar from "./ui/Avatar"; +import Tabs from "./ui/Tabs"; import Feed from "./Feed"; import ChannelAboutContent from "./ChannelAbout"; -import { PageToolbar, PageToolbarSlot } from "./PageShell"; +import { PageToolbar, PageToolbarSlot } from "./ui/PageShell"; import { useFeedFilters } from "./FeedFiltersProvider"; import { useConfirm } from "./ConfirmProvider"; import { notify } from "../lib/notifications"; diff --git a/frontend/src/components/ChannelPicker.tsx b/frontend/src/components/ChannelPicker.tsx index 3c4386de..bebc16d2 100644 --- a/frontend/src/components/ChannelPicker.tsx +++ b/frontend/src/components/ChannelPicker.tsx @@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next"; import { useQuery } from "@tanstack/react-query"; import { qk } from "../lib/queryKeys"; import { api, type FeedFilters } from "../lib/api"; -import Combobox, { type ComboboxOption } from "./Combobox"; +import Combobox, { type ComboboxOption } from "./ui/Combobox"; // Pick channels to narrow the feed to — several OR together, so every other filter then applies // across all of them. Until this existed the filter was settable only via a shared "?channel=" link: diff --git a/frontend/src/components/ChannelStatsPopover.tsx b/frontend/src/components/ChannelStatsPopover.tsx index 5c8a3b13..3d20d933 100644 --- a/frontend/src/components/ChannelStatsPopover.tsx +++ b/frontend/src/components/ChannelStatsPopover.tsx @@ -12,7 +12,7 @@ import { import { useTranslation } from "react-i18next"; import { type MyStatus } from "../lib/api"; import { formatEta } from "../lib/format"; -import { PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; +import { PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; // The channel-manager catalog stats, collapsed from a crowded inline row into a compact "Stats" chip // that reveals the numbers on hover (reuses the AnchoredPopover hover mode). One row per stat, each diff --git a/frontend/src/components/ChannelTags.tsx b/frontend/src/components/ChannelTags.tsx index f580d7a6..1a6e497d 100644 --- a/frontend/src/components/ChannelTags.tsx +++ b/frontend/src/components/ChannelTags.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { Check, Plus, Sparkles, X } from "lucide-react"; import { useTranslation } from "react-i18next"; import { type Tag } from "../lib/api"; -import { useAnchoredPopover, PopoverSurface, PopoverItem } from "./AnchoredPopover"; +import { useAnchoredPopover, PopoverSurface, PopoverItem } from "./ui/AnchoredPopover"; import { useScrollFade } from "../lib/useScrollFade"; import { fitChipsWithPill } from "../lib/chipOverflow"; import { useChipOverflowFit } from "../lib/useChipOverflowFit"; diff --git a/frontend/src/components/Channels.tsx b/frontend/src/components/Channels.tsx index ca9c1426..2edc6817 100644 --- a/frontend/src/components/Channels.tsx +++ b/frontend/src/components/Channels.tsx @@ -10,26 +10,26 @@ import { notifyYouTubeActionError } from "../lib/youtubeErrors"; import { accountKey, LS } from "../lib/storage"; import { buildChannelColumns, applySyncFilter } from "./channelColumns"; import { notify } from "../lib/notifications"; -import Tooltip from "./Tooltip"; -import DataTable from "./DataTable"; -import Tabs, { type TabDef } from "./Tabs"; +import Tooltip from "./ui/Tooltip"; +import DataTable from "./ui/DataTable"; +import Tabs, { type TabDef } from "./ui/Tabs"; import ChannelLayoutGrid from "./ChannelLayoutGrid"; -import { LoadingState, StateMessage } from "./QueryState"; +import { LoadingState, StateMessage } from "./ui/QueryState"; import { useChannelTagActions } from "../lib/useChannelTagActions"; import { useTagSuggestions } from "./useTagSuggestions"; import { useChannelPriority } from "../lib/useChannelPriority"; import ViewSwitcher, { type ViewOption } from "./ViewSwitcher"; -import ColumnSortControl from "./ColumnSortControl"; -import FilterChipGroup from "./FilterChipGroup"; +import ColumnSortControl from "./ui/ColumnSortControl"; +import FilterChipGroup from "./ui/FilterChipGroup"; import TagFilterChips, { type FilterChip } from "./TagFilterChips"; -import ActionMenu, { type ActionMenuItem } from "./ActionMenu"; +import ActionMenu, { type ActionMenuItem } from "./ui/ActionMenu"; import ChannelStatsPopover from "./ChannelStatsPopover"; -import Pager from "./Pager"; +import Pager from "./ui/Pager"; import { CHANNEL_LAYOUTS, type ChannelLayout } from "../lib/channelLayout"; import { sortRows } from "../lib/columnSort"; -import { filterRows } from "./tableHeader"; +import { filterRows } from "./ui/tableHeader"; import { useCardPager } from "../lib/useCardPager"; -import { PageToolbar } from "./PageShell"; +import { PageToolbar } from "./ui/PageShell"; import ChannelDiscovery from "./ChannelDiscovery"; import TagManager from "./TagManager"; import { useConfirm } from "./ConfirmProvider"; diff --git a/frontend/src/components/ChannelsProvider.tsx b/frontend/src/components/ChannelsProvider.tsx index 3504a49d..4111b803 100644 --- a/frontend/src/components/ChannelsProvider.tsx +++ b/frontend/src/components/ChannelsProvider.tsx @@ -22,8 +22,8 @@ import { hasChannelsParams, type ChannelsUrlState, } from "../lib/channelsUrlState"; -import { usePersistedTab } from "./Tabs"; -import type { FilterMap } from "./tableHeader"; +import { usePersistedTab } from "./ui/Tabs"; +import type { FilterMap } from "./ui/tableHeader"; import { useNavigation, useNavigationActions } from "./NavigationProvider"; import type { ChannelsView } from "./Channels"; diff --git a/frontend/src/components/ChatDock.tsx b/frontend/src/components/ChatDock.tsx index e638a306..1d3c1669 100644 --- a/frontend/src/components/ChatDock.tsx +++ b/frontend/src/components/ChatDock.tsx @@ -13,9 +13,9 @@ import { } from "../lib/chatDock"; import { onMessage, onUnread } from "../lib/messagesSocket"; import * as e2ee from "../lib/e2ee"; -import Avatar from "./Avatar"; +import Avatar from "./ui/Avatar"; import ChatThread from "./ChatThread"; -import Overlay from "./Overlay"; +import Overlay from "./ui/Overlay"; // The Messenger-style floating chat dock: open conversations live here, bottom-right, across // page navigation (state persisted per user). Always mounted (for human users) so it also owns diff --git a/frontend/src/components/ChatThread.tsx b/frontend/src/components/ChatThread.tsx index 76689e30..da6d128d 100644 --- a/frontend/src/components/ChatThread.tsx +++ b/frontend/src/components/ChatThread.tsx @@ -6,7 +6,7 @@ import { Send } from "lucide-react"; import { api, HttpError, type Message, type MessageUser } from "../lib/api"; import { useLiveQuery } from "../lib/useLiveQuery"; import { useScrollFade } from "../lib/useScrollFade"; -import { LoadingState, StateMessage } from "./QueryState"; +import { LoadingState, StateMessage } from "./ui/QueryState"; import { relativeTime } from "../lib/format"; import { notify } from "../lib/notifications"; import { partnerPub, POLL_MS, refreshPartnerPub, useKeyState } from "../lib/messaging"; diff --git a/frontend/src/components/ConfigPanel.tsx b/frontend/src/components/ConfigPanel.tsx index f16f1631..0b8b35ee 100644 --- a/frontend/src/components/ConfigPanel.tsx +++ b/frontend/src/components/ConfigPanel.tsx @@ -5,12 +5,12 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Plug, RotateCcw, Send } from "lucide-react"; import { api, type ConfigItem, type PlexTestResult } from "../lib/api"; import { notify } from "../lib/notifications"; -import Tooltip from "./Tooltip"; -import InlineNotice from "./InlineNotice"; +import Tooltip from "./ui/Tooltip"; +import InlineNotice from "./ui/InlineNotice"; import { useMe } from "../lib/useMe"; -import Tabs, { usePersistedTab } from "./Tabs"; -import { LoadingState, StateMessage } from "./QueryState"; -import { PageToolbar } from "./PageShell"; +import Tabs, { usePersistedTab } from "./ui/Tabs"; +import { LoadingState, StateMessage } from "./ui/QueryState"; +import { PageToolbar } from "./ui/PageShell"; import { Switch } from "./ui/form"; import { DraftSaveBar, type SaveState } from "./ui/DraftSaveBar"; import { LS } from "../lib/storage"; diff --git a/frontend/src/components/ConfirmProvider.tsx b/frontend/src/components/ConfirmProvider.tsx index 47cc9dd3..191df296 100644 --- a/frontend/src/components/ConfirmProvider.tsx +++ b/frontend/src/components/ConfirmProvider.tsx @@ -1,6 +1,6 @@ import { createContext, useCallback, useContext, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; import { Button } from "./ui/Button"; // App-styled, promise-based replacement for window.confirm. Wrap the tree in diff --git a/frontend/src/components/DateFilterSelect.tsx b/frontend/src/components/DateFilterSelect.tsx index f4fe6587..ff4a5d43 100644 --- a/frontend/src/components/DateFilterSelect.tsx +++ b/frontend/src/components/DateFilterSelect.tsx @@ -1,7 +1,7 @@ import { useLayoutEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { CalendarDays, Check, ChevronDown } from "lucide-react"; -import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; +import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; import { type FeedFilters } from "../lib/api"; import { deriveDateKey, selectDateChip, type DatePreset } from "../lib/dateFilter"; diff --git a/frontend/src/components/DemoBanner.tsx b/frontend/src/components/DemoBanner.tsx index d9b6cb94..e7837a92 100644 --- a/frontend/src/components/DemoBanner.tsx +++ b/frontend/src/components/DemoBanner.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; import { AlertTriangle } from "lucide-react"; -import Banner from "./Banner"; +import Banner from "./ui/Banner"; // Permanent, non-dismissible bar shown to the shared demo account so it's always clear the // state is communal. (Replaces the old login-time toast, which re-popped on every reload.) diff --git a/frontend/src/components/DownloadCenter.tsx b/frontend/src/components/DownloadCenter.tsx index 819890dd..a4fa1787 100644 --- a/frontend/src/components/DownloadCenter.tsx +++ b/frontend/src/components/DownloadCenter.tsx @@ -19,9 +19,9 @@ import { Trash2, UploadCloud, } from "lucide-react"; -import Tabs from "./Tabs"; -import { PageToolbar } from "./PageShell"; -import { LoadingState, StateMessage } from "./QueryState"; +import Tabs from "./ui/Tabs"; +import { PageToolbar } from "./ui/PageShell"; +import { LoadingState, StateMessage } from "./ui/QueryState"; // Lazy: these dialogs/editors open on demand, so they stay out of the Downloads page chunk until // used (the video editor in particular is heavy — filmstrip + scrubber). const DownloadDialog = lazy(() => import("./DownloadDialog")); diff --git a/frontend/src/components/DownloadDialog.tsx b/frontend/src/components/DownloadDialog.tsx index 1c6d3eb4..0ad44a56 100644 --- a/frontend/src/components/DownloadDialog.tsx +++ b/frontend/src/components/DownloadDialog.tsx @@ -2,7 +2,7 @@ import { lazy, Suspense, useState } from "react"; import { qk } from "../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; import { Button } from "./ui/Button"; import { api } from "../lib/api"; import { invalidateDownloads } from "../lib/downloads"; diff --git a/frontend/src/components/ErrorDialog.tsx b/frontend/src/components/ErrorDialog.tsx index 5b8968bb..34ba1805 100644 --- a/frontend/src/components/ErrorDialog.tsx +++ b/frontend/src/components/ErrorDialog.tsx @@ -1,7 +1,7 @@ import { useSyncExternalStore } from "react"; import { useTranslation } from "react-i18next"; import { dismissError, getError, subscribeError } from "../lib/errorDialog"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; import { Button } from "./ui/Button"; // Renders the current app error (if any) as a modal the user must acknowledge. diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx index a4253c7d..46fb5979 100644 --- a/frontend/src/components/Feed.tsx +++ b/frontend/src/components/Feed.tsx @@ -38,12 +38,12 @@ import { FEED_VIEWS, type FeedView } from "../lib/feedView"; import { clearedFilters, useActiveFilters } from "../lib/useActiveFilters"; import ActiveFilterChips from "./ActiveFilterChips"; import SelectedViewChip from "./SelectedViewChip"; -import { LoadingState, StateMessage } from "./QueryState"; -import { PageToolbar } from "./PageShell"; +import { LoadingState, StateMessage } from "./ui/QueryState"; +import { PageToolbar } from "./ui/PageShell"; import { useWizard } from "./WizardProvider"; -import SortControl from "./SortControl"; -import FilterSelect from "./FilterSelect"; -import ItemCount from "./ItemCount"; +import SortControl from "./ui/SortControl"; +import FilterSelect from "./ui/FilterSelect"; +import ItemCount from "./ui/ItemCount"; import ViewSwitcher from "./ViewSwitcher"; import VirtualFeed from "./VirtualFeed"; // Lazy: the in-app YouTube player (IFrame API) loads only when a video is first opened. diff --git a/frontend/src/components/FeedScopeToggle.tsx b/frontend/src/components/FeedScopeToggle.tsx index b943f19c..e218bb31 100644 --- a/frontend/src/components/FeedScopeToggle.tsx +++ b/frontend/src/components/FeedScopeToggle.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; import { Library, User } from "lucide-react"; -import SegmentedToggle from "./SegmentedToggle"; +import SegmentedToggle from "./ui/SegmentedToggle"; import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider"; // The feed's Mine/Library scope, relocated from the filter sidebar to sit just right of the header diff --git a/frontend/src/components/FilterRail.tsx b/frontend/src/components/FilterRail.tsx index 420e705c..a0d6e241 100644 --- a/frontend/src/components/FilterRail.tsx +++ b/frontend/src/components/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 "./SidePanel"; -import PanelGroups, { type PanelItem } from "./PanelGroups"; +import SidePanel from "./ui/SidePanel"; +import PanelGroups, { type PanelItem } from "./ui/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/GlassTuner.tsx b/frontend/src/components/GlassTuner.tsx index fed6ad06..ffada379 100644 --- a/frontend/src/components/GlassTuner.tsx +++ b/frontend/src/components/GlassTuner.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from "react"; -import Overlay from "./Overlay"; +import Overlay from "./ui/Overlay"; import { copyText } from "../lib/clipboard"; import { usePrefs } from "./PrefsProvider"; diff --git a/frontend/src/components/LanguageSwitcher.tsx b/frontend/src/components/LanguageSwitcher.tsx index 66c59cc7..cb33aab5 100644 --- a/frontend/src/components/LanguageSwitcher.tsx +++ b/frontend/src/components/LanguageSwitcher.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { Check, Globe } from "lucide-react"; import { LANGUAGES, type LangCode } from "../i18n"; -import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; +import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; // 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/LibraryPlayer.tsx b/frontend/src/components/LibraryPlayer.tsx index 0b8caa4c..39c7c79d 100644 --- a/frontend/src/components/LibraryPlayer.tsx +++ b/frontend/src/components/LibraryPlayer.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; import { api, type DownloadJob } from "../lib/api"; import { jobTitle } from "./downloadRow"; diff --git a/frontend/src/components/Messages.tsx b/frontend/src/components/Messages.tsx index 64559ff2..d7ed17a6 100644 --- a/frontend/src/components/Messages.tsx +++ b/frontend/src/components/Messages.tsx @@ -5,13 +5,13 @@ import { useQuery } from "@tanstack/react-query"; import { ArrowLeft, ShieldCheck, SquarePen, ExternalLink } from "lucide-react"; import { api, type Conversation, type MessageUser } from "../lib/api"; import { useLiveQuery } from "../lib/useLiveQuery"; -import { LoadingState, StateMessage } from "./QueryState"; +import { LoadingState, StateMessage } from "./ui/QueryState"; import { relativeTime } from "../lib/format"; import { partnerPub, POLL_MS, SYSTEM_ID, useKeyState } from "../lib/messaging"; import { useHistorySubview } from "../lib/history"; import { openChat } from "../lib/chatDock"; import * as e2ee from "../lib/e2ee"; -import Avatar from "./Avatar"; +import Avatar from "./ui/Avatar"; import KeyGate from "./KeyGate"; import { inputCls } from "./ui/form"; import ChatThread from "./ChatThread"; diff --git a/frontend/src/components/NavSidebar.tsx b/frontend/src/components/NavSidebar.tsx index 9e84fee9..71d05ae7 100644 --- a/frontend/src/components/NavSidebar.tsx +++ b/frontend/src/components/NavSidebar.tsx @@ -12,10 +12,10 @@ import type { Page } from "../lib/urlState"; import { moduleDef, moduleLabelKey, moduleOrder, SYSTEM_PAGES } from "../lib/modules"; import { useScrollFade } from "../lib/useScrollFade"; import { type LangCode } from "../i18n"; -import AvatarImg from "./Avatar"; -import Tooltip from "./Tooltip"; +import AvatarImg from "./ui/Avatar"; +import Tooltip from "./ui/Tooltip"; import LanguageSwitcher from "./LanguageSwitcher"; -import { PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; +import { PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; // Primary app navigation: a thin icon-only left rail. The modules used to hide under the avatar // dropdown; they now live here, each revealing its name in a hover/focus flyout label. Account diff --git a/frontend/src/components/NotificationsPanel.tsx b/frontend/src/components/NotificationsPanel.tsx index 3189ec68..1be66ff3 100644 --- a/frontend/src/components/NotificationsPanel.tsx +++ b/frontend/src/components/NotificationsPanel.tsx @@ -37,9 +37,9 @@ import { type VideoWatchedMeta, } from "../lib/notifications"; import { channelYouTubeUrl, relativeFromMs, relativeTime } from "../lib/format"; -import { LEVEL_STYLE } from "./Toaster"; -import { PageToolbar } from "./PageShell"; -import { LoadingState, StateMessage } from "./QueryState"; +import { LEVEL_STYLE } from "./ui/Toaster"; +import { PageToolbar } from "./ui/PageShell"; +import { LoadingState, StateMessage } from "./ui/QueryState"; import { useNavigationActions } from "./NavigationProvider"; import { useChannelsActions } from "./ChannelsProvider"; import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider"; diff --git a/frontend/src/components/OnboardingWizard.tsx b/frontend/src/components/OnboardingWizard.tsx index c963bf0b..46c962af 100644 --- a/frontend/src/components/OnboardingWizard.tsx +++ b/frontend/src/components/OnboardingWizard.tsx @@ -5,7 +5,7 @@ import { qk } from "../lib/queryKeys"; import { AlertTriangle, Check, Loader2, RefreshCw, ShieldCheck, X, Youtube } from "lucide-react"; import { api, type Me } from "../lib/api"; import { beginGrant, endOnboarding } from "../lib/onboarding"; -import Overlay from "./Overlay"; +import Overlay from "./ui/Overlay"; // A first-login wizard that walks the user through granting YouTube access one scope at a // time, each with a plain explanation and an up-front heads-up about Google's "unverified diff --git a/frontend/src/components/PersonalViewsModal.tsx b/frontend/src/components/PersonalViewsModal.tsx index 2a66c37e..ad5ea85e 100644 --- a/frontend/src/components/PersonalViewsModal.tsx +++ b/frontend/src/components/PersonalViewsModal.tsx @@ -17,13 +17,13 @@ import { } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { coercePlexFilters, type FeedFilters, type PlexFilters, type SavedView } from "../lib/api"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; import { Button } from "./ui/Button"; import { useSavedViews, asFilters } from "./SavedViewsProvider"; import { useActiveFilters } from "../lib/useActiveFilters"; import { buildPlexActiveFilters } from "./usePlexActiveFilters"; import { useUndoable } from "../lib/useUndoable"; -import UndoToolbar from "./UndoToolbar"; +import UndoToolbar from "./ui/UndoToolbar"; // Master–detail manager for saved ("personal") views: the left column lists the views (drag to // reorder) plus a "save the current filters" input; the right column shows the selected view's name diff --git a/frontend/src/components/PlayerModal.tsx b/frontend/src/components/PlayerModal.tsx index 67ca30ad..73875a7d 100644 --- a/frontend/src/components/PlayerModal.tsx +++ b/frontend/src/components/PlayerModal.tsx @@ -28,9 +28,9 @@ import { VolumeX, X, } from "lucide-react"; -import Avatar from "./Avatar"; +import Avatar from "./ui/Avatar"; import AddToPlaylist from "./AddToPlaylist"; -import { PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; +import { PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; import { pushLayer, removeLayer, anyLayerAbove, escapeHandled } from "../lib/layerStack"; import DownloadButton from "./DownloadButton"; import { api, type Video, type VideoStatus } from "../lib/api"; diff --git a/frontend/src/components/Playlists.tsx b/frontend/src/components/Playlists.tsx index da772928..2d82d550 100644 --- a/frontend/src/components/Playlists.tsx +++ b/frontend/src/components/Playlists.tsx @@ -42,8 +42,8 @@ import { cycleSort, type SortState } from "../lib/columnSort"; import { formatRuntime } from "../lib/format"; import { applyView, sameOrder } from "../lib/playlistView"; const PlayerModal = lazy(() => import("./PlayerModal")); -import UndoToolbar from "./UndoToolbar"; -import { PageToolbar } from "./PageShell"; +import UndoToolbar from "./ui/UndoToolbar"; +import { PageToolbar } from "./ui/PageShell"; import { useChoice, useConfirm } from "./ConfirmProvider"; import { usePlaylists, usePlaylistsActions } from "./PlaylistsProvider"; import { useMe } from "../lib/useMe"; @@ -54,7 +54,7 @@ import { isActive, type Column, type FilterMap, -} from "./tableHeader"; +} from "./ui/tableHeader"; import { GRIP_KEY, playlistColumns } from "./playlistColumns"; // The playlist detail is a HYBRID table (E4 S4): real sortable/filterable column headers over a diff --git a/frontend/src/components/PlaylistsRail.tsx b/frontend/src/components/PlaylistsRail.tsx index 3944b966..a23aab7f 100644 --- a/frontend/src/components/PlaylistsRail.tsx +++ b/frontend/src/components/PlaylistsRail.tsx @@ -10,8 +10,8 @@ import { notifyYouTubeActionError } from "../lib/youtubeErrors"; import { LS, readAccountMerged, writeAccount } from "../lib/storage"; import { type PanelLayout } from "../lib/panelLayout"; import FilterRail from "./FilterRail"; -import { type PanelItem } from "./PanelGroups"; -import SortControl from "./SortControl"; +import { type PanelItem } from "./ui/PanelGroups"; +import SortControl from "./ui/SortControl"; import { type SortOption } from "../lib/columnSort"; import { usePlaylists, usePlaylistsActions } from "./PlaylistsProvider"; import { useMe } from "../lib/useMe"; diff --git a/frontend/src/components/PlexBrowse.tsx b/frontend/src/components/PlexBrowse.tsx index f6df040d..f4ed08ea 100644 --- a/frontend/src/components/PlexBrowse.tsx +++ b/frontend/src/components/PlexBrowse.tsx @@ -25,15 +25,15 @@ import { useDebounced } from "../lib/useDebounced"; import { useHistorySubview } from "../lib/history"; import { PLEX_SUB_URL, type Sub } from "./plexSubUrl"; import { toggleWatchState } from "../lib/plexWatch"; -import SortControl from "./SortControl"; -import FilterSelect from "./FilterSelect"; +import SortControl from "./ui/SortControl"; +import FilterSelect from "./ui/FilterSelect"; import PlexDiscover from "./PlexDiscover"; import { PlexCollectionsBrowse, PlexPlaylistsBrowse } from "./plexBrowseLists"; -import { PageToolbar } from "./PageShell"; +import { PageToolbar } from "./ui/PageShell"; import ActiveFilterChips from "./ActiveFilterChips"; import SelectedViewChip from "./SelectedViewChip"; -import ItemCount from "./ItemCount"; -import { StateMessage } from "./QueryState"; +import ItemCount from "./ui/ItemCount"; +import { StateMessage } from "./ui/QueryState"; import { usePlexActiveFilters, usePlexClearFilters } from "./usePlexActiveFilters"; import { usePlex, usePlexActions } from "./PlexProvider"; import { usePublishPlexView } from "./PlexViewBridge"; diff --git a/frontend/src/components/PlexCollectionEditor.tsx b/frontend/src/components/PlexCollectionEditor.tsx index 17ba8b76..16855b9a 100644 --- a/frontend/src/components/PlexCollectionEditor.tsx +++ b/frontend/src/components/PlexCollectionEditor.tsx @@ -6,7 +6,7 @@ import { Check, Plus } from "lucide-react"; import { api } from "../lib/api"; import { notifyCollectionEditError } from "../lib/actionErrors"; import { useScrollFade } from "../lib/useScrollFade"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; import { useConfirm } from "./ConfirmProvider"; // Admin-only "manage this movie's collections" dialog (Collections P2). Lists the library's EDITABLE diff --git a/frontend/src/components/PlexCollectionView.tsx b/frontend/src/components/PlexCollectionView.tsx index fb6e7d6c..74efe5a8 100644 --- a/frontend/src/components/PlexCollectionView.tsx +++ b/frontend/src/components/PlexCollectionView.tsx @@ -13,7 +13,7 @@ import { BackBtn, ActionBtn, DetailHero } from "../lib/plexDetailUi"; import { PlexPosterCard } from "./plexCards"; import PlexLibraryPicker from "./PlexLibraryPicker"; import { toggleWatchState } from "../lib/plexWatch"; -import { LoadingState, StateMessage } from "./QueryState"; +import { LoadingState, StateMessage } from "./ui/QueryState"; // The Collection info page (ask B): a hero (art/title/cert/year-range/count/runtime/summary) + the // ordered member grid, a Play-all / Shuffle over the playable leaves, and — for a NON-external, diff --git a/frontend/src/components/PlexDiscover.tsx b/frontend/src/components/PlexDiscover.tsx index 4b23af4f..858c126b 100644 --- a/frontend/src/components/PlexDiscover.tsx +++ b/frontend/src/components/PlexDiscover.tsx @@ -8,9 +8,9 @@ import PosterTile from "./PosterTile"; import { plexLang } from "../i18n"; import { toggleWatchState } from "../lib/plexWatch"; import { usePlex } from "./PlexProvider"; -import { Carousel } from "./Carousel"; +import { Carousel } from "./ui/Carousel"; import { PlexPosterCard } from "./plexCards"; -import { LoadingState, StateMessage } from "./QueryState"; +import { LoadingState, StateMessage } from "./ui/QueryState"; // The Plex "Discover" landing (E5 S4): a stack of horizontal Carousel rails served from ONE // /api/plex/discover call — Continue watching, Recently added, Browse by genre (tiles), Because you diff --git a/frontend/src/components/PlexInfo.tsx b/frontend/src/components/PlexInfo.tsx index c4b07416..3174c666 100644 --- a/frontend/src/components/PlexInfo.tsx +++ b/frontend/src/components/PlexInfo.tsx @@ -16,7 +16,7 @@ import { } from "../lib/plexDetailUi"; import { usePref } from "../lib/usePref"; import i18n from "../i18n"; -import { Carousel } from "./Carousel"; +import { Carousel } from "./ui/Carousel"; import PlexCollectionEditor from "./PlexCollectionEditor"; import PlexPlaylistAdd from "./PlexPlaylistAdd"; import { PLAYABLE_TINT, playableClass } from "./plexCards"; diff --git a/frontend/src/components/PlexLibraryPicker.tsx b/frontend/src/components/PlexLibraryPicker.tsx index a689417a..e5ed2233 100644 --- a/frontend/src/components/PlexLibraryPicker.tsx +++ b/frontend/src/components/PlexLibraryPicker.tsx @@ -4,7 +4,7 @@ import { useQuery } from "@tanstack/react-query"; import { qk } from "../lib/queryKeys"; import { api, type PlexCard } from "../lib/api"; import { useDebounced } from "../lib/useDebounced"; -import { LoadingState } from "./QueryState"; +import { LoadingState } from "./ui/QueryState"; // A debounced library search-and-pick list, shared by the "Add title" panel (PlexCollectionView) and // the "New collection" first-title panel (plexBrowseLists) — both search the library the same way and diff --git a/frontend/src/components/PlexPlaylistAdd.tsx b/frontend/src/components/PlexPlaylistAdd.tsx index 884f3385..7f7f9be2 100644 --- a/frontend/src/components/PlexPlaylistAdd.tsx +++ b/frontend/src/components/PlexPlaylistAdd.tsx @@ -5,7 +5,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Check, Minus, Plus } from "lucide-react"; import { api, type PlexPlaylist } from "../lib/api"; import { useScrollFade } from "../lib/useScrollFade"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; // 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/PlexPlaylistView.tsx b/frontend/src/components/PlexPlaylistView.tsx index 0bb24816..4b42c3b4 100644 --- a/frontend/src/components/PlexPlaylistView.tsx +++ b/frontend/src/components/PlexPlaylistView.tsx @@ -35,7 +35,7 @@ import { api, type PlexCard } from "../lib/api"; import { formatRuntime } from "../lib/format"; import { getAccountRaw, LS, setAccountRaw } from "../lib/storage"; import { useConfirm } from "./ConfirmProvider"; -import { LoadingState, StateMessage } from "./QueryState"; +import { LoadingState, StateMessage } from "./ui/QueryState"; import ViewSwitcher, { type ViewOption } from "./ViewSwitcher"; // A playlist's items, grouped for readability: movies stay standalone, while a run of episodes from the diff --git a/frontend/src/components/PlexScopeToggle.tsx b/frontend/src/components/PlexScopeToggle.tsx index 6d436a69..d4a2ca15 100644 --- a/frontend/src/components/PlexScopeToggle.tsx +++ b/frontend/src/components/PlexScopeToggle.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import SegmentedToggle from "./SegmentedToggle"; +import SegmentedToggle from "./ui/SegmentedToggle"; import { usePlex, usePlexActions } from "./PlexProvider"; import { usePlexViewControls } from "./PlexViewBridge"; diff --git a/frontend/src/components/PlexSearchBox.tsx b/frontend/src/components/PlexSearchBox.tsx index 69905c6a..03b3dadd 100644 --- a/frontend/src/components/PlexSearchBox.tsx +++ b/frontend/src/components/PlexSearchBox.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { useQuery } from "@tanstack/react-query"; import { ChevronDown, Film, Tv } from "lucide-react"; import SearchBar from "./SearchBar"; -import ActionMenu from "./ActionMenu"; +import ActionMenu from "./ui/ActionMenu"; import { usePlex, usePlexActions } from "./PlexProvider"; import { api } from "../lib/api"; import { qk } from "../lib/queryKeys"; diff --git a/frontend/src/components/PlexSidebar.tsx b/frontend/src/components/PlexSidebar.tsx index 61e64dfd..fcdb92d2 100644 --- a/frontend/src/components/PlexSidebar.tsx +++ b/frontend/src/components/PlexSidebar.tsx @@ -9,10 +9,10 @@ import { type PanelLayout } from "../lib/panelLayout"; import { type MediaSourceKind } from "../lib/mediaSource"; import { useDebounced } from "../lib/useDebounced"; import FilterRail from "./FilterRail"; -import { type PanelItem } from "./PanelGroups"; -import SegmentedToggle from "./SegmentedToggle"; -import FilterChipGroup from "./FilterChipGroup"; -import Combobox, { type ComboboxOption } from "./Combobox"; +import { type PanelItem } from "./ui/PanelGroups"; +import SegmentedToggle from "./ui/SegmentedToggle"; +import FilterChipGroup from "./ui/FilterChipGroup"; +import Combobox, { type ComboboxOption } from "./ui/Combobox"; import SavedViewsPopover from "./SavedViewsPopover"; import PersonalViewsModal from "./PersonalViewsModal"; import { useSavedViews } from "./SavedViewsProvider"; diff --git a/frontend/src/components/PlexViewToggle.tsx b/frontend/src/components/PlexViewToggle.tsx index 61a60582..59fadede 100644 --- a/frontend/src/components/PlexViewToggle.tsx +++ b/frontend/src/components/PlexViewToggle.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import SegmentedToggle from "./SegmentedToggle"; +import SegmentedToggle from "./ui/SegmentedToggle"; import { usePlexViewControls } from "./PlexViewBridge"; // The Discover ⟷ Browse switch, hosted in the header next to the library-scope pill (All/Movies/Shows). diff --git a/frontend/src/components/ProfileEditor.tsx b/frontend/src/components/ProfileEditor.tsx index 7466b520..199b2689 100644 --- a/frontend/src/components/ProfileEditor.tsx +++ b/frontend/src/components/ProfileEditor.tsx @@ -3,7 +3,7 @@ import { qk } from "../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Lock, Pencil, Plus, Trash2 } from "lucide-react"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; import { useConfirm } from "./ConfirmProvider"; import { api, type DownloadProfile, type DownloadSpec } from "../lib/api"; import { inputCls } from "./ui/form"; diff --git a/frontend/src/components/ReleaseNotes.tsx b/frontend/src/components/ReleaseNotes.tsx index 1ec3ba00..70e0631f 100644 --- a/frontend/src/components/ReleaseNotes.tsx +++ b/frontend/src/components/ReleaseNotes.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { Bug, Sparkles } from "lucide-react"; import { RELEASE_NOTES } from "../lib/releaseNotes"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; function fmtDate(d: string): string { const t = new Date(d); diff --git a/frontend/src/components/SavedViewsPopover.tsx b/frontend/src/components/SavedViewsPopover.tsx index fe2a3296..877fea69 100644 --- a/frontend/src/components/SavedViewsPopover.tsx +++ b/frontend/src/components/SavedViewsPopover.tsx @@ -1,7 +1,7 @@ import { useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { Bookmark, Check, Settings2 } from "lucide-react"; -import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; +import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; import { useSavedViews } from "./SavedViewsProvider"; // The saved-views quick-picker. The filter-panel header's icon (and the toolbar's view chip) use it as diff --git a/frontend/src/components/Scheduler.tsx b/frontend/src/components/Scheduler.tsx index d0c2bc6d..6fae8a9c 100644 --- a/frontend/src/components/Scheduler.tsx +++ b/frontend/src/components/Scheduler.tsx @@ -19,11 +19,11 @@ import { } from "lucide-react"; import { api, type SchedulerJob, type SchedulerStatus } from "../lib/api"; import { useLiveQuery } from "../lib/useLiveQuery"; -import { LoadingState, StateMessage } from "./QueryState"; +import { LoadingState, StateMessage } from "./ui/QueryState"; import { useConfirm } from "./ConfirmProvider"; import { relativeTime } from "../lib/format"; import { notify } from "../lib/notifications"; -import Tooltip from "./Tooltip"; +import Tooltip from "./ui/Tooltip"; const POLL_MS = 4000; // While any job is running, poll faster so short scheduled runs aren't missed between ticks. diff --git a/frontend/src/components/SettingsPanel.tsx b/frontend/src/components/SettingsPanel.tsx index 8645b5cb..0cea96a9 100644 --- a/frontend/src/components/SettingsPanel.tsx +++ b/frontend/src/components/SettingsPanel.tsx @@ -7,17 +7,17 @@ import { Bell, Check, Loader2, Monitor, RefreshCw, Trash2, User } from "lucide-r import { SCHEMES, type Scheme, type ThemePrefs } from "../lib/theme"; import { api, HttpError, type Me } from "../lib/api"; import { LS, useAccountPersistedState } from "../lib/storage"; -import Avatar from "./Avatar"; +import Avatar from "./ui/Avatar"; import { notify, type NotifSettings } from "../lib/notifications"; -import Tooltip from "./Tooltip"; +import Tooltip from "./ui/Tooltip"; import { inputCls, Section, SettingRow, Switch } from "./ui/form"; import { Button } from "./ui/Button"; import { type SaveState } from "./ui/DraftSaveBar"; import { useConfirm } from "./ConfirmProvider"; -import { StateMessage } from "./QueryState"; +import { StateMessage } from "./ui/QueryState"; import { useWizard } from "./WizardProvider"; import { usePrefs } from "./PrefsProvider"; -import Tabs from "./Tabs"; +import Tabs from "./ui/Tabs"; import { useMe } from "../lib/useMe"; // The Settings page edits server-persisted preferences. Each change applies locally for instant diff --git a/frontend/src/components/ShareDialog.tsx b/frontend/src/components/ShareDialog.tsx index f8c72a27..5a44bf9d 100644 --- a/frontend/src/components/ShareDialog.tsx +++ b/frontend/src/components/ShareDialog.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Check, Copy, Link2, Lock, Trash2, UserPlus, Eye } from "lucide-react"; import clsx from "clsx"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; import { useConfirm } from "./ConfirmProvider"; import { notify } from "../lib/notifications"; import { copyText } from "../lib/clipboard"; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index efe3baa2..c83c0e1c 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -11,12 +11,12 @@ import TagManager from "./TagManager"; import SavedViewsPopover from "./SavedViewsPopover"; import { useSavedViews } from "./SavedViewsProvider"; import FilterRail from "./FilterRail"; -import { type PanelItem } from "./PanelGroups"; +import { type PanelItem } from "./ui/PanelGroups"; import { type MediaSourceKind } from "../lib/mediaSource"; import ChannelPicker from "./ChannelPicker"; import DateFilterSelect from "./DateFilterSelect"; import YearFilter from "./YearFilter"; -import Combobox, { type ComboboxOption } from "./Combobox"; +import Combobox, { type ComboboxOption } from "./ui/Combobox"; import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider"; import { useChannelsActions } from "./ChannelsProvider"; import { useMe } from "../lib/useMe"; diff --git a/frontend/src/components/Stats.tsx b/frontend/src/components/Stats.tsx index 734a17cd..7efc4342 100644 --- a/frontend/src/components/Stats.tsx +++ b/frontend/src/components/Stats.tsx @@ -9,10 +9,10 @@ import { notify } from "../lib/notifications"; import { notifyYouTubeActionError } from "../lib/youtubeErrors"; import { useWizard } from "./WizardProvider"; import { LS, useAccountPersistedState } from "../lib/storage"; -import Tooltip from "./Tooltip"; -import { LoadingState, StateMessage } from "./QueryState"; -import FilterChipGroup from "./FilterChipGroup"; -import Tabs from "./Tabs"; +import Tooltip from "./ui/Tooltip"; +import { LoadingState, StateMessage } from "./ui/QueryState"; +import FilterChipGroup from "./ui/FilterChipGroup"; +import Tabs from "./ui/Tabs"; import { Section, SettingRow } from "./ui/form"; import { useMe } from "../lib/useMe"; diff --git a/frontend/src/components/SyncStatus.tsx b/frontend/src/components/SyncStatus.tsx index ab396906..5d04fa71 100644 --- a/frontend/src/components/SyncStatus.tsx +++ b/frontend/src/components/SyncStatus.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import { CheckCircle2, Clock, Database, History, Loader2, Pause, Play } from "lucide-react"; import { api, type MyStatus } from "../lib/api"; import { formatViews } from "../lib/format"; -import { PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; +import { PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; // Per-user status (not the global catalog): shows the number of videos available to *this* // user, how many of *their* channels are still being fetched, and how many lack full diff --git a/frontend/src/components/TagFilterChips.tsx b/frontend/src/components/TagFilterChips.tsx index e2efd945..fb5590ed 100644 --- a/frontend/src/components/TagFilterChips.tsx +++ b/frontend/src/components/TagFilterChips.tsx @@ -2,8 +2,8 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { fitChipsWithPill } from "../lib/chipOverflow"; import { useChipOverflowFit } from "../lib/useChipOverflowFit"; -import { filterChipClass } from "./FilterChipGroup"; -import { useAnchoredPopover, PopoverSurface } from "./AnchoredPopover"; +import { filterChipClass } from "./ui/FilterChipGroup"; +import { useAnchoredPopover, PopoverSurface } from "./ui/AnchoredPopover"; // A single-line row of toggle filter chips that collapses its overflow into a trailing "+N" pill whose // popover holds the rest (still toggleable) — the same compression the feed's applied-filter row uses diff --git a/frontend/src/components/TagManager.tsx b/frontend/src/components/TagManager.tsx index acb7527c..156e457e 100644 --- a/frontend/src/components/TagManager.tsx +++ b/frontend/src/components/TagManager.tsx @@ -6,9 +6,9 @@ import { Plus, Trash2 } from "lucide-react"; import { api, type Tag } from "../lib/api"; import { notify } from "../lib/notifications"; import { useScrollFade } from "../lib/useScrollFade"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; import { useConfirm } from "./ConfirmProvider"; -import { PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; +import { PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; // One editable row: rename in place (commit on Enter/blur), delete with a confirm, and a // hover popover on the count listing the tagged channels (each a link that focuses it in the diff --git a/frontend/src/components/UploadDialog.tsx b/frontend/src/components/UploadDialog.tsx index c5eeebca..7b586b07 100644 --- a/frontend/src/components/UploadDialog.tsx +++ b/frontend/src/components/UploadDialog.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useQueryClient } from "@tanstack/react-query"; import { UploadCloud, File as FileIcon } from "lucide-react"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; import { Button } from "./ui/Button"; import { api } from "../lib/api"; import { invalidateDownloads } from "../lib/downloads"; diff --git a/frontend/src/components/VerifyBanner.tsx b/frontend/src/components/VerifyBanner.tsx index b38fae61..77c1f45c 100644 --- a/frontend/src/components/VerifyBanner.tsx +++ b/frontend/src/components/VerifyBanner.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; import { CheckCircle2, AlertTriangle } from "lucide-react"; -import Banner from "./Banner"; +import Banner from "./ui/Banner"; import type { VerifyEntry } from "../lib/useVerifyEntry"; // The top-of-app outcome of an emailed verification link opened in a browser that ALREADY holds a diff --git a/frontend/src/components/VersionBanner.tsx b/frontend/src/components/VersionBanner.tsx index d486bb68..93843c5b 100644 --- a/frontend/src/components/VersionBanner.tsx +++ b/frontend/src/components/VersionBanner.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Sparkles } from "lucide-react"; import { FRONTEND_VERSION, SEEN_VERSION_KEY } from "../lib/version"; -import Banner from "./Banner"; +import Banner from "./ui/Banner"; // Eye-catching, dismissible bar shown once after the running build's version changes // (compares the baked frontend version to the last one the user acknowledged). diff --git a/frontend/src/components/VideoCard.tsx b/frontend/src/components/VideoCard.tsx index f50c573f..da9103da 100644 --- a/frontend/src/components/VideoCard.tsx +++ b/frontend/src/components/VideoCard.tsx @@ -14,7 +14,7 @@ import { // Aliased: `Video` is this app's own domain type (lib/api), and it wins the name. Video as VideoIcon, } from "lucide-react"; -import Avatar from "./Avatar"; +import Avatar from "./ui/Avatar"; import AddToPlaylist from "./AddToPlaylist"; import DownloadButton from "./DownloadButton"; import VideoInfoCard from "./VideoInfoCard"; diff --git a/frontend/src/components/VideoEditor.tsx b/frontend/src/components/VideoEditor.tsx index 042956c9..8b5f563e 100644 --- a/frontend/src/components/VideoEditor.tsx +++ b/frontend/src/components/VideoEditor.tsx @@ -4,7 +4,7 @@ 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 from "./Modal"; +import Modal from "./ui/Modal"; import { notify } from "../lib/notifications"; import { api, type DownloadJob, type EditSpec } from "../lib/api"; import { contentRect, cropToSourcePx, type Frac } from "../lib/cropGeometry"; diff --git a/frontend/src/components/VideoInfoCard.tsx b/frontend/src/components/VideoInfoCard.tsx index bf95da9a..8acaad25 100644 --- a/frontend/src/components/VideoInfoCard.tsx +++ b/frontend/src/components/VideoInfoCard.tsx @@ -6,8 +6,8 @@ import { qk } from "../lib/queryKeys"; import { api, type Video } from "../lib/api"; import { formatDate, formatDuration, formatViews } from "../lib/format"; import { linkify } from "../lib/linkify"; -import Avatar from "./Avatar"; -import { PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; +import Avatar from "./ui/Avatar"; +import { PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; // The feed card's "more than the card face" hover infobox (E8·S3, decision D3): full (un-clamped) // title, channel, views·duration·date, a description snippet, and the channel's tags. It hangs off a diff --git a/frontend/src/components/ViewSwitcher.tsx b/frontend/src/components/ViewSwitcher.tsx index 836ae7cc..d1e0d730 100644 --- a/frontend/src/components/ViewSwitcher.tsx +++ b/frontend/src/components/ViewSwitcher.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { Check, ChevronDown, type LucideIcon } from "lucide-react"; import clsx from "clsx"; -import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; +import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; export interface ViewOption { id: T; diff --git a/frontend/src/components/VirtualFeed.tsx b/frontend/src/components/VirtualFeed.tsx index 5268ea39..22aa17bd 100644 --- a/frontend/src/components/VirtualFeed.tsx +++ b/frontend/src/components/VirtualFeed.tsx @@ -1,7 +1,7 @@ import type { Video, VideoStatus } from "../lib/api"; import { FEED_VIEW_SPEC, type FeedView } from "../lib/feedView"; import VideoCard from "./VideoCard"; -import VirtualGrid from "./VirtualGrid"; +import VirtualGrid from "./ui/VirtualGrid"; // Estimated row heights before measurement; real heights are measured per-row so these only affect // the very first paint and the scrollbar's initial guess. Taken from measuring each mode in the diff --git a/frontend/src/components/YearFilter.tsx b/frontend/src/components/YearFilter.tsx index 50eebe9e..12b8124e 100644 --- a/frontend/src/components/YearFilter.tsx +++ b/frontend/src/components/YearFilter.tsx @@ -4,9 +4,9 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { ChevronDown } from "lucide-react"; import { qk } from "../lib/queryKeys"; import { api, type FeedFilters } from "../lib/api"; -import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; -import { filterChipClass } from "./FilterChipGroup"; -import CountBadge from "./CountBadge"; +import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover"; +import { filterChipClass } from "./ui/FilterChipGroup"; +import CountBadge from "./ui/CountBadge"; import { useDebounced } from "../lib/useDebounced"; // The feed rail's "Year" control (item 8 / E8·S2): quick chips for the most recent years plus a diff --git a/frontend/src/components/adminInvites.dom.test.tsx b/frontend/src/components/adminInvites.dom.test.tsx index fb1dcbd9..12b7be7e 100644 --- a/frontend/src/components/adminInvites.dom.test.tsx +++ b/frontend/src/components/adminInvites.dom.test.tsx @@ -53,7 +53,7 @@ vi.mock("../lib/useMe", () => ({ useMe: () => meMock })); // The tab strip is rendered into an app-level toolbar slot that does not exist here, so pin the // active tab directly — this file is about the Access-requests panel, not the tab chrome. -vi.mock("./Tabs", () => ({ +vi.mock("./ui/Tabs", () => ({ default: () => null, usePersistedTab: () => ["access", () => {}], })); diff --git a/frontend/src/components/adminPendingAccess.dom.test.tsx b/frontend/src/components/adminPendingAccess.dom.test.tsx index bfedaa10..980eb7bd 100644 --- a/frontend/src/components/adminPendingAccess.dom.test.tsx +++ b/frontend/src/components/adminPendingAccess.dom.test.tsx @@ -46,7 +46,7 @@ const meMock = { vi.mock("../lib/useMe", () => ({ useMe: () => meMock })); // The tab strip renders into an app-level toolbar slot that does not exist here; pin the tab. -vi.mock("./Tabs", () => ({ +vi.mock("./ui/Tabs", () => ({ default: () => null, usePersistedTab: () => ["access", () => {}], })); diff --git a/frontend/src/components/auditColumns.tsx b/frontend/src/components/auditColumns.tsx index 25b1b3d7..40d406cc 100644 --- a/frontend/src/components/auditColumns.tsx +++ b/frontend/src/components/auditColumns.tsx @@ -2,7 +2,7 @@ import type { TFunction } from "i18next"; import { relativeTime, formatDate } from "../lib/format"; import i18n from "../i18n"; import type { AuditRow } from "../lib/api"; -import type { Column } from "./DataTable"; +import type { Column } from "./ui/DataTable"; function actionLabel(t: TFunction, action: string): string { // Canonical key → localized label; falls back to the raw key for any unmapped action. diff --git a/frontend/src/components/channelColumns.tsx b/frontend/src/components/channelColumns.tsx index bf9082c7..b8b831db 100644 --- a/frontend/src/components/channelColumns.tsx +++ b/frontend/src/components/channelColumns.tsx @@ -4,9 +4,9 @@ import { useTranslation } from "react-i18next"; import { Check, Eye, EyeOff, History, UserMinus } from "lucide-react"; import { NO_TAG_SENTINEL, type ManagedChannel } from "../lib/api"; import { formatCountOrDash, formatTotalHours, relativeTime } from "../lib/format"; -import type { Column } from "./DataTable"; -import Tooltip from "./Tooltip"; -import { PriorityStepper } from "./PriorityStepper"; +import type { Column } from "./ui/DataTable"; +import Tooltip from "./ui/Tooltip"; +import { PriorityStepper } from "./ui/PriorityStepper"; import ChannelLink from "./ChannelLink"; import AboutCell from "./AboutCell"; import { ChannelTagsControl, type SuggestedTag } from "./ChannelTags"; diff --git a/frontend/src/components/downloadAdmin.tsx b/frontend/src/components/downloadAdmin.tsx index c3f71898..1a4a6870 100644 --- a/frontend/src/components/downloadAdmin.tsx +++ b/frontend/src/components/downloadAdmin.tsx @@ -3,7 +3,7 @@ import { qk } from "../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Settings2 } from "lucide-react"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; import { useLiveQuery } from "../lib/useLiveQuery"; import { api } from "../lib/api"; import { formatBytes } from "../lib/format"; diff --git a/frontend/src/components/downloadModals.tsx b/frontend/src/components/downloadModals.tsx index 154d58da..0cf918e8 100644 --- a/frontend/src/components/downloadModals.tsx +++ b/frontend/src/components/downloadModals.tsx @@ -3,7 +3,7 @@ import { qk } from "../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Plus, Trash2 } from "lucide-react"; -import Modal from "./Modal"; +import Modal from "./ui/Modal"; import { api, type DownloadJob } from "../lib/api"; import { inputCls } from "./ui/form"; import { Button } from "./ui/Button"; diff --git a/frontend/src/components/playlistColumns.tsx b/frontend/src/components/playlistColumns.tsx index 6862514a..5d01b2e3 100644 --- a/frontend/src/components/playlistColumns.tsx +++ b/frontend/src/components/playlistColumns.tsx @@ -2,7 +2,7 @@ import { Play, X } from "lucide-react"; import type { TFunction } from "i18next"; import type { Video } from "../lib/api"; import { formatDuration } from "../lib/format"; -import type { Column } from "./tableHeader"; +import type { Column } from "./ui/tableHeader"; // Column definitions for the playlist detail table, in their own file like `channelColumns.tsx`. // diff --git a/frontend/src/components/plexBrowseLists.tsx b/frontend/src/components/plexBrowseLists.tsx index a487bd22..07187a74 100644 --- a/frontend/src/components/plexBrowseLists.tsx +++ b/frontend/src/components/plexBrowseLists.tsx @@ -8,7 +8,7 @@ import { api, isIntrinsicReadOnly, type PlexCard, type PlexCollection } from ".. import { formatRuntime } from "../lib/format"; import { notifyCollectionEditError } from "../lib/actionErrors"; import { useDebounced } from "../lib/useDebounced"; -import { LoadingState, StateMessage } from "./QueryState"; +import { LoadingState, StateMessage } from "./ui/QueryState"; import PosterTile from "./PosterTile"; import PlexLibraryPicker from "./PlexLibraryPicker"; diff --git a/frontend/src/components/plexCards.tsx b/frontend/src/components/plexCards.tsx index 7608fe2e..d4d6ec73 100644 --- a/frontend/src/components/plexCards.tsx +++ b/frontend/src/components/plexCards.tsx @@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next"; import { CheckCircle2, Circle, Film, Info, ListPlus, Play, Star, Tv2 } from "lucide-react"; import { type CSSProperties } from "react"; import { formatRuntime } from "../lib/format"; -import { Carousel } from "./Carousel"; +import { Carousel } from "./ui/Carousel"; import type { PlexCard, PlexCastMember, PlexFilters, PlexSeasonDetail } from "../lib/api"; // Presentational Plex cards + strips, split out of PlexBrowse (all purely prop-driven, no shared diff --git a/frontend/src/components/plexViews.tsx b/frontend/src/components/plexViews.tsx index 2aa2c5d4..45488292 100644 --- a/frontend/src/components/plexViews.tsx +++ b/frontend/src/components/plexViews.tsx @@ -22,10 +22,10 @@ import { WatchedToggle, } from "../lib/plexDetailUi"; import { CastStrip, EpisodeCard, PlexPosterCard, RelatedStrip, SeasonCard } from "./plexCards"; -import { Carousel } from "./Carousel"; +import { Carousel } from "./ui/Carousel"; import PlexPlaylistAdd, { type PlexAddTarget } from "./PlexPlaylistAdd"; import PlexCollectionEditor from "./PlexCollectionEditor"; -import { LoadingState, StateMessage } from "./QueryState"; +import { LoadingState, StateMessage } from "./ui/QueryState"; // The Plex sub-router's view branches, split out of PlexBrowse. Each is invoked by PlexBrowse with // shell callbacks as props (onBack/onPlay/onFilter/…) — no view reaches back into the shell's state, diff --git a/frontend/src/components/ActionMenu.dom.test.tsx b/frontend/src/components/ui/ActionMenu.dom.test.tsx similarity index 100% rename from frontend/src/components/ActionMenu.dom.test.tsx rename to frontend/src/components/ui/ActionMenu.dom.test.tsx diff --git a/frontend/src/components/ActionMenu.tsx b/frontend/src/components/ui/ActionMenu.tsx similarity index 100% rename from frontend/src/components/ActionMenu.tsx rename to frontend/src/components/ui/ActionMenu.tsx diff --git a/frontend/src/components/AnchoredPopover.dom.test.tsx b/frontend/src/components/ui/AnchoredPopover.dom.test.tsx similarity index 100% rename from frontend/src/components/AnchoredPopover.dom.test.tsx rename to frontend/src/components/ui/AnchoredPopover.dom.test.tsx diff --git a/frontend/src/components/AnchoredPopover.tsx b/frontend/src/components/ui/AnchoredPopover.tsx similarity index 99% rename from frontend/src/components/AnchoredPopover.tsx rename to frontend/src/components/ui/AnchoredPopover.tsx index 8f980402..a7a57b04 100644 --- a/frontend/src/components/AnchoredPopover.tsx +++ b/frontend/src/components/ui/AnchoredPopover.tsx @@ -36,8 +36,8 @@ import { type Placement, type UseRoleProps, } from "@floating-ui/react"; -import { useLayer } from "../lib/layerStack"; -import { useFullscreenElement } from "../lib/fullscreen"; +import { useLayer } from "../../lib/layerStack"; +import { useFullscreenElement } from "../../lib/fullscreen"; // R8 popover platform. ONE anchored-popover primitive over @floating-ui/react, replacing the ~9 // hand-rolled positioning engines (measured flip / width clamp / rAF scroll-follow / scroller-relative diff --git a/frontend/src/components/Avatar.tsx b/frontend/src/components/ui/Avatar.tsx similarity index 100% rename from frontend/src/components/Avatar.tsx rename to frontend/src/components/ui/Avatar.tsx diff --git a/frontend/src/components/BackToTop.tsx b/frontend/src/components/ui/BackToTop.tsx similarity index 100% rename from frontend/src/components/BackToTop.tsx rename to frontend/src/components/ui/BackToTop.tsx diff --git a/frontend/src/components/Banner.tsx b/frontend/src/components/ui/Banner.tsx similarity index 100% rename from frontend/src/components/Banner.tsx rename to frontend/src/components/ui/Banner.tsx diff --git a/frontend/src/components/Carousel.dom.test.tsx b/frontend/src/components/ui/Carousel.dom.test.tsx similarity index 100% rename from frontend/src/components/Carousel.dom.test.tsx rename to frontend/src/components/ui/Carousel.dom.test.tsx diff --git a/frontend/src/components/Carousel.tsx b/frontend/src/components/ui/Carousel.tsx similarity index 98% rename from frontend/src/components/Carousel.tsx rename to frontend/src/components/ui/Carousel.tsx index 46f4affa..3ef705b4 100644 --- a/frontend/src/components/Carousel.tsx +++ b/frontend/src/components/ui/Carousel.tsx @@ -2,7 +2,7 @@ import { useCallback, type ReactNode } from "react"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { useTranslation } from "react-i18next"; import clsx from "clsx"; -import { useScrollEdges } from "../lib/useScrollEdges"; +import { useScrollEdges } from "../../lib/useScrollEdges"; // Shared horizontal rail — the one place the app's "row of cards you scroll sideways" lives, replacing // the hand-rolled `flex gap overflow-x-auto` strips (Cast / Related / collections) and the future diff --git a/frontend/src/components/ColumnSortControl.tsx b/frontend/src/components/ui/ColumnSortControl.tsx similarity index 90% rename from frontend/src/components/ColumnSortControl.tsx rename to frontend/src/components/ui/ColumnSortControl.tsx index 2599ac1c..ca4c5f69 100644 --- a/frontend/src/components/ColumnSortControl.tsx +++ b/frontend/src/components/ui/ColumnSortControl.tsx @@ -1,6 +1,6 @@ import { type Column } from "./DataTable"; -import { type SortState } from "../lib/columnSort"; -import { optionsFromColumns } from "../lib/sortOptions"; +import { type SortState } from "../../lib/columnSort"; +import { optionsFromColumns } from "../../lib/sortOptions"; import SortControl from "./SortControl"; // The headerless sort control for views without clickable column headers (the channel manager's and diff --git a/frontend/src/components/Combobox.dom.test.tsx b/frontend/src/components/ui/Combobox.dom.test.tsx similarity index 100% rename from frontend/src/components/Combobox.dom.test.tsx rename to frontend/src/components/ui/Combobox.dom.test.tsx diff --git a/frontend/src/components/Combobox.tsx b/frontend/src/components/ui/Combobox.tsx similarity index 99% rename from frontend/src/components/Combobox.tsx rename to frontend/src/components/ui/Combobox.tsx index 9cf34c78..600bf6f2 100644 --- a/frontend/src/components/Combobox.tsx +++ b/frontend/src/components/ui/Combobox.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } fro import { useTranslation } from "react-i18next"; import { Check, ChevronDown, X } from "lucide-react"; import { useAnchoredPopover, PopoverSurface, PopoverItem } from "./AnchoredPopover"; -import { useScrollFade } from "../lib/useScrollFade"; +import { useScrollFade } from "../../lib/useScrollFade"; // The universal combobox: a react-select / Overseerr-style "combi input" — selected values are // removable chips that live INSIDE a text field you type into, and a checklist of options opens below. diff --git a/frontend/src/components/CountBadge.tsx b/frontend/src/components/ui/CountBadge.tsx similarity index 100% rename from frontend/src/components/CountBadge.tsx rename to frontend/src/components/ui/CountBadge.tsx diff --git a/frontend/src/components/DataTable.fixedLayout.dom.test.tsx b/frontend/src/components/ui/DataTable.fixedLayout.dom.test.tsx similarity index 100% rename from frontend/src/components/DataTable.fixedLayout.dom.test.tsx rename to frontend/src/components/ui/DataTable.fixedLayout.dom.test.tsx diff --git a/frontend/src/components/DataTable.tsx b/frontend/src/components/ui/DataTable.tsx similarity index 99% rename from frontend/src/components/DataTable.tsx rename to frontend/src/components/ui/DataTable.tsx index 8173ce6e..b8271eba 100644 --- a/frontend/src/components/DataTable.tsx +++ b/frontend/src/components/ui/DataTable.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { sortRows, cycleSort, type SortState } from "../lib/columnSort"; +import { sortRows, cycleSort, type SortState } from "../../lib/columnSort"; import Pager, { hasPagerContent } from "./Pager"; import { PageToolbar } from "./PageShell"; import { ColumnHead, alignClass, filterRows, type Column, type FilterMap } from "./tableHeader"; diff --git a/frontend/src/components/FilterChipGroup.dom.test.tsx b/frontend/src/components/ui/FilterChipGroup.dom.test.tsx similarity index 100% rename from frontend/src/components/FilterChipGroup.dom.test.tsx rename to frontend/src/components/ui/FilterChipGroup.dom.test.tsx diff --git a/frontend/src/components/FilterChipGroup.tsx b/frontend/src/components/ui/FilterChipGroup.tsx similarity index 100% rename from frontend/src/components/FilterChipGroup.tsx rename to frontend/src/components/ui/FilterChipGroup.tsx diff --git a/frontend/src/components/FilterSelect.dom.test.tsx b/frontend/src/components/ui/FilterSelect.dom.test.tsx similarity index 100% rename from frontend/src/components/FilterSelect.dom.test.tsx rename to frontend/src/components/ui/FilterSelect.dom.test.tsx diff --git a/frontend/src/components/FilterSelect.tsx b/frontend/src/components/ui/FilterSelect.tsx similarity index 100% rename from frontend/src/components/FilterSelect.tsx rename to frontend/src/components/ui/FilterSelect.tsx diff --git a/frontend/src/components/InlineNotice.tsx b/frontend/src/components/ui/InlineNotice.tsx similarity index 100% rename from frontend/src/components/InlineNotice.tsx rename to frontend/src/components/ui/InlineNotice.tsx diff --git a/frontend/src/components/ItemCount.dom.test.tsx b/frontend/src/components/ui/ItemCount.dom.test.tsx similarity index 96% rename from frontend/src/components/ItemCount.dom.test.tsx rename to frontend/src/components/ui/ItemCount.dom.test.tsx index 560adc93..5295edb4 100644 --- a/frontend/src/components/ItemCount.dom.test.tsx +++ b/frontend/src/components/ui/ItemCount.dom.test.tsx @@ -5,7 +5,7 @@ // those are the two branches worth guarding. English is forced so the singular/plural strings are stable. import { describe, it, expect, beforeAll, afterEach } from "vitest"; import { render, screen, cleanup } from "@testing-library/react"; -import "../i18n"; // side-effect: initialise the shared i18next instance +import "../../i18n"; // side-effect: initialise the shared i18next instance import i18n from "i18next"; import ItemCount from "./ItemCount"; diff --git a/frontend/src/components/ItemCount.tsx b/frontend/src/components/ui/ItemCount.tsx similarity index 100% rename from frontend/src/components/ItemCount.tsx rename to frontend/src/components/ui/ItemCount.tsx diff --git a/frontend/src/components/Modal.dom.test.tsx b/frontend/src/components/ui/Modal.dom.test.tsx similarity index 98% rename from frontend/src/components/Modal.dom.test.tsx rename to frontend/src/components/ui/Modal.dom.test.tsx index d622314c..ef886c69 100644 --- a/frontend/src/components/Modal.dom.test.tsx +++ b/frontend/src/components/ui/Modal.dom.test.tsx @@ -17,7 +17,7 @@ vi.mock("react-i18next", async (importOriginal) => { return { ...actual, useTranslation: () => ({ t: (key: string) => key }) }; }); // Isolate the focus trap from the Back-to-close history stack (tested in history.dom.test.ts). -vi.mock("../lib/history", () => ({ useBackToClose: () => {} })); +vi.mock("../../lib/history", () => ({ useBackToClose: () => {} })); import Modal from "./Modal"; diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/ui/Modal.tsx similarity index 97% rename from frontend/src/components/Modal.tsx rename to frontend/src/components/ui/Modal.tsx index 295986d1..fb819d8b 100644 --- a/frontend/src/components/Modal.tsx +++ b/frontend/src/components/ui/Modal.tsx @@ -2,9 +2,9 @@ import { useEffect, useId, useRef, useState, type ReactNode } from "react"; import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; import { X } from "lucide-react"; -import { useBackToClose } from "../lib/history"; -import { pushLayer, removeLayer, isTopmost } from "../lib/layerStack"; -import { useFullscreenElement } from "../lib/fullscreen"; +import { useBackToClose } from "../../lib/history"; +import { pushLayer, removeLayer, isTopmost } from "../../lib/layerStack"; +import { useFullscreenElement } from "../../lib/fullscreen"; // Tabbable elements inside the dialog, in DOM order. `:not([tabindex="-1"])` drops programmatically // focusable-only nodes; the visibility filter (offsetParent) drops display:none ones (e.g. a diff --git a/frontend/src/components/Overlay.tsx b/frontend/src/components/ui/Overlay.tsx similarity index 100% rename from frontend/src/components/Overlay.tsx rename to frontend/src/components/ui/Overlay.tsx diff --git a/frontend/src/components/PageScroller.dom.test.tsx b/frontend/src/components/ui/PageScroller.dom.test.tsx similarity index 98% rename from frontend/src/components/PageScroller.dom.test.tsx rename to frontend/src/components/ui/PageScroller.dom.test.tsx index 872857be..94834eb9 100644 --- a/frontend/src/components/PageScroller.dom.test.tsx +++ b/frontend/src/components/ui/PageScroller.dom.test.tsx @@ -15,7 +15,7 @@ vi.mock("react-i18next", async (importOriginal) => { }); import PageScroller from "./PageScroller"; -import { notifyNav } from "../lib/history"; +import { notifyNav } from "../../lib/history"; beforeAll(() => { // useScrollFade wires a ResizeObserver (watch-the-children); jsdom ships none. diff --git a/frontend/src/components/PageScroller.tsx b/frontend/src/components/ui/PageScroller.tsx similarity index 98% rename from frontend/src/components/PageScroller.tsx rename to frontend/src/components/ui/PageScroller.tsx index 7733f3da..93d3749a 100644 --- a/frontend/src/components/PageScroller.tsx +++ b/frontend/src/components/ui/PageScroller.tsx @@ -1,6 +1,6 @@ import { useCallback, useLayoutEffect, useRef, type ReactNode } from "react"; -import { useScrollFade } from "../lib/useScrollFade"; -import { ensureNavId, peekNavId, subscribeNav } from "../lib/history"; +import { useScrollFade } from "../../lib/useScrollFade"; +import { ensureNavId, peekNavId, subscribeNav } from "../../lib/history"; // Saved scroll offsets, keyed by `${historyEntryId}:${scrollKey}`. The entry id ties an offset to the // exact history entry, so Back restores where you left it and a forward navigation (a new entry) opens diff --git a/frontend/src/components/PageShell.dom.test.tsx b/frontend/src/components/ui/PageShell.dom.test.tsx similarity index 100% rename from frontend/src/components/PageShell.dom.test.tsx rename to frontend/src/components/ui/PageShell.dom.test.tsx diff --git a/frontend/src/components/PageShell.tsx b/frontend/src/components/ui/PageShell.tsx similarity index 100% rename from frontend/src/components/PageShell.tsx rename to frontend/src/components/ui/PageShell.tsx diff --git a/frontend/src/components/Pager.tsx b/frontend/src/components/ui/Pager.tsx similarity index 100% rename from frontend/src/components/Pager.tsx rename to frontend/src/components/ui/Pager.tsx diff --git a/frontend/src/components/PanelGroup.tsx b/frontend/src/components/ui/PanelGroup.tsx similarity index 100% rename from frontend/src/components/PanelGroup.tsx rename to frontend/src/components/ui/PanelGroup.tsx diff --git a/frontend/src/components/PanelGroups.tsx b/frontend/src/components/ui/PanelGroups.tsx similarity index 98% rename from frontend/src/components/PanelGroups.tsx rename to frontend/src/components/ui/PanelGroups.tsx index 93892e77..82d494cb 100644 --- a/frontend/src/components/PanelGroups.tsx +++ b/frontend/src/components/ui/PanelGroups.tsx @@ -14,7 +14,7 @@ import { sortableKeyboardCoordinates, verticalListSortingStrategy, } from "@dnd-kit/sortable"; -import type { PanelLayout } from "../lib/panelLayout"; +import type { PanelLayout } from "../../lib/panelLayout"; import PanelGroup from "./PanelGroup"; export type PanelItem = { diff --git a/frontend/src/components/PriorityStepper.dom.test.tsx b/frontend/src/components/ui/PriorityStepper.dom.test.tsx similarity index 100% rename from frontend/src/components/PriorityStepper.dom.test.tsx rename to frontend/src/components/ui/PriorityStepper.dom.test.tsx diff --git a/frontend/src/components/PriorityStepper.tsx b/frontend/src/components/ui/PriorityStepper.tsx similarity index 100% rename from frontend/src/components/PriorityStepper.tsx rename to frontend/src/components/ui/PriorityStepper.tsx diff --git a/frontend/src/components/QueryState.tsx b/frontend/src/components/ui/QueryState.tsx similarity index 100% rename from frontend/src/components/QueryState.tsx rename to frontend/src/components/ui/QueryState.tsx diff --git a/frontend/src/components/ui/README.md b/frontend/src/components/ui/README.md new file mode 100644 index 00000000..86cfa80d --- /dev/null +++ b/frontend/src/components/ui/README.md @@ -0,0 +1,44 @@ +# `components/ui/` — the platform layer + +Everything in this directory is a **platform component**: generic, reusable, and **domain-free**. +Everything in `components/` above it is a **module component**: it knows about videos, channels, +playlists, Plex or downloads. + +That boundary is the whole point of this directory. Before E10 S1 these 33 modules sat flat among +~160 feature components, and the platform was invisible — several of them literally describe +themselves as "the one place", "shared", "canonical", "app-wide", and were still routinely bypassed +by a hand-rolled `