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.
This commit is contained in:
@@ -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,
|
prettier,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ import Welcome from "./components/Welcome";
|
|||||||
import SetupWizard from "./components/SetupWizard";
|
import SetupWizard from "./components/SetupWizard";
|
||||||
import Header from "./components/Header";
|
import Header from "./components/Header";
|
||||||
import NavSidebar from "./components/NavSidebar";
|
import NavSidebar from "./components/NavSidebar";
|
||||||
import BackToTop from "./components/BackToTop";
|
import BackToTop from "./components/ui/BackToTop";
|
||||||
import PageShell from "./components/PageShell";
|
import PageShell from "./components/ui/PageShell";
|
||||||
import GlassTuner from "./components/GlassTuner";
|
import GlassTuner from "./components/GlassTuner";
|
||||||
import ChatDock from "./components/ChatDock";
|
import ChatDock from "./components/ChatDock";
|
||||||
import Toaster from "./components/Toaster";
|
import Toaster from "./components/ui/Toaster";
|
||||||
import { StateMessage } from "./components/QueryState";
|
import { StateMessage } from "./components/ui/QueryState";
|
||||||
import ErrorDialog from "./components/ErrorDialog";
|
import ErrorDialog from "./components/ErrorDialog";
|
||||||
import VersionBanner from "./components/VersionBanner";
|
import VersionBanner from "./components/VersionBanner";
|
||||||
import DemoBanner from "./components/DemoBanner";
|
import DemoBanner from "./components/DemoBanner";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Info, Sparkles } from "lucide-react";
|
import { Info, Sparkles } from "lucide-react";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { FRONTEND_VERSION, FRONTEND_SHA, FRONTEND_BUILD_DATE } from "../lib/version";
|
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 {
|
function fmtDate(d?: string | null): string {
|
||||||
if (!d) return "—";
|
if (!d) return "—";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../lib/queryKeys";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import ChannelAboutContent from "./ChannelAbout";
|
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"
|
* A channel-description cell: one truncated line in the table, revealing the FULL channel "About"
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Check, Clock, Copy, Globe, KeyRound, Mail, Send, ShieldAlert } from "lucide-react";
|
import { Check, Clock, Copy, Globe, KeyRound, Mail, Send, ShieldAlert } from "lucide-react";
|
||||||
import Modal from "./Modal";
|
import Modal from "./ui/Modal";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../lib/queryKeys";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../lib/notifications";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Filter, X } from "lucide-react";
|
|||||||
import type { ActiveFilter } from "../lib/useActiveFilters";
|
import type { ActiveFilter } from "../lib/useActiveFilters";
|
||||||
import { decideChipLayout } from "../lib/chipOverflow";
|
import { decideChipLayout } from "../lib/chipOverflow";
|
||||||
import { useChipOverflowFit } from "../lib/useChipOverflowFit";
|
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
|
// 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
|
// own. It exists because the filter panel now sits collapsed as a tab — without this you'd have to
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Check, ListPlus, Plus, Youtube } from "lucide-react";
|
|||||||
import { api, type Playlist } from "../lib/api";
|
import { api, type Playlist } from "../lib/api";
|
||||||
import { useScrollFade } from "../lib/useScrollFade";
|
import { useScrollFade } from "../lib/useScrollFade";
|
||||||
import { playlistName } from "../lib/playlistName";
|
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
|
// 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
|
// "new playlist" creation. Same shape as the channel tag editor (R8 S1's pilot): radio-style
|
||||||
|
|||||||
@@ -26,16 +26,16 @@ import {
|
|||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../lib/notifications";
|
||||||
import { invalidateUserAccess, isEmailOff } from "../lib/adminUsers";
|
import { invalidateUserAccess, isEmailOff } from "../lib/adminUsers";
|
||||||
import { ADMIN_USERS_TAB_KEY } from "../lib/adminUsersTab";
|
import { ADMIN_USERS_TAB_KEY } from "../lib/adminUsersTab";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./ui/Tooltip";
|
||||||
import InlineNotice from "./InlineNotice";
|
import InlineNotice from "./ui/InlineNotice";
|
||||||
import { Section } from "./ui/form";
|
import { Section } from "./ui/form";
|
||||||
import SegmentedToggle from "./SegmentedToggle";
|
import SegmentedToggle from "./ui/SegmentedToggle";
|
||||||
import { LANGUAGES, type LangCode } from "../i18n";
|
import { LANGUAGES, type LangCode } from "../i18n";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
import AccountPanel from "./AccountPanel";
|
import AccountPanel from "./AccountPanel";
|
||||||
import { useConfirm } from "./ConfirmProvider";
|
import { useConfirm } from "./ConfirmProvider";
|
||||||
import Tabs, { usePersistedTab } from "./Tabs";
|
import Tabs, { usePersistedTab } from "./ui/Tabs";
|
||||||
import { PageToolbar } from "./PageShell";
|
import { PageToolbar } from "./ui/PageShell";
|
||||||
import { useMe } from "../lib/useMe";
|
import { useMe } from "../lib/useMe";
|
||||||
|
|
||||||
// Admin-only user & access management: roles, access requests (the Invite whitelist), and the
|
// Admin-only user & access management: roles, access requests (the Invite whitelist), and the
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { notify } from "../lib/notifications";
|
|||||||
import { LS } from "../lib/storage";
|
import { LS } from "../lib/storage";
|
||||||
import { Section } from "./ui/form";
|
import { Section } from "./ui/form";
|
||||||
import { useConfirm } from "./ConfirmProvider";
|
import { useConfirm } from "./ConfirmProvider";
|
||||||
import DataTable from "./DataTable";
|
import DataTable from "./ui/DataTable";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
import { auditColumns } from "./auditColumns";
|
import { auditColumns } from "./auditColumns";
|
||||||
|
|
||||||
// Admin-only audit trail: an append-only who/what/when log of admin actions + scheduler run
|
// Admin-only audit trail: an append-only who/what/when log of admin actions + scheduler run
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ import { useCardPager } from "../lib/useCardPager";
|
|||||||
import { subsColumn } from "./channelColumns";
|
import { subsColumn } from "./channelColumns";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../lib/notifications";
|
||||||
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
|
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./ui/Tooltip";
|
||||||
import ChannelLink from "./ChannelLink";
|
import ChannelLink from "./ChannelLink";
|
||||||
import AboutCell from "./AboutCell";
|
import AboutCell from "./AboutCell";
|
||||||
import DataTable, { type Column } from "./DataTable";
|
import DataTable, { type Column } from "./ui/DataTable";
|
||||||
import ChannelLayoutGrid from "./ChannelLayoutGrid";
|
import ChannelLayoutGrid from "./ChannelLayoutGrid";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
import ColumnSortControl from "./ColumnSortControl";
|
import ColumnSortControl from "./ui/ColumnSortControl";
|
||||||
import Pager from "./Pager";
|
import Pager from "./ui/Pager";
|
||||||
import { PageToolbar } from "./PageShell";
|
import { PageToolbar } from "./ui/PageShell";
|
||||||
import { useConfirm } from "./ConfirmProvider";
|
import { useConfirm } from "./ConfirmProvider";
|
||||||
import { useChannelLayout } from "./ChannelsProvider";
|
import { useChannelLayout } from "./ChannelsProvider";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { type Column } from "./DataTable";
|
import { type Column } from "./ui/DataTable";
|
||||||
import VirtualGrid from "./VirtualGrid";
|
import VirtualGrid from "./ui/VirtualGrid";
|
||||||
import { CHANNEL_LAYOUT_SPEC } from "../lib/channelLayout";
|
import { CHANNEL_LAYOUT_SPEC } from "../lib/channelLayout";
|
||||||
|
|
||||||
// The channel manager's `cards` layout (E4 S3), built by reusing the SAME `Column<T>` render closures
|
// The channel manager's `cards` layout (E4 S3), built by reusing the SAME `Column<T>` render closures
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./ui/Avatar";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./ui/Tooltip";
|
||||||
import { channelYouTubeUrl } from "../lib/format";
|
import { channelYouTubeUrl } from "../lib/format";
|
||||||
|
|
||||||
// Avatar + channel name + "open on YouTube" link, shared by the Channel manager and the
|
// Avatar + channel name + "open on YouTube" link, shared by the Channel manager and the
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useLiveQuery } from "../lib/useLiveQuery";
|
|||||||
import { channelDetailPollMs } from "../lib/channelDetailPoll";
|
import { channelDetailPollMs } from "../lib/channelDetailPoll";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../lib/queryKeys";
|
||||||
import { useNavigationActions } from "./NavigationProvider";
|
import { useNavigationActions } from "./NavigationProvider";
|
||||||
import { PriorityStepper } from "./PriorityStepper";
|
import { PriorityStepper } from "./ui/PriorityStepper";
|
||||||
import { useChannelPriority } from "../lib/useChannelPriority";
|
import { useChannelPriority } from "../lib/useChannelPriority";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
@@ -20,11 +20,11 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./ui/Avatar";
|
||||||
import Tabs from "./Tabs";
|
import Tabs from "./ui/Tabs";
|
||||||
import Feed from "./Feed";
|
import Feed from "./Feed";
|
||||||
import ChannelAboutContent from "./ChannelAbout";
|
import ChannelAboutContent from "./ChannelAbout";
|
||||||
import { PageToolbar, PageToolbarSlot } from "./PageShell";
|
import { PageToolbar, PageToolbarSlot } from "./ui/PageShell";
|
||||||
import { useFeedFilters } from "./FeedFiltersProvider";
|
import { useFeedFilters } from "./FeedFiltersProvider";
|
||||||
import { useConfirm } from "./ConfirmProvider";
|
import { useConfirm } from "./ConfirmProvider";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../lib/notifications";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../lib/queryKeys";
|
||||||
import { api, type FeedFilters } from "../lib/api";
|
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
|
// 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:
|
// across all of them. Until this existed the filter was settable only via a shared "?channel=" link:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { type MyStatus } from "../lib/api";
|
import { type MyStatus } from "../lib/api";
|
||||||
import { formatEta } from "../lib/format";
|
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
|
// 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
|
// that reveals the numbers on hover (reuses the AnchoredPopover hover mode). One row per stat, each
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { Check, Plus, Sparkles, X } from "lucide-react";
|
import { Check, Plus, Sparkles, X } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { type Tag } from "../lib/api";
|
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 { useScrollFade } from "../lib/useScrollFade";
|
||||||
import { fitChipsWithPill } from "../lib/chipOverflow";
|
import { fitChipsWithPill } from "../lib/chipOverflow";
|
||||||
import { useChipOverflowFit } from "../lib/useChipOverflowFit";
|
import { useChipOverflowFit } from "../lib/useChipOverflowFit";
|
||||||
|
|||||||
@@ -10,26 +10,26 @@ import { notifyYouTubeActionError } from "../lib/youtubeErrors";
|
|||||||
import { accountKey, LS } from "../lib/storage";
|
import { accountKey, LS } from "../lib/storage";
|
||||||
import { buildChannelColumns, applySyncFilter } from "./channelColumns";
|
import { buildChannelColumns, applySyncFilter } from "./channelColumns";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../lib/notifications";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./ui/Tooltip";
|
||||||
import DataTable from "./DataTable";
|
import DataTable from "./ui/DataTable";
|
||||||
import Tabs, { type TabDef } from "./Tabs";
|
import Tabs, { type TabDef } from "./ui/Tabs";
|
||||||
import ChannelLayoutGrid from "./ChannelLayoutGrid";
|
import ChannelLayoutGrid from "./ChannelLayoutGrid";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
import { useChannelTagActions } from "../lib/useChannelTagActions";
|
import { useChannelTagActions } from "../lib/useChannelTagActions";
|
||||||
import { useTagSuggestions } from "./useTagSuggestions";
|
import { useTagSuggestions } from "./useTagSuggestions";
|
||||||
import { useChannelPriority } from "../lib/useChannelPriority";
|
import { useChannelPriority } from "../lib/useChannelPriority";
|
||||||
import ViewSwitcher, { type ViewOption } from "./ViewSwitcher";
|
import ViewSwitcher, { type ViewOption } from "./ViewSwitcher";
|
||||||
import ColumnSortControl from "./ColumnSortControl";
|
import ColumnSortControl from "./ui/ColumnSortControl";
|
||||||
import FilterChipGroup from "./FilterChipGroup";
|
import FilterChipGroup from "./ui/FilterChipGroup";
|
||||||
import TagFilterChips, { type FilterChip } from "./TagFilterChips";
|
import TagFilterChips, { type FilterChip } from "./TagFilterChips";
|
||||||
import ActionMenu, { type ActionMenuItem } from "./ActionMenu";
|
import ActionMenu, { type ActionMenuItem } from "./ui/ActionMenu";
|
||||||
import ChannelStatsPopover from "./ChannelStatsPopover";
|
import ChannelStatsPopover from "./ChannelStatsPopover";
|
||||||
import Pager from "./Pager";
|
import Pager from "./ui/Pager";
|
||||||
import { CHANNEL_LAYOUTS, type ChannelLayout } from "../lib/channelLayout";
|
import { CHANNEL_LAYOUTS, type ChannelLayout } from "../lib/channelLayout";
|
||||||
import { sortRows } from "../lib/columnSort";
|
import { sortRows } from "../lib/columnSort";
|
||||||
import { filterRows } from "./tableHeader";
|
import { filterRows } from "./ui/tableHeader";
|
||||||
import { useCardPager } from "../lib/useCardPager";
|
import { useCardPager } from "../lib/useCardPager";
|
||||||
import { PageToolbar } from "./PageShell";
|
import { PageToolbar } from "./ui/PageShell";
|
||||||
import ChannelDiscovery from "./ChannelDiscovery";
|
import ChannelDiscovery from "./ChannelDiscovery";
|
||||||
import TagManager from "./TagManager";
|
import TagManager from "./TagManager";
|
||||||
import { useConfirm } from "./ConfirmProvider";
|
import { useConfirm } from "./ConfirmProvider";
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import {
|
|||||||
hasChannelsParams,
|
hasChannelsParams,
|
||||||
type ChannelsUrlState,
|
type ChannelsUrlState,
|
||||||
} from "../lib/channelsUrlState";
|
} from "../lib/channelsUrlState";
|
||||||
import { usePersistedTab } from "./Tabs";
|
import { usePersistedTab } from "./ui/Tabs";
|
||||||
import type { FilterMap } from "./tableHeader";
|
import type { FilterMap } from "./ui/tableHeader";
|
||||||
import { useNavigation, useNavigationActions } from "./NavigationProvider";
|
import { useNavigation, useNavigationActions } from "./NavigationProvider";
|
||||||
import type { ChannelsView } from "./Channels";
|
import type { ChannelsView } from "./Channels";
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import {
|
|||||||
} from "../lib/chatDock";
|
} from "../lib/chatDock";
|
||||||
import { onMessage, onUnread } from "../lib/messagesSocket";
|
import { onMessage, onUnread } from "../lib/messagesSocket";
|
||||||
import * as e2ee from "../lib/e2ee";
|
import * as e2ee from "../lib/e2ee";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./ui/Avatar";
|
||||||
import ChatThread from "./ChatThread";
|
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
|
// 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
|
// page navigation (state persisted per user). Always mounted (for human users) so it also owns
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Send } from "lucide-react";
|
|||||||
import { api, HttpError, type Message, type MessageUser } from "../lib/api";
|
import { api, HttpError, type Message, type MessageUser } from "../lib/api";
|
||||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||||
import { useScrollFade } from "../lib/useScrollFade";
|
import { useScrollFade } from "../lib/useScrollFade";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
import { relativeTime } from "../lib/format";
|
import { relativeTime } from "../lib/format";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../lib/notifications";
|
||||||
import { partnerPub, POLL_MS, refreshPartnerPub, useKeyState } from "../lib/messaging";
|
import { partnerPub, POLL_MS, refreshPartnerPub, useKeyState } from "../lib/messaging";
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { Plug, RotateCcw, Send } from "lucide-react";
|
import { Plug, RotateCcw, Send } from "lucide-react";
|
||||||
import { api, type ConfigItem, type PlexTestResult } from "../lib/api";
|
import { api, type ConfigItem, type PlexTestResult } from "../lib/api";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../lib/notifications";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./ui/Tooltip";
|
||||||
import InlineNotice from "./InlineNotice";
|
import InlineNotice from "./ui/InlineNotice";
|
||||||
import { useMe } from "../lib/useMe";
|
import { useMe } from "../lib/useMe";
|
||||||
import Tabs, { usePersistedTab } from "./Tabs";
|
import Tabs, { usePersistedTab } from "./ui/Tabs";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
import { PageToolbar } from "./PageShell";
|
import { PageToolbar } from "./ui/PageShell";
|
||||||
import { Switch } from "./ui/form";
|
import { Switch } from "./ui/form";
|
||||||
import { DraftSaveBar, type SaveState } from "./ui/DraftSaveBar";
|
import { DraftSaveBar, type SaveState } from "./ui/DraftSaveBar";
|
||||||
import { LS } from "../lib/storage";
|
import { LS } from "../lib/storage";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createContext, useCallback, useContext, useState, type ReactNode } from "react";
|
import { createContext, useCallback, useContext, useState, type ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Modal from "./Modal";
|
import Modal from "./ui/Modal";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "./ui/Button";
|
||||||
|
|
||||||
// App-styled, promise-based replacement for window.confirm. Wrap the tree in
|
// App-styled, promise-based replacement for window.confirm. Wrap the tree in
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useLayoutEffect, useState } from "react";
|
import { useLayoutEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { CalendarDays, Check, ChevronDown } from "lucide-react";
|
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 { type FeedFilters } from "../lib/api";
|
||||||
import { deriveDateKey, selectDateChip, type DatePreset } from "../lib/dateFilter";
|
import { deriveDateKey, selectDateChip, type DatePreset } from "../lib/dateFilter";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { AlertTriangle } from "lucide-react";
|
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
|
// 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.)
|
// state is communal. (Replaces the old login-time toast, which re-popped on every reload.)
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
UploadCloud,
|
UploadCloud,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Tabs from "./Tabs";
|
import Tabs from "./ui/Tabs";
|
||||||
import { PageToolbar } from "./PageShell";
|
import { PageToolbar } from "./ui/PageShell";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
// Lazy: these dialogs/editors open on demand, so they stay out of the Downloads page chunk until
|
// 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).
|
// used (the video editor in particular is heavy — filmstrip + scrubber).
|
||||||
const DownloadDialog = lazy(() => import("./DownloadDialog"));
|
const DownloadDialog = lazy(() => import("./DownloadDialog"));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { lazy, Suspense, useState } from "react";
|
|||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../lib/queryKeys";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import Modal from "./Modal";
|
import Modal from "./ui/Modal";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "./ui/Button";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { invalidateDownloads } from "../lib/downloads";
|
import { invalidateDownloads } from "../lib/downloads";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useSyncExternalStore } from "react";
|
import { useSyncExternalStore } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { dismissError, getError, subscribeError } from "../lib/errorDialog";
|
import { dismissError, getError, subscribeError } from "../lib/errorDialog";
|
||||||
import Modal from "./Modal";
|
import Modal from "./ui/Modal";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "./ui/Button";
|
||||||
|
|
||||||
// Renders the current app error (if any) as a modal the user must acknowledge.
|
// Renders the current app error (if any) as a modal the user must acknowledge.
|
||||||
|
|||||||
@@ -38,12 +38,12 @@ import { FEED_VIEWS, type FeedView } from "../lib/feedView";
|
|||||||
import { clearedFilters, useActiveFilters } from "../lib/useActiveFilters";
|
import { clearedFilters, useActiveFilters } from "../lib/useActiveFilters";
|
||||||
import ActiveFilterChips from "./ActiveFilterChips";
|
import ActiveFilterChips from "./ActiveFilterChips";
|
||||||
import SelectedViewChip from "./SelectedViewChip";
|
import SelectedViewChip from "./SelectedViewChip";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
import { PageToolbar } from "./PageShell";
|
import { PageToolbar } from "./ui/PageShell";
|
||||||
import { useWizard } from "./WizardProvider";
|
import { useWizard } from "./WizardProvider";
|
||||||
import SortControl from "./SortControl";
|
import SortControl from "./ui/SortControl";
|
||||||
import FilterSelect from "./FilterSelect";
|
import FilterSelect from "./ui/FilterSelect";
|
||||||
import ItemCount from "./ItemCount";
|
import ItemCount from "./ui/ItemCount";
|
||||||
import ViewSwitcher from "./ViewSwitcher";
|
import ViewSwitcher from "./ViewSwitcher";
|
||||||
import VirtualFeed from "./VirtualFeed";
|
import VirtualFeed from "./VirtualFeed";
|
||||||
// Lazy: the in-app YouTube player (IFrame API) loads only when a video is first opened.
|
// Lazy: the in-app YouTube player (IFrame API) loads only when a video is first opened.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Library, User } from "lucide-react";
|
import { Library, User } from "lucide-react";
|
||||||
import SegmentedToggle from "./SegmentedToggle";
|
import SegmentedToggle from "./ui/SegmentedToggle";
|
||||||
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
|
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
|
||||||
|
|
||||||
// The feed's Mine/Library scope, relocated from the filter sidebar to sit just right of the header
|
// The feed's Mine/Library scope, relocated from the filter sidebar to sit just right of the header
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState, type ReactNode } from "react";
|
import { useState, type ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { defaultLayout, type PanelId, type PanelLayout } from "../lib/panelLayout";
|
import { defaultLayout, type PanelId, type PanelLayout } from "../lib/panelLayout";
|
||||||
import SidePanel from "./SidePanel";
|
import SidePanel from "./ui/SidePanel";
|
||||||
import PanelGroups, { type PanelItem } from "./PanelGroups";
|
import PanelGroups, { type PanelItem } from "./ui/PanelGroups";
|
||||||
|
|
||||||
// The shared filter-rail shell, extracted from the three rails that all render the same SidePanel
|
// 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
|
// chrome around a PanelGroups body: the feed Sidebar, the PlexSidebar, and the PlaylistsRail. It owns
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import Overlay from "./Overlay";
|
import Overlay from "./ui/Overlay";
|
||||||
import { copyText } from "../lib/clipboard";
|
import { copyText } from "../lib/clipboard";
|
||||||
import { usePrefs } from "./PrefsProvider";
|
import { usePrefs } from "./PrefsProvider";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Check, Globe } from "lucide-react";
|
import { Check, Globe } from "lucide-react";
|
||||||
import { LANGUAGES, type LangCode } from "../i18n";
|
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
|
// Compact language picker (globe + current code). Presentational: the parent decides what
|
||||||
// changing the language does (set it; persist server-side when signed in).
|
// changing the language does (set it; persist server-side when signed in).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Modal from "./Modal";
|
import Modal from "./ui/Modal";
|
||||||
import { api, type DownloadJob } from "../lib/api";
|
import { api, type DownloadJob } from "../lib/api";
|
||||||
import { jobTitle } from "./downloadRow";
|
import { jobTitle } from "./downloadRow";
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { ArrowLeft, ShieldCheck, SquarePen, ExternalLink } from "lucide-react";
|
import { ArrowLeft, ShieldCheck, SquarePen, ExternalLink } from "lucide-react";
|
||||||
import { api, type Conversation, type MessageUser } from "../lib/api";
|
import { api, type Conversation, type MessageUser } from "../lib/api";
|
||||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
import { relativeTime } from "../lib/format";
|
import { relativeTime } from "../lib/format";
|
||||||
import { partnerPub, POLL_MS, SYSTEM_ID, useKeyState } from "../lib/messaging";
|
import { partnerPub, POLL_MS, SYSTEM_ID, useKeyState } from "../lib/messaging";
|
||||||
import { useHistorySubview } from "../lib/history";
|
import { useHistorySubview } from "../lib/history";
|
||||||
import { openChat } from "../lib/chatDock";
|
import { openChat } from "../lib/chatDock";
|
||||||
import * as e2ee from "../lib/e2ee";
|
import * as e2ee from "../lib/e2ee";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./ui/Avatar";
|
||||||
import KeyGate from "./KeyGate";
|
import KeyGate from "./KeyGate";
|
||||||
import { inputCls } from "./ui/form";
|
import { inputCls } from "./ui/form";
|
||||||
import ChatThread from "./ChatThread";
|
import ChatThread from "./ChatThread";
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import type { Page } from "../lib/urlState";
|
|||||||
import { moduleDef, moduleLabelKey, moduleOrder, SYSTEM_PAGES } from "../lib/modules";
|
import { moduleDef, moduleLabelKey, moduleOrder, SYSTEM_PAGES } from "../lib/modules";
|
||||||
import { useScrollFade } from "../lib/useScrollFade";
|
import { useScrollFade } from "../lib/useScrollFade";
|
||||||
import { type LangCode } from "../i18n";
|
import { type LangCode } from "../i18n";
|
||||||
import AvatarImg from "./Avatar";
|
import AvatarImg from "./ui/Avatar";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./ui/Tooltip";
|
||||||
import LanguageSwitcher from "./LanguageSwitcher";
|
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
|
// 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
|
// dropdown; they now live here, each revealing its name in a hover/focus flyout label. Account
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ import {
|
|||||||
type VideoWatchedMeta,
|
type VideoWatchedMeta,
|
||||||
} from "../lib/notifications";
|
} from "../lib/notifications";
|
||||||
import { channelYouTubeUrl, relativeFromMs, relativeTime } from "../lib/format";
|
import { channelYouTubeUrl, relativeFromMs, relativeTime } from "../lib/format";
|
||||||
import { LEVEL_STYLE } from "./Toaster";
|
import { LEVEL_STYLE } from "./ui/Toaster";
|
||||||
import { PageToolbar } from "./PageShell";
|
import { PageToolbar } from "./ui/PageShell";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
import { useNavigationActions } from "./NavigationProvider";
|
import { useNavigationActions } from "./NavigationProvider";
|
||||||
import { useChannelsActions } from "./ChannelsProvider";
|
import { useChannelsActions } from "./ChannelsProvider";
|
||||||
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
|
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { qk } from "../lib/queryKeys";
|
|||||||
import { AlertTriangle, Check, Loader2, RefreshCw, ShieldCheck, X, Youtube } from "lucide-react";
|
import { AlertTriangle, Check, Loader2, RefreshCw, ShieldCheck, X, Youtube } from "lucide-react";
|
||||||
import { api, type Me } from "../lib/api";
|
import { api, type Me } from "../lib/api";
|
||||||
import { beginGrant, endOnboarding } from "../lib/onboarding";
|
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
|
// 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
|
// time, each with a plain explanation and an up-front heads-up about Google's "unverified
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import {
|
|||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import { coercePlexFilters, type FeedFilters, type PlexFilters, type SavedView } from "../lib/api";
|
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 { Button } from "./ui/Button";
|
||||||
import { useSavedViews, asFilters } from "./SavedViewsProvider";
|
import { useSavedViews, asFilters } from "./SavedViewsProvider";
|
||||||
import { useActiveFilters } from "../lib/useActiveFilters";
|
import { useActiveFilters } from "../lib/useActiveFilters";
|
||||||
import { buildPlexActiveFilters } from "./usePlexActiveFilters";
|
import { buildPlexActiveFilters } from "./usePlexActiveFilters";
|
||||||
import { useUndoable } from "../lib/useUndoable";
|
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
|
// 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
|
// reorder) plus a "save the current filters" input; the right column shows the selected view's name
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ import {
|
|||||||
VolumeX,
|
VolumeX,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./ui/Avatar";
|
||||||
import AddToPlaylist from "./AddToPlaylist";
|
import AddToPlaylist from "./AddToPlaylist";
|
||||||
import { PopoverSurface, useAnchoredPopover } from "./AnchoredPopover";
|
import { PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover";
|
||||||
import { pushLayer, removeLayer, anyLayerAbove, escapeHandled } from "../lib/layerStack";
|
import { pushLayer, removeLayer, anyLayerAbove, escapeHandled } from "../lib/layerStack";
|
||||||
import DownloadButton from "./DownloadButton";
|
import DownloadButton from "./DownloadButton";
|
||||||
import { api, type Video, type VideoStatus } from "../lib/api";
|
import { api, type Video, type VideoStatus } from "../lib/api";
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ import { cycleSort, type SortState } from "../lib/columnSort";
|
|||||||
import { formatRuntime } from "../lib/format";
|
import { formatRuntime } from "../lib/format";
|
||||||
import { applyView, sameOrder } from "../lib/playlistView";
|
import { applyView, sameOrder } from "../lib/playlistView";
|
||||||
const PlayerModal = lazy(() => import("./PlayerModal"));
|
const PlayerModal = lazy(() => import("./PlayerModal"));
|
||||||
import UndoToolbar from "./UndoToolbar";
|
import UndoToolbar from "./ui/UndoToolbar";
|
||||||
import { PageToolbar } from "./PageShell";
|
import { PageToolbar } from "./ui/PageShell";
|
||||||
import { useChoice, useConfirm } from "./ConfirmProvider";
|
import { useChoice, useConfirm } from "./ConfirmProvider";
|
||||||
import { usePlaylists, usePlaylistsActions } from "./PlaylistsProvider";
|
import { usePlaylists, usePlaylistsActions } from "./PlaylistsProvider";
|
||||||
import { useMe } from "../lib/useMe";
|
import { useMe } from "../lib/useMe";
|
||||||
@@ -54,7 +54,7 @@ import {
|
|||||||
isActive,
|
isActive,
|
||||||
type Column,
|
type Column,
|
||||||
type FilterMap,
|
type FilterMap,
|
||||||
} from "./tableHeader";
|
} from "./ui/tableHeader";
|
||||||
import { GRIP_KEY, playlistColumns } from "./playlistColumns";
|
import { GRIP_KEY, playlistColumns } from "./playlistColumns";
|
||||||
|
|
||||||
// The playlist detail is a HYBRID table (E4 S4): real sortable/filterable column headers over a
|
// The playlist detail is a HYBRID table (E4 S4): real sortable/filterable column headers over a
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { notifyYouTubeActionError } from "../lib/youtubeErrors";
|
|||||||
import { LS, readAccountMerged, writeAccount } from "../lib/storage";
|
import { LS, readAccountMerged, writeAccount } from "../lib/storage";
|
||||||
import { type PanelLayout } from "../lib/panelLayout";
|
import { type PanelLayout } from "../lib/panelLayout";
|
||||||
import FilterRail from "./FilterRail";
|
import FilterRail from "./FilterRail";
|
||||||
import { type PanelItem } from "./PanelGroups";
|
import { type PanelItem } from "./ui/PanelGroups";
|
||||||
import SortControl from "./SortControl";
|
import SortControl from "./ui/SortControl";
|
||||||
import { type SortOption } from "../lib/columnSort";
|
import { type SortOption } from "../lib/columnSort";
|
||||||
import { usePlaylists, usePlaylistsActions } from "./PlaylistsProvider";
|
import { usePlaylists, usePlaylistsActions } from "./PlaylistsProvider";
|
||||||
import { useMe } from "../lib/useMe";
|
import { useMe } from "../lib/useMe";
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ import { useDebounced } from "../lib/useDebounced";
|
|||||||
import { useHistorySubview } from "../lib/history";
|
import { useHistorySubview } from "../lib/history";
|
||||||
import { PLEX_SUB_URL, type Sub } from "./plexSubUrl";
|
import { PLEX_SUB_URL, type Sub } from "./plexSubUrl";
|
||||||
import { toggleWatchState } from "../lib/plexWatch";
|
import { toggleWatchState } from "../lib/plexWatch";
|
||||||
import SortControl from "./SortControl";
|
import SortControl from "./ui/SortControl";
|
||||||
import FilterSelect from "./FilterSelect";
|
import FilterSelect from "./ui/FilterSelect";
|
||||||
import PlexDiscover from "./PlexDiscover";
|
import PlexDiscover from "./PlexDiscover";
|
||||||
import { PlexCollectionsBrowse, PlexPlaylistsBrowse } from "./plexBrowseLists";
|
import { PlexCollectionsBrowse, PlexPlaylistsBrowse } from "./plexBrowseLists";
|
||||||
import { PageToolbar } from "./PageShell";
|
import { PageToolbar } from "./ui/PageShell";
|
||||||
import ActiveFilterChips from "./ActiveFilterChips";
|
import ActiveFilterChips from "./ActiveFilterChips";
|
||||||
import SelectedViewChip from "./SelectedViewChip";
|
import SelectedViewChip from "./SelectedViewChip";
|
||||||
import ItemCount from "./ItemCount";
|
import ItemCount from "./ui/ItemCount";
|
||||||
import { StateMessage } from "./QueryState";
|
import { StateMessage } from "./ui/QueryState";
|
||||||
import { usePlexActiveFilters, usePlexClearFilters } from "./usePlexActiveFilters";
|
import { usePlexActiveFilters, usePlexClearFilters } from "./usePlexActiveFilters";
|
||||||
import { usePlex, usePlexActions } from "./PlexProvider";
|
import { usePlex, usePlexActions } from "./PlexProvider";
|
||||||
import { usePublishPlexView } from "./PlexViewBridge";
|
import { usePublishPlexView } from "./PlexViewBridge";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Check, Plus } from "lucide-react";
|
|||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { notifyCollectionEditError } from "../lib/actionErrors";
|
import { notifyCollectionEditError } from "../lib/actionErrors";
|
||||||
import { useScrollFade } from "../lib/useScrollFade";
|
import { useScrollFade } from "../lib/useScrollFade";
|
||||||
import Modal from "./Modal";
|
import Modal from "./ui/Modal";
|
||||||
import { useConfirm } from "./ConfirmProvider";
|
import { useConfirm } from "./ConfirmProvider";
|
||||||
|
|
||||||
// Admin-only "manage this movie's collections" dialog (Collections P2). Lists the library's EDITABLE
|
// Admin-only "manage this movie's collections" dialog (Collections P2). Lists the library's EDITABLE
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { BackBtn, ActionBtn, DetailHero } from "../lib/plexDetailUi";
|
|||||||
import { PlexPosterCard } from "./plexCards";
|
import { PlexPosterCard } from "./plexCards";
|
||||||
import PlexLibraryPicker from "./PlexLibraryPicker";
|
import PlexLibraryPicker from "./PlexLibraryPicker";
|
||||||
import { toggleWatchState } from "../lib/plexWatch";
|
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
|
// 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,
|
// ordered member grid, a Play-all / Shuffle over the playable leaves, and — for a NON-external,
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import PosterTile from "./PosterTile";
|
|||||||
import { plexLang } from "../i18n";
|
import { plexLang } from "../i18n";
|
||||||
import { toggleWatchState } from "../lib/plexWatch";
|
import { toggleWatchState } from "../lib/plexWatch";
|
||||||
import { usePlex } from "./PlexProvider";
|
import { usePlex } from "./PlexProvider";
|
||||||
import { Carousel } from "./Carousel";
|
import { Carousel } from "./ui/Carousel";
|
||||||
import { PlexPosterCard } from "./plexCards";
|
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
|
// 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
|
// /api/plex/discover call — Continue watching, Recently added, Browse by genre (tiles), Because you
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
} from "../lib/plexDetailUi";
|
} from "../lib/plexDetailUi";
|
||||||
import { usePref } from "../lib/usePref";
|
import { usePref } from "../lib/usePref";
|
||||||
import i18n from "../i18n";
|
import i18n from "../i18n";
|
||||||
import { Carousel } from "./Carousel";
|
import { Carousel } from "./ui/Carousel";
|
||||||
import PlexCollectionEditor from "./PlexCollectionEditor";
|
import PlexCollectionEditor from "./PlexCollectionEditor";
|
||||||
import PlexPlaylistAdd from "./PlexPlaylistAdd";
|
import PlexPlaylistAdd from "./PlexPlaylistAdd";
|
||||||
import { PLAYABLE_TINT, playableClass } from "./plexCards";
|
import { PLAYABLE_TINT, playableClass } from "./plexCards";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../lib/queryKeys";
|
||||||
import { api, type PlexCard } from "../lib/api";
|
import { api, type PlexCard } from "../lib/api";
|
||||||
import { useDebounced } from "../lib/useDebounced";
|
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
|
// 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
|
// the "New collection" first-title panel (plexBrowseLists) — both search the library the same way and
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { Check, Minus, Plus } from "lucide-react";
|
import { Check, Minus, Plus } from "lucide-react";
|
||||||
import { api, type PlexPlaylist } from "../lib/api";
|
import { api, type PlexPlaylist } from "../lib/api";
|
||||||
import { useScrollFade } from "../lib/useScrollFade";
|
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
|
// 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.
|
// set of episode rating_keys). Groups get a tri-state (none / some / all) per playlist.
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { api, type PlexCard } from "../lib/api";
|
|||||||
import { formatRuntime } from "../lib/format";
|
import { formatRuntime } from "../lib/format";
|
||||||
import { getAccountRaw, LS, setAccountRaw } from "../lib/storage";
|
import { getAccountRaw, LS, setAccountRaw } from "../lib/storage";
|
||||||
import { useConfirm } from "./ConfirmProvider";
|
import { useConfirm } from "./ConfirmProvider";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
import ViewSwitcher, { type ViewOption } from "./ViewSwitcher";
|
import ViewSwitcher, { type ViewOption } from "./ViewSwitcher";
|
||||||
|
|
||||||
// A playlist's items, grouped for readability: movies stay standalone, while a run of episodes from the
|
// A playlist's items, grouped for readability: movies stay standalone, while a run of episodes from the
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import SegmentedToggle from "./SegmentedToggle";
|
import SegmentedToggle from "./ui/SegmentedToggle";
|
||||||
import { usePlex, usePlexActions } from "./PlexProvider";
|
import { usePlex, usePlexActions } from "./PlexProvider";
|
||||||
import { usePlexViewControls } from "./PlexViewBridge";
|
import { usePlexViewControls } from "./PlexViewBridge";
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { ChevronDown, Film, Tv } from "lucide-react";
|
import { ChevronDown, Film, Tv } from "lucide-react";
|
||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
import ActionMenu from "./ActionMenu";
|
import ActionMenu from "./ui/ActionMenu";
|
||||||
import { usePlex, usePlexActions } from "./PlexProvider";
|
import { usePlex, usePlexActions } from "./PlexProvider";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../lib/queryKeys";
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import { type PanelLayout } from "../lib/panelLayout";
|
|||||||
import { type MediaSourceKind } from "../lib/mediaSource";
|
import { type MediaSourceKind } from "../lib/mediaSource";
|
||||||
import { useDebounced } from "../lib/useDebounced";
|
import { useDebounced } from "../lib/useDebounced";
|
||||||
import FilterRail from "./FilterRail";
|
import FilterRail from "./FilterRail";
|
||||||
import { type PanelItem } from "./PanelGroups";
|
import { type PanelItem } from "./ui/PanelGroups";
|
||||||
import SegmentedToggle from "./SegmentedToggle";
|
import SegmentedToggle from "./ui/SegmentedToggle";
|
||||||
import FilterChipGroup from "./FilterChipGroup";
|
import FilterChipGroup from "./ui/FilterChipGroup";
|
||||||
import Combobox, { type ComboboxOption } from "./Combobox";
|
import Combobox, { type ComboboxOption } from "./ui/Combobox";
|
||||||
import SavedViewsPopover from "./SavedViewsPopover";
|
import SavedViewsPopover from "./SavedViewsPopover";
|
||||||
import PersonalViewsModal from "./PersonalViewsModal";
|
import PersonalViewsModal from "./PersonalViewsModal";
|
||||||
import { useSavedViews } from "./SavedViewsProvider";
|
import { useSavedViews } from "./SavedViewsProvider";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import SegmentedToggle from "./SegmentedToggle";
|
import SegmentedToggle from "./ui/SegmentedToggle";
|
||||||
import { usePlexViewControls } from "./PlexViewBridge";
|
import { usePlexViewControls } from "./PlexViewBridge";
|
||||||
|
|
||||||
// The Discover ⟷ Browse switch, hosted in the header next to the library-scope pill (All/Movies/Shows).
|
// The Discover ⟷ Browse switch, hosted in the header next to the library-scope pill (All/Movies/Shows).
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { qk } from "../lib/queryKeys";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Lock, Pencil, Plus, Trash2 } from "lucide-react";
|
import { Lock, Pencil, Plus, Trash2 } from "lucide-react";
|
||||||
import Modal from "./Modal";
|
import Modal from "./ui/Modal";
|
||||||
import { useConfirm } from "./ConfirmProvider";
|
import { useConfirm } from "./ConfirmProvider";
|
||||||
import { api, type DownloadProfile, type DownloadSpec } from "../lib/api";
|
import { api, type DownloadProfile, type DownloadSpec } from "../lib/api";
|
||||||
import { inputCls } from "./ui/form";
|
import { inputCls } from "./ui/form";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Bug, Sparkles } from "lucide-react";
|
import { Bug, Sparkles } from "lucide-react";
|
||||||
import { RELEASE_NOTES } from "../lib/releaseNotes";
|
import { RELEASE_NOTES } from "../lib/releaseNotes";
|
||||||
import Modal from "./Modal";
|
import Modal from "./ui/Modal";
|
||||||
|
|
||||||
function fmtDate(d: string): string {
|
function fmtDate(d: string): string {
|
||||||
const t = new Date(d);
|
const t = new Date(d);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, type ReactNode } from "react";
|
import { useState, type ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Bookmark, Check, Settings2 } from "lucide-react";
|
import { Bookmark, Check, Settings2 } from "lucide-react";
|
||||||
import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./AnchoredPopover";
|
import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover";
|
||||||
import { useSavedViews } from "./SavedViewsProvider";
|
import { useSavedViews } from "./SavedViewsProvider";
|
||||||
|
|
||||||
// The saved-views quick-picker. The filter-panel header's icon (and the toolbar's view chip) use it as
|
// The saved-views quick-picker. The filter-panel header's icon (and the toolbar's view chip) use it as
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { api, type SchedulerJob, type SchedulerStatus } from "../lib/api";
|
import { api, type SchedulerJob, type SchedulerStatus } from "../lib/api";
|
||||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
import { useConfirm } from "./ConfirmProvider";
|
import { useConfirm } from "./ConfirmProvider";
|
||||||
import { relativeTime } from "../lib/format";
|
import { relativeTime } from "../lib/format";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../lib/notifications";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./ui/Tooltip";
|
||||||
|
|
||||||
const POLL_MS = 4000;
|
const POLL_MS = 4000;
|
||||||
// While any job is running, poll faster so short scheduled runs aren't missed between ticks.
|
// While any job is running, poll faster so short scheduled runs aren't missed between ticks.
|
||||||
|
|||||||
@@ -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 { SCHEMES, type Scheme, type ThemePrefs } from "../lib/theme";
|
||||||
import { api, HttpError, type Me } from "../lib/api";
|
import { api, HttpError, type Me } from "../lib/api";
|
||||||
import { LS, useAccountPersistedState } from "../lib/storage";
|
import { LS, useAccountPersistedState } from "../lib/storage";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./ui/Avatar";
|
||||||
import { notify, type NotifSettings } from "../lib/notifications";
|
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 { inputCls, Section, SettingRow, Switch } from "./ui/form";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "./ui/Button";
|
||||||
import { type SaveState } from "./ui/DraftSaveBar";
|
import { type SaveState } from "./ui/DraftSaveBar";
|
||||||
import { useConfirm } from "./ConfirmProvider";
|
import { useConfirm } from "./ConfirmProvider";
|
||||||
import { StateMessage } from "./QueryState";
|
import { StateMessage } from "./ui/QueryState";
|
||||||
import { useWizard } from "./WizardProvider";
|
import { useWizard } from "./WizardProvider";
|
||||||
import { usePrefs } from "./PrefsProvider";
|
import { usePrefs } from "./PrefsProvider";
|
||||||
import Tabs from "./Tabs";
|
import Tabs from "./ui/Tabs";
|
||||||
import { useMe } from "../lib/useMe";
|
import { useMe } from "../lib/useMe";
|
||||||
|
|
||||||
// The Settings page edits server-persisted preferences. Each change applies locally for instant
|
// The Settings page edits server-persisted preferences. Each change applies locally for instant
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Check, Copy, Link2, Lock, Trash2, UserPlus, Eye } from "lucide-react";
|
import { Check, Copy, Link2, Lock, Trash2, UserPlus, Eye } from "lucide-react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Modal from "./Modal";
|
import Modal from "./ui/Modal";
|
||||||
import { useConfirm } from "./ConfirmProvider";
|
import { useConfirm } from "./ConfirmProvider";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../lib/notifications";
|
||||||
import { copyText } from "../lib/clipboard";
|
import { copyText } from "../lib/clipboard";
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import TagManager from "./TagManager";
|
|||||||
import SavedViewsPopover from "./SavedViewsPopover";
|
import SavedViewsPopover from "./SavedViewsPopover";
|
||||||
import { useSavedViews } from "./SavedViewsProvider";
|
import { useSavedViews } from "./SavedViewsProvider";
|
||||||
import FilterRail from "./FilterRail";
|
import FilterRail from "./FilterRail";
|
||||||
import { type PanelItem } from "./PanelGroups";
|
import { type PanelItem } from "./ui/PanelGroups";
|
||||||
import { type MediaSourceKind } from "../lib/mediaSource";
|
import { type MediaSourceKind } from "../lib/mediaSource";
|
||||||
import ChannelPicker from "./ChannelPicker";
|
import ChannelPicker from "./ChannelPicker";
|
||||||
import DateFilterSelect from "./DateFilterSelect";
|
import DateFilterSelect from "./DateFilterSelect";
|
||||||
import YearFilter from "./YearFilter";
|
import YearFilter from "./YearFilter";
|
||||||
import Combobox, { type ComboboxOption } from "./Combobox";
|
import Combobox, { type ComboboxOption } from "./ui/Combobox";
|
||||||
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
|
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
|
||||||
import { useChannelsActions } from "./ChannelsProvider";
|
import { useChannelsActions } from "./ChannelsProvider";
|
||||||
import { useMe } from "../lib/useMe";
|
import { useMe } from "../lib/useMe";
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import { notify } from "../lib/notifications";
|
|||||||
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
|
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
|
||||||
import { useWizard } from "./WizardProvider";
|
import { useWizard } from "./WizardProvider";
|
||||||
import { LS, useAccountPersistedState } from "../lib/storage";
|
import { LS, useAccountPersistedState } from "../lib/storage";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./ui/Tooltip";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
import FilterChipGroup from "./FilterChipGroup";
|
import FilterChipGroup from "./ui/FilterChipGroup";
|
||||||
import Tabs from "./Tabs";
|
import Tabs from "./ui/Tabs";
|
||||||
import { Section, SettingRow } from "./ui/form";
|
import { Section, SettingRow } from "./ui/form";
|
||||||
import { useMe } from "../lib/useMe";
|
import { useMe } from "../lib/useMe";
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { CheckCircle2, Clock, Database, History, Loader2, Pause, Play } from "lucide-react";
|
import { CheckCircle2, Clock, Database, History, Loader2, Pause, Play } from "lucide-react";
|
||||||
import { api, type MyStatus } from "../lib/api";
|
import { api, type MyStatus } from "../lib/api";
|
||||||
import { formatViews } from "../lib/format";
|
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*
|
// 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
|
// user, how many of *their* channels are still being fetched, and how many lack full
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { fitChipsWithPill } from "../lib/chipOverflow";
|
import { fitChipsWithPill } from "../lib/chipOverflow";
|
||||||
import { useChipOverflowFit } from "../lib/useChipOverflowFit";
|
import { useChipOverflowFit } from "../lib/useChipOverflowFit";
|
||||||
import { filterChipClass } from "./FilterChipGroup";
|
import { filterChipClass } from "./ui/FilterChipGroup";
|
||||||
import { useAnchoredPopover, PopoverSurface } from "./AnchoredPopover";
|
import { useAnchoredPopover, PopoverSurface } from "./ui/AnchoredPopover";
|
||||||
|
|
||||||
// A single-line row of toggle filter chips that collapses its overflow into a trailing "+N" pill whose
|
// 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
|
// popover holds the rest (still toggleable) — the same compression the feed's applied-filter row uses
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { Plus, Trash2 } from "lucide-react";
|
|||||||
import { api, type Tag } from "../lib/api";
|
import { api, type Tag } from "../lib/api";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../lib/notifications";
|
||||||
import { useScrollFade } from "../lib/useScrollFade";
|
import { useScrollFade } from "../lib/useScrollFade";
|
||||||
import Modal from "./Modal";
|
import Modal from "./ui/Modal";
|
||||||
import { useConfirm } from "./ConfirmProvider";
|
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
|
// 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
|
// hover popover on the count listing the tagged channels (each a link that focuses it in the
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { UploadCloud, File as FileIcon } from "lucide-react";
|
import { UploadCloud, File as FileIcon } from "lucide-react";
|
||||||
import Modal from "./Modal";
|
import Modal from "./ui/Modal";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "./ui/Button";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { invalidateDownloads } from "../lib/downloads";
|
import { invalidateDownloads } from "../lib/downloads";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { CheckCircle2, AlertTriangle } from "lucide-react";
|
import { CheckCircle2, AlertTriangle } from "lucide-react";
|
||||||
import Banner from "./Banner";
|
import Banner from "./ui/Banner";
|
||||||
import type { VerifyEntry } from "../lib/useVerifyEntry";
|
import type { VerifyEntry } from "../lib/useVerifyEntry";
|
||||||
|
|
||||||
// The top-of-app outcome of an emailed verification link opened in a browser that ALREADY holds a
|
// The top-of-app outcome of an emailed verification link opened in a browser that ALREADY holds a
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Sparkles } from "lucide-react";
|
import { Sparkles } from "lucide-react";
|
||||||
import { FRONTEND_VERSION, SEEN_VERSION_KEY } from "../lib/version";
|
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
|
// 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).
|
// (compares the baked frontend version to the last one the user acknowledged).
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
// Aliased: `Video` is this app's own domain type (lib/api), and it wins the name.
|
// Aliased: `Video` is this app's own domain type (lib/api), and it wins the name.
|
||||||
Video as VideoIcon,
|
Video as VideoIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./ui/Avatar";
|
||||||
import AddToPlaylist from "./AddToPlaylist";
|
import AddToPlaylist from "./AddToPlaylist";
|
||||||
import DownloadButton from "./DownloadButton";
|
import DownloadButton from "./DownloadButton";
|
||||||
import VideoInfoCard from "./VideoInfoCard";
|
import VideoInfoCard from "./VideoInfoCard";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Crop, Eye, EyeOff, Pause, Play, Scissors, SplitSquareHorizontal, X } from "lucide-react";
|
import { Crop, Eye, EyeOff, Pause, Play, Scissors, SplitSquareHorizontal, X } from "lucide-react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Modal from "./Modal";
|
import Modal from "./ui/Modal";
|
||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../lib/notifications";
|
||||||
import { api, type DownloadJob, type EditSpec } from "../lib/api";
|
import { api, type DownloadJob, type EditSpec } from "../lib/api";
|
||||||
import { contentRect, cropToSourcePx, type Frac } from "../lib/cropGeometry";
|
import { contentRect, cropToSourcePx, type Frac } from "../lib/cropGeometry";
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { qk } from "../lib/queryKeys";
|
|||||||
import { api, type Video } from "../lib/api";
|
import { api, type Video } from "../lib/api";
|
||||||
import { formatDate, formatDuration, formatViews } from "../lib/format";
|
import { formatDate, formatDuration, formatViews } from "../lib/format";
|
||||||
import { linkify } from "../lib/linkify";
|
import { linkify } from "../lib/linkify";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./ui/Avatar";
|
||||||
import { PopoverSurface, useAnchoredPopover } from "./AnchoredPopover";
|
import { PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover";
|
||||||
|
|
||||||
// The feed card's "more than the card face" hover infobox (E8·S3, decision D3): full (un-clamped)
|
// 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
|
// title, channel, views·duration·date, a description snippet, and the channel's tags. It hangs off a
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Check, ChevronDown, type LucideIcon } from "lucide-react";
|
import { Check, ChevronDown, type LucideIcon } from "lucide-react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./AnchoredPopover";
|
import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover";
|
||||||
|
|
||||||
export interface ViewOption<T extends string> {
|
export interface ViewOption<T extends string> {
|
||||||
id: T;
|
id: T;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Video, VideoStatus } from "../lib/api";
|
import type { Video, VideoStatus } from "../lib/api";
|
||||||
import { FEED_VIEW_SPEC, type FeedView } from "../lib/feedView";
|
import { FEED_VIEW_SPEC, type FeedView } from "../lib/feedView";
|
||||||
import VideoCard from "./VideoCard";
|
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
|
// 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
|
// the very first paint and the scrollbar's initial guess. Taken from measuring each mode in the
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../lib/queryKeys";
|
||||||
import { api, type FeedFilters } from "../lib/api";
|
import { api, type FeedFilters } from "../lib/api";
|
||||||
import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./AnchoredPopover";
|
import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./ui/AnchoredPopover";
|
||||||
import { filterChipClass } from "./FilterChipGroup";
|
import { filterChipClass } from "./ui/FilterChipGroup";
|
||||||
import CountBadge from "./CountBadge";
|
import CountBadge from "./ui/CountBadge";
|
||||||
import { useDebounced } from "../lib/useDebounced";
|
import { useDebounced } from "../lib/useDebounced";
|
||||||
|
|
||||||
// The feed rail's "Year" control (item 8 / E8·S2): quick chips for the most recent years plus a
|
// The feed rail's "Year" control (item 8 / E8·S2): quick chips for the most recent years plus a
|
||||||
|
|||||||
@@ -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
|
// 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.
|
// active tab directly — this file is about the Access-requests panel, not the tab chrome.
|
||||||
vi.mock("./Tabs", () => ({
|
vi.mock("./ui/Tabs", () => ({
|
||||||
default: () => null,
|
default: () => null,
|
||||||
usePersistedTab: () => ["access", () => {}],
|
usePersistedTab: () => ["access", () => {}],
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const meMock = {
|
|||||||
vi.mock("../lib/useMe", () => ({ useMe: () => 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.
|
// 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,
|
default: () => null,
|
||||||
usePersistedTab: () => ["access", () => {}],
|
usePersistedTab: () => ["access", () => {}],
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { TFunction } from "i18next";
|
|||||||
import { relativeTime, formatDate } from "../lib/format";
|
import { relativeTime, formatDate } from "../lib/format";
|
||||||
import i18n from "../i18n";
|
import i18n from "../i18n";
|
||||||
import type { AuditRow } from "../lib/api";
|
import type { AuditRow } from "../lib/api";
|
||||||
import type { Column } from "./DataTable";
|
import type { Column } from "./ui/DataTable";
|
||||||
|
|
||||||
function actionLabel(t: TFunction, action: string): string {
|
function actionLabel(t: TFunction, action: string): string {
|
||||||
// Canonical key → localized label; falls back to the raw key for any unmapped action.
|
// Canonical key → localized label; falls back to the raw key for any unmapped action.
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Check, Eye, EyeOff, History, UserMinus } from "lucide-react";
|
import { Check, Eye, EyeOff, History, UserMinus } from "lucide-react";
|
||||||
import { NO_TAG_SENTINEL, type ManagedChannel } from "../lib/api";
|
import { NO_TAG_SENTINEL, type ManagedChannel } from "../lib/api";
|
||||||
import { formatCountOrDash, formatTotalHours, relativeTime } from "../lib/format";
|
import { formatCountOrDash, formatTotalHours, relativeTime } from "../lib/format";
|
||||||
import type { Column } from "./DataTable";
|
import type { Column } from "./ui/DataTable";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./ui/Tooltip";
|
||||||
import { PriorityStepper } from "./PriorityStepper";
|
import { PriorityStepper } from "./ui/PriorityStepper";
|
||||||
import ChannelLink from "./ChannelLink";
|
import ChannelLink from "./ChannelLink";
|
||||||
import AboutCell from "./AboutCell";
|
import AboutCell from "./AboutCell";
|
||||||
import { ChannelTagsControl, type SuggestedTag } from "./ChannelTags";
|
import { ChannelTagsControl, type SuggestedTag } from "./ChannelTags";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { qk } from "../lib/queryKeys";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Settings2 } from "lucide-react";
|
import { Settings2 } from "lucide-react";
|
||||||
import Modal from "./Modal";
|
import Modal from "./ui/Modal";
|
||||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { formatBytes } from "../lib/format";
|
import { formatBytes } from "../lib/format";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { qk } from "../lib/queryKeys";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Plus, Trash2 } from "lucide-react";
|
import { Plus, Trash2 } from "lucide-react";
|
||||||
import Modal from "./Modal";
|
import Modal from "./ui/Modal";
|
||||||
import { api, type DownloadJob } from "../lib/api";
|
import { api, type DownloadJob } from "../lib/api";
|
||||||
import { inputCls } from "./ui/form";
|
import { inputCls } from "./ui/form";
|
||||||
import { Button } from "./ui/Button";
|
import { Button } from "./ui/Button";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Play, X } from "lucide-react";
|
|||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
import type { Video } from "../lib/api";
|
import type { Video } from "../lib/api";
|
||||||
import { formatDuration } from "../lib/format";
|
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`.
|
// Column definitions for the playlist detail table, in their own file like `channelColumns.tsx`.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { api, isIntrinsicReadOnly, type PlexCard, type PlexCollection } from "..
|
|||||||
import { formatRuntime } from "../lib/format";
|
import { formatRuntime } from "../lib/format";
|
||||||
import { notifyCollectionEditError } from "../lib/actionErrors";
|
import { notifyCollectionEditError } from "../lib/actionErrors";
|
||||||
import { useDebounced } from "../lib/useDebounced";
|
import { useDebounced } from "../lib/useDebounced";
|
||||||
import { LoadingState, StateMessage } from "./QueryState";
|
import { LoadingState, StateMessage } from "./ui/QueryState";
|
||||||
import PosterTile from "./PosterTile";
|
import PosterTile from "./PosterTile";
|
||||||
import PlexLibraryPicker from "./PlexLibraryPicker";
|
import PlexLibraryPicker from "./PlexLibraryPicker";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { CheckCircle2, Circle, Film, Info, ListPlus, Play, Star, Tv2 } from "lucide-react";
|
import { CheckCircle2, Circle, Film, Info, ListPlus, Play, Star, Tv2 } from "lucide-react";
|
||||||
import { type CSSProperties } from "react";
|
import { type CSSProperties } from "react";
|
||||||
import { formatRuntime } from "../lib/format";
|
import { formatRuntime } from "../lib/format";
|
||||||
import { Carousel } from "./Carousel";
|
import { Carousel } from "./ui/Carousel";
|
||||||
import type { PlexCard, PlexCastMember, PlexFilters, PlexSeasonDetail } from "../lib/api";
|
import type { PlexCard, PlexCastMember, PlexFilters, PlexSeasonDetail } from "../lib/api";
|
||||||
|
|
||||||
// Presentational Plex cards + strips, split out of PlexBrowse (all purely prop-driven, no shared
|
// Presentational Plex cards + strips, split out of PlexBrowse (all purely prop-driven, no shared
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ import {
|
|||||||
WatchedToggle,
|
WatchedToggle,
|
||||||
} from "../lib/plexDetailUi";
|
} from "../lib/plexDetailUi";
|
||||||
import { CastStrip, EpisodeCard, PlexPosterCard, RelatedStrip, SeasonCard } from "./plexCards";
|
import { CastStrip, EpisodeCard, PlexPosterCard, RelatedStrip, SeasonCard } from "./plexCards";
|
||||||
import { Carousel } from "./Carousel";
|
import { Carousel } from "./ui/Carousel";
|
||||||
import PlexPlaylistAdd, { type PlexAddTarget } from "./PlexPlaylistAdd";
|
import PlexPlaylistAdd, { type PlexAddTarget } from "./PlexPlaylistAdd";
|
||||||
import PlexCollectionEditor from "./PlexCollectionEditor";
|
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
|
// 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,
|
// shell callbacks as props (onBack/onPlay/onFilter/…) — no view reaches back into the shell's state,
|
||||||
|
|||||||
+2
-2
@@ -36,8 +36,8 @@ import {
|
|||||||
type Placement,
|
type Placement,
|
||||||
type UseRoleProps,
|
type UseRoleProps,
|
||||||
} from "@floating-ui/react";
|
} from "@floating-ui/react";
|
||||||
import { useLayer } from "../lib/layerStack";
|
import { useLayer } from "../../lib/layerStack";
|
||||||
import { useFullscreenElement } from "../lib/fullscreen";
|
import { useFullscreenElement } from "../../lib/fullscreen";
|
||||||
|
|
||||||
// R8 popover platform. ONE anchored-popover primitive over @floating-ui/react, replacing the ~9
|
// 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
|
// hand-rolled positioning engines (measured flip / width clamp / rAF scroll-follow / scroller-relative
|
||||||
@@ -2,7 +2,7 @@ import { useCallback, type ReactNode } from "react";
|
|||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import clsx from "clsx";
|
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
|
// 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
|
// the hand-rolled `flex gap overflow-x-auto` strips (Cast / Related / collections) and the future
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { type Column } from "./DataTable";
|
import { type Column } from "./DataTable";
|
||||||
import { type SortState } from "../lib/columnSort";
|
import { type SortState } from "../../lib/columnSort";
|
||||||
import { optionsFromColumns } from "../lib/sortOptions";
|
import { optionsFromColumns } from "../../lib/sortOptions";
|
||||||
import SortControl from "./SortControl";
|
import SortControl from "./SortControl";
|
||||||
|
|
||||||
// The headerless sort control for views without clickable column headers (the channel manager's and
|
// The headerless sort control for views without clickable column headers (the channel manager's and
|
||||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } fro
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Check, ChevronDown, X } from "lucide-react";
|
import { Check, ChevronDown, X } from "lucide-react";
|
||||||
import { useAnchoredPopover, PopoverSurface, PopoverItem } from "./AnchoredPopover";
|
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
|
// 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.
|
// removable chips that live INSIDE a text field you type into, and a checklist of options opens below.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 Pager, { hasPagerContent } from "./Pager";
|
||||||
import { PageToolbar } from "./PageShell";
|
import { PageToolbar } from "./PageShell";
|
||||||
import { ColumnHead, alignClass, filterRows, type Column, type FilterMap } from "./tableHeader";
|
import { ColumnHead, alignClass, filterRows, type Column, type FilterMap } from "./tableHeader";
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user