diff --git a/frontend/src/components/ActionMenu.tsx b/frontend/src/components/ActionMenu.tsx new file mode 100644 index 00000000..f959b974 --- /dev/null +++ b/frontend/src/components/ActionMenu.tsx @@ -0,0 +1,74 @@ +import { useState, type ReactNode } from "react"; +import { MoreHorizontal } from "lucide-react"; +import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; + +// A reusable "⋯ more actions" overflow menu: a compact trigger that opens a list of one-shot actions. +// Built on the R8 AnchoredPopover platform (arrows/typeahead/Escape/anchor-flip), same as ViewSwitcher +// and LanguageSwitcher. Use it to keep occasional actions out of a crowded toolbar. The items are +// fire-and-close (`onSelect`); a `disabled` item (e.g. a mutation already in flight) is inert. + +export interface ActionMenuItem { + id: string; + label: string; + icon?: ReactNode; // an element (so a caller can swap in a spinning icon while pending) + onSelect: () => void; + disabled?: boolean; + title?: string; // optional native tooltip (carries a longer hint the label alone can't) +} + +export default function ActionMenu({ + items, + ariaLabel, + className, + trigger, +}: { + items: readonly ActionMenuItem[]; + /** Accessible name for the trigger (e.g. "Catalog actions"). */ + ariaLabel: string; + /** Trigger className override; omit for the default glass ⋯ icon button. */ + className?: string; + /** Custom trigger content; omit for the default ⋯ icon. */ + trigger?: ReactNode; +}) { + const [open, setOpen] = useState(false); + const pop = useAnchoredPopover({ open, onOpenChange: setOpen, placement: "bottom-end" }); + return ( + <> + + + {items.map((it) => ( + { + it.onSelect(); + setOpen(false); + }} + className="flex w-full items-center gap-2.5 rounded-lg px-2 py-1.5 text-left text-sm outline-hidden transition disabled:opacity-50" + > + {it.icon && {it.icon}} + {it.label} + + ))} + + + ); +} diff --git a/frontend/src/components/Channels.tsx b/frontend/src/components/Channels.tsx index b6ba7706..a3f8190a 100644 --- a/frontend/src/components/Channels.tsx +++ b/frontend/src/components/Channels.tsx @@ -20,6 +20,7 @@ import { useChannelPriority } from "../lib/useChannelPriority"; import ViewSwitcher, { type ViewOption } from "./ViewSwitcher"; import ColumnSortControl from "./ColumnSortControl"; import FilterChipGroup, { filterChipClass } from "./FilterChipGroup"; +import ActionMenu, { type ActionMenuItem } from "./ActionMenu"; import Pager from "./Pager"; import { CHANNEL_LAYOUTS, type ChannelLayout } from "../lib/channelLayout"; import { sortRows } from "../lib/columnSort"; @@ -347,30 +348,28 @@ export default function Channels() { ); - // Sync + backfill — now share the tags row (right side). + // Catalog-wide, occasional actions — tucked into a shared ⋯ overflow menu so they stop crowding the + // tags row. Pending mutations keep the item visible but disabled, with a spinning icon. + const catalogActions: ActionMenuItem[] = [ + { + id: "sync", + label: t("channels.syncSubscriptions"), + title: t("channels.syncSubscriptionsHint"), + icon: , + onSelect: () => syncSubs.mutate(), + disabled: syncSubs.isPending, + }, + { + id: "backfill", + label: t("channels.backfillEverything"), + title: t("channels.backfillEverythingHint"), + icon: , + onSelect: () => deepAll.mutate(), + disabled: deepAll.isPending, + }, + ]; const actionButtons = ( -
- - - - - - -
+ ); // Table / cards density — shared by the subscribed + discovery tables (blocked is a chip list, no @@ -465,10 +464,16 @@ export default function Channels() { search lives in the shared top SearchBar, tag create/delete in the manager. */}
+ {/* The "TAGS" label IS the tags-editor trigger (pencil), replacing the separate + "Manage tags" button; the tooltip explains what tags are + that it's clickable. */} - - {t("channels.tags.yourTags")} - + {userTags.map((tg) => (
{actionButtons}
diff --git a/frontend/src/i18n/locales/en/channels.json b/frontend/src/i18n/locales/en/channels.json index 0073af9c..899ca173 100644 --- a/frontend/src/i18n/locales/en/channels.json +++ b/frontend/src/i18n/locales/en/channels.json @@ -1,4 +1,5 @@ { + "actionsLabel": "Catalog actions", "syncSubscriptions": "Read subscriptions from YouTube", "syncSubscriptionsHint": "Re-import your subscription list from YouTube — adds channels you've newly followed and drops ones you've unfollowed. The videos themselves keep syncing automatically in the background; this does not re-fetch them.", "backfillEverything": "Backfill everything", @@ -45,6 +46,7 @@ "fullySynced": "Fully synced" }, "tags": { + "label": "Tags", "yourTags": "Your tags", "manage": "Manage tags", "yourTagsHint": "Your personal labels. Attach them to channels below, then filter the feed by tag from the sidebar. (Separate from the automatic language/topic tags.)", diff --git a/frontend/src/i18n/locales/hu/channels.json b/frontend/src/i18n/locales/hu/channels.json index 1f685502..517ed5b4 100644 --- a/frontend/src/i18n/locales/hu/channels.json +++ b/frontend/src/i18n/locales/hu/channels.json @@ -45,6 +45,7 @@ "fullySynced": "Teljesen szinkronizálva" }, "tags": { + "label": "Címkék", "yourTags": "Címkéid", "manage": "Címkék kezelése", "yourTagsHint": "Saját személyes címkéid. Csatold őket az alábbi csatornákhoz, majd szűrd a hírfolyamot címke szerint az oldalsávból. (Az automatikus nyelvi/téma címkéktől függetlenül.)",