"use client" /** * App shell wiring for {@link WhatsNewSheet} — product catalog, routing, seen list. */ import * as React from "react" import { Link, useLocation, useNavigate } from "react-router" import { Button } from "@/components/ui/button" import { useWhatsNewSeen } from "@/components/product-home/product-home-parts" import { useProductSwitch } from "@/contexts/product-route-sync" import { useProduct, type Product } from "@/contexts/product-context" import { getProductBrand } from "@/lib/product-brand" import { findCatalogEntry, type ProductWhatsNewItem } from "@/lib/mock/product-catalog" import { isProductsHomePath } from "@/lib/product-home" import { buildProductHomeInventory, collectWhatsNewUpdates, productHomeSlug, visibleWhatsNew, whatsNewUpdateId, type ProductHomeCard, } from "@/lib/product-home" import { WhatsNewSheet, WhatsNewSheetEmpty, WhatsNewSheetTrigger, type WhatsNewSheetFeaturedUpdate, } from "@exxatdesignux/ui/components/ui/whats-new-sheet" export { WHATS_NEW_SHEET_PANEL_PROPS, WhatsNewSheetTrigger, type WhatsNewSheetFeaturedUpdate, } from "@exxatdesignux/ui/components/ui/whats-new-sheet" /** @deprecated Import `WhatsNewSheet` from `@exxatdesignux/ui/components/ui/whats-new-sheet`. */ export const WhatsNewSheetSmall = WhatsNewSheet interface PanelUpdate { id: string item: ProductWhatsNewItem card: ProductHomeCard | null product?: Product sourceLabel?: string brandColor: string } export interface WhatsNewPanelModel { updates: PanelUpdate[] unseen: PanelUpdate[] featured: PanelUpdate | null dismiss: (id: string) => void } function panelUpdatesForProduct(product: Product): PanelUpdate[] { const slug = productHomeSlug(product) const brand = getProductBrand(product) return visibleWhatsNew(findCatalogEntry(product)?.whatsNew).map(item => ({ id: whatsNewUpdateId(slug, item.title), item, card: null, product, brandColor: brand?.brandColor ?? "var(--brand)", })) } function panelUpdatesForWorkspace( customProducts: ReturnType["customProducts"], hiddenProducts: ReturnType["hiddenProducts"], ): PanelUpdate[] { const { owned } = buildProductHomeInventory(customProducts, hiddenProducts) return collectWhatsNewUpdates(owned).map(({ id, card, item }) => ({ id, item, card, sourceLabel: `New in ${card.label}`, brandColor: card.brandColor, })) } /** Product scope first; falls back to workspace when the current product has no notes. */ export function useWhatsNewPanelModel(): WhatsNewPanelModel { const { pathname } = useLocation() const { product, customProducts, hiddenProducts } = useProduct() const { seen, dismiss } = useWhatsNewSeen() const updates = React.useMemo(() => { const onProductsHome = isProductsHomePath(pathname) if (onProductsHome) { return panelUpdatesForWorkspace(customProducts, hiddenProducts) } const productUpdates = panelUpdatesForProduct(product) if (productUpdates.length > 0) return productUpdates return panelUpdatesForWorkspace(customProducts, hiddenProducts) }, [pathname, product, customProducts, hiddenProducts]) return React.useMemo(() => { const seenIds = new Set(seen) const unseen = updates.filter(update => !seenIds.has(update.id)) const featured = unseen[0] ?? updates[0] ?? null return { updates, unseen, featured, dismiss } }, [updates, seen, dismiss]) } /** All visible release notes for the timeline page (owned products, catalog order). */ export function useWhatsNewTimelineUpdates(): PanelUpdate[] { const { customProducts, hiddenProducts } = useProduct() return React.useMemo( () => panelUpdatesForWorkspace(customProducts, hiddenProducts), [customProducts, hiddenProducts], ) } const WhatsNewSheetContext = React.createContext<{ open: boolean openSheet: () => void closeSheet: () => void unseenCount: number } | null>(null) function ReleaseHistoryFooter({ onNavigate, }: { onNavigate: () => void }) { return ( ) } function WhatsNewSheetShell({ open, onOpenChange, featured, onDismiss, }: { open: boolean onOpenChange: (open: boolean) => void featured: PanelUpdate | null onDismiss: (id: string) => void }) { const navigate = useNavigate() const switchProduct = useProductSwitch() const footerSecondary = ( onOpenChange(false)} /> ) if (!featured) { return ( ) } const { id, item, card, product, sourceLabel, brandColor } = featured const handlePrimaryAction = () => { if (card?.href) { navigate(card.href) } else if (card) { switchProduct(card.product, card.customIndex) } else if (product) { switchProduct(product) } onOpenChange(false) } const sheetFeatured: WhatsNewSheetFeaturedUpdate = { id, brandColor, item: { title: item.title, eyebrow: item.eyebrow, sourceLabel, highlights: item.highlights, body: item.body, primaryActionLabel: item.primaryActionLabel, }, } return ( ) } export function WhatsNewSheetProvider({ children }: { children: React.ReactNode }) { const [open, setOpen] = React.useState(false) const model = useWhatsNewPanelModel() const value = React.useMemo( () => ({ open, openSheet: () => setOpen(true), closeSheet: () => setOpen(false), unseenCount: model.unseen.length, }), [open, model.unseen.length], ) return ( {children} ) } export function useWhatsNewSheet() { const context = React.useContext(WhatsNewSheetContext) if (!context) { throw new Error("useWhatsNewSheet must be used within WhatsNewSheetProvider") } return context }