refactor(frontend): the flat components/ folder moves into shell/, modules/<page>/ and features/<name>/
A pure move (git mv, history kept): 333 files leave components/, which now holds only the platform layer (ui/) and the legal pages. The shell is the frame (App, header, nav rail, palette, boot); each page lives in modules/<page>/ with its rail, store and tests; what several pages share sits in features/<name>/; the shared-link page is watch/. Domain-free helpers went straight to ui/ (ViewSwitcher, FilterRail, the confirm dialog, the language switcher, the copy rows). Every relative specifier was rewritten (imports, dynamic imports, vi.mock, new URL), and the contract tests that scan sources now scan the whole app source, not only components/ — each glob was counted before and after so none went green over fewer files. The locale-format rule matched `/src/components/` only, so the move would have taken ~300 files out from under its i18n-singleton check; it now covers every folder that holds components, with a test per folder.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { api } from "../../lib/api";
|
||||
import ChannelAboutContent from "./ChannelAbout";
|
||||
import { PopoverSurface } from "../../components/ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "../../components/ui/anchoredPopoverHooks";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
|
||||
/**
|
||||
* A channel-description cell: one truncated line in the table, revealing the FULL channel "About"
|
||||
* (the same content as the detail page's About tab — description, links, country, language, topics,
|
||||
* keywords) in a glass hover card. The detail is fetched ON DEMAND on hover/focus, sharing the
|
||||
* ["channel", id] react-query cache with the channel page, so the list stays light. The card is
|
||||
* interactive (scrollable, links clickable) — the platform's safePolygon lets the cursor travel the
|
||||
* gap, replacing the hand-rolled 160 ms hide-timer bridge. Shared by the subscribed + discovery
|
||||
* tables.
|
||||
*
|
||||
* The card keeps the platform's default hover role (`tooltip`); making this rich, interactive panel
|
||||
* properly keyboard-reachable belongs to the focus/a11y work, not to the mechanical migration.
|
||||
*/
|
||||
export default function AboutCell({ text, channelId }: { text: string | null; channelId: string }) {
|
||||
const preview = (text ?? "").trim();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const pop = useAnchoredPopover({
|
||||
open: open && !!preview,
|
||||
onOpenChange: setOpen,
|
||||
mode: "hover",
|
||||
offset: 6,
|
||||
// Pop instantly like the old cell did; the close grace lets the cursor reach the card.
|
||||
hoverDelay: { open: 0, close: 160 },
|
||||
// Cap the panel to the space between the trigger and the viewport edge (publishes --fit-max-h) so a
|
||||
// long About never runs its top off-screen — at high browser zoom the short viewport made a fixed
|
||||
// max-height overflow above the top edge, where the scroll container can't reach.
|
||||
fitHeight: true,
|
||||
});
|
||||
|
||||
const { setReference } = pop.refs;
|
||||
|
||||
const { data: ch } = useQuery({
|
||||
queryKey: qk.channel(channelId),
|
||||
queryFn: () => api.channelDetail(channelId),
|
||||
enabled: open && !!channelId,
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
|
||||
if (!preview) return <span className="text-muted">—</span>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
ref={setReference}
|
||||
{...pop.getReferenceProps()}
|
||||
tabIndex={0}
|
||||
className="block max-w-88 truncate text-muted cursor-help rounded-sm outline-hidden focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2"
|
||||
>
|
||||
{preview}
|
||||
</span>
|
||||
<PopoverSurface
|
||||
popover={pop}
|
||||
className={`${SURFACE.popover} w-120 overflow-y-auto px-3.5 py-3 animate-[fadeIn_0.12s_ease]`}
|
||||
// min(design cap, available space): keeps the 28rem cap on a tall viewport, shrinks to fit on a
|
||||
// short one (high zoom). --fit-max-h is set by the fitHeight `size` middleware once positioned.
|
||||
style={{ maxHeight: "min(28rem, var(--fit-max-h, 28rem))" }}
|
||||
>
|
||||
{/* Full About once the detail loads; until then the list's truncated description. */}
|
||||
{ch ? (
|
||||
<ChannelAboutContent ch={ch} />
|
||||
) : (
|
||||
<p className="text-sm whitespace-pre-wrap wrap-break-word leading-relaxed">{preview}</p>
|
||||
)}
|
||||
</PopoverSurface>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user