ai1ywm5xi: Add the Discover sheet with featured, trending, find experts panels and the appearance switch

This commit is contained in:
2026-10-20 08:49:11 -06:00
parent 695ebe1095
commit cfdca1f270
10 changed files with 307 additions and 10 deletions
+148
View File
@@ -0,0 +1,148 @@
"use client";
// Mobile Discover sheet (spec "Sheets"): three soft depth rows that open the
// Featured experts, Trending minnects and Find experts panels as 80% height
// sheets, then the Dark/Light appearance switch. Styles: src/styles/sheet.css.
import type { ReactNode } from "react";
import type { DiscoverExpert, DiscoverMinnect } from "@/lib/discover";
import { useTheme } from "@/lib/use-theme";
import type { Theme } from "@/lib/theme";
import { BottomSheet } from "./BottomSheet";
import { DepthButton } from "./Buttons";
import { FeaturedCarousel } from "./FeaturedCarousel";
import { HexAvatar } from "./HexAvatar";
import { TrendingMinnectsRail } from "./TrendingMinnectsRail";
export type DiscoverPanel = "featured" | "trending" | "search";
/** Width of the center photo in the featured panel sheet. */
export const PANEL_CAROUSEL_SIZE = 150;
/** "Name1, Name2 and N more" for the Featured experts row; shorter lists read plainly. */
export function featuredSubtitle(experts: Pick<DiscoverExpert, "displayName">[]): string {
const names = experts.map((e) => e.displayName || "Expert");
if (names.length === 0) return "";
if (names.length === 1) return names[0];
if (names.length === 2) return `${names[0]} and ${names[1]}`;
return `${names[0]}, ${names[1]} and ${names.length - 2} more`;
}
function Chevron() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="btn-depth-chev">
<path d="m9 6 6 6-6 6" />
</svg>
);
}
const icon = { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round" } as const;
function FlameIcon() {
return (
<svg {...icon}>
<path d="M12 21c4 0 7-2.8 7-6.8 0-4.5-4-6.7-5-11.2-2.3 1.7-3.5 4-3.2 6.5C9.5 9 8.6 8 8.3 6.6 6.3 8.5 5 10.9 5 14.2 5 18.2 8 21 12 21z" />
</svg>
);
}
function SearchIcon() {
return (
<svg {...icon}>
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.5-3.5" />
</svg>
);
}
function MoonIcon() {
return (
<svg {...icon} aria-hidden="true">
<path d="M20.5 14.5A8.5 8.5 0 0 1 9.5 3.5a8.5 8.5 0 1 0 11 11z" />
</svg>
);
}
function SunIcon() {
return (
<svg {...icon} aria-hidden="true">
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
</svg>
);
}
/** The Dark/Light switch in the soft depth style; the choice persists through useTheme. */
export function AppearanceSwitch() {
const { theme, setTheme } = useTheme();
const options: { value: Theme; label: string; icon: ReactNode }[] = [
{ value: "dark", label: "Dark", icon: <MoonIcon /> },
{ value: "light", label: "Light", icon: <SunIcon /> },
];
return (
<div className="seg-depth" role="group" aria-label="Appearance">
{options.map((o) => (
<button key={o.value} type="button" className={theme === o.value ? "on" : undefined} aria-pressed={theme === o.value} onClick={() => setTheme(o.value)}>
{o.icon}
{o.label}
</button>
))}
</div>
);
}
export interface DiscoverSheetProps {
experts: DiscoverExpert[];
onOpen: (panel: DiscoverPanel) => void;
onClose: () => void;
}
export function DiscoverSheet({ experts, onOpen, onClose }: DiscoverSheetProps) {
return (
<BottomSheet title="Discover" onClose={onClose} className="sheet-discover">
{experts.length > 0 ? (
<DepthButton
label="Featured experts"
sublabel={featuredSubtitle(experts)}
onClick={() => onOpen("featured")}
leading={
<span className="depth-stack" aria-hidden="true">
{experts.slice(0, 3).map((e) => (
<HexAvatar key={String(e.id)} name={e.displayName || "Expert"} pictureUrl={e.pictureUrl} size="sm" />
))}
</span>
}
trailing={<Chevron />}
/>
) : null}
<DepthButton label="Trending minnects" sublabel="Answers people are watching" icon={<FlameIcon />} trailing={<Chevron />} onClick={() => onOpen("trending")} />
<DepthButton label="Find experts" sublabel="Search by name, topic or category" icon={<SearchIcon />} trailing={<Chevron />} onClick={() => onOpen("search")} />
<span className="type-eyebrow sheet-sub">Appearance</span>
<AppearanceSwitch />
</BottomSheet>
);
}
export interface DiscoverPanelSheetProps {
panel: DiscoverPanel;
experts: DiscoverExpert[];
trending: DiscoverMinnect[];
search?: ReactNode;
onClose: () => void;
}
const TITLES: Record<DiscoverPanel, string> = {
featured: "Featured experts",
trending: "Trending minnects",
search: "Find experts",
};
/** One Discover panel as an 80% height sheet with a close button; reuses the desktop components. */
export function DiscoverPanelSheet({ panel, experts, trending, search, onClose }: DiscoverPanelSheetProps) {
return (
<BottomSheet title={TITLES[panel]} onClose={onClose} className={`sheet-panel sheet-panel-${panel}`}>
{panel === "featured" ? <FeaturedCarousel experts={experts} size={PANEL_CAROUSEL_SIZE} variant="slide" /> : null}
{panel === "trending" ? <TrendingMinnectsRail minnects={trending} /> : null}
{panel === "search" ? search : null}
</BottomSheet>
);
}