import type { ElementType, ReactNode, RefObject } from "react"; import type { EmptyStateProps } from "../EmptyState/EmptyStateProps"; import type { Tone } from "../../Theme/tokens/builders/tone"; import type { DaySectionLabels } from "./notificationHelpers"; /** Shared tone vocabulary for the badge, row tile and zone/filter buttons — an alias of the theme's Tone. */ export type NotificationTone = Tone; /** Button size scale for the bell trigger. */ export type NotificationBellSize = "xs" | "sm" | "md" | "lg"; /** Bell trigger with the three-state badge (capped count -> dot -> nothing); renders no panel itself. */ export interface NotificationBellProps { /** Renders the capped number pill. 0 or undefined falls through to `showDot`. */ count?: number; /** Ambient-activity dot, shown only when `count` renders nothing. */ showDot?: boolean; /** Counts above this render as "+". Defaults to 99 (matches MUI/Ant). */ max?: number; /** Number-pill tone; defaults to "warning". */ tone?: NotificationTone; /** Dot tone; defaults to "info". */ dotTone?: NotificationTone; /** Defaults to lucide `Bell`. Pass a component reference, not an element. */ icon?: ElementType; /** Defaults to "sm". */ size?: NotificationBellSize; /** Badge ring colour so the pill reads on a tinted bar. Defaults to the canvas. */ ringColor?: string; /** Fires when the bell button is activated; the bell renders no panel, so toggle your popover here. */ onClick?: () => void; /** Reflected as `aria-expanded`; pass the panel's open state when the bell drives a popover. */ isOpen?: boolean; /** Required — tickets hardcoded English here; the caller must name the counts (i18n-able). */ "aria-label": string; } /** What the row chassis actually paints. Consumers map their own row type onto this. */ export interface NotificationItemModel { /** Used as the React key within a list; must be unique among the rows. */ id: string; /** Main line; ReactNode so a caller can inject links/bold without the library parsing markup. */ title: ReactNode; /** Secondary line, clamped to 2 lines. */ body?: ReactNode; /** Epoch ms, an ISO string, or a Date; drives both the relative label and day bucketing. */ timestamp: number | string | Date; /** False shows the unread indicator chosen by `unreadIndicator`; true renders the row plain. */ isRead: boolean; /** Icon for the 28x28 tile; pass a component reference, not an element. */ icon?: ElementType; /** Tints the tile and the unread indicator. */ tone?: NotificationTone; /** Avatar overlaid on the tile's corner. */ actor?: { name?: string; avatarUrl?: string | null; }; /** Small label above the title — tickets' "PT-104", hrms' "LEAVE". */ eyebrow?: ReactNode; /** Adjacent rows sharing this key collapse into one expandable run; null/undefined never collapses. */ groupKey?: string | null; /** Optional destination; NotificationItem never navigates itself. */ href?: string; } /** How the unread state reads on a row. Defaults to "bar" (the 2px left border both apps use). */ export type NotificationUnreadIndicator = "bar" | "dot" | "tint" | "none"; /** @deprecated Build rows with NotificationRow inside a NotificationCenterProvider. */ export interface NotificationItemProps { /** The mapped row model that this chassis paints. */ item: NotificationItemModel; /** Makes the tile and text a focusable button when set; without it the row is non-interactive. */ onClick?: () => void; /** Action cluster, hidden until row hover/focus unless `actionsAlwaysVisible`. */ actions?: ReactNode; /** hrms' approval rows show Approve/Reject at rest; tickets reveals on hover. */ actionsAlwaysVisible?: boolean; /** Extra block under the body: a comment blockquote, a status from-to pair. */ preview?: ReactNode; /** Right-hand meta, e.g. hrms' "waiting 3 days". */ trailing?: ReactNode; /** Applies only when `item.isRead` is false; `"none"` shows no unread cue. */ unreadIndicator?: NotificationUnreadIndicator; /** When this row represents a collapsed run, the number of rows it stands in for. */ groupCount?: number; /** Fires when a collapsed run's "Show N updates" affordance is activated. */ onExpandGroup?: () => void; } /** @deprecated Use NotificationCenter or NotificationInbox, which take NotificationEntry rows. */ export interface NotificationListProps { /** Rows in display order; each is mapped through `toItem` for rendering and, with `groupBy` "day", bucketed by its timestamp. */ items: T[]; /** The adapter. The one required piece of consumer glue. */ toItem: (row: T) => NotificationItemModel; /** "day" buckets into Today/Yesterday/Earlier; "none" is a flat list. Defaults to "day". */ groupBy?: "day" | "none"; /** Collapse adjacent rows sharing a `groupKey` behind "Show N updates". Defaults to false. */ collapseGroups?: boolean; /** Overrides the three default bucket labels — the i18n hook. */ dayLabels?: DaySectionLabels; /** Receives the original row, not the mapped model; setting it makes every default row a button. */ onItemClick?: (row: T) => void; /** The hover-revealed, keyboard-reachable action cluster per row. */ itemActions?: (row: T) => ReactNode; /** Full row override; receives the mapped model so a caller can decorate, not restart. */ renderItem?: (row: T, model: NotificationItemModel) => ReactNode; /** Replaces the sticky day-section header. */ renderSectionHeader?: (label: string) => ReactNode; /** Forwarded to each default row; has no effect when `renderItem` replaces the row. */ unreadIndicator?: NotificationUnreadIndicator; /** Forwarded to each default row; shows `itemActions` at rest instead of on hover or focus. */ actionsAlwaysVisible?: boolean; /** Names the list as a region landmark; no region role is added when omitted. */ "aria-label"?: string; } /** One tab in the zone row; `render` exists so a foreign queue (hrms' "Needs you") can host its own body. */ export interface NotificationZone { /** Matched against `activeZone` and passed to `onZoneChange` when the tab is clicked. */ id: string; /** Tab text; the capped `count` is appended in parentheses. */ label: string; /** Rendered as a capped count suffix on the tab; omit for no count. */ count?: number; /** Tab tone; defaults to "warning". */ tone?: NotificationTone; /** Replaces the whole zone body; when set, `items` for this zone is ignored. */ render?: () => ReactNode; } /** A filter chip rendered next to the zone row; count is suffixed as "(n)", capped like the badge. */ export interface NotificationFilter { /** Matched against `activeFilter` and passed to `onFilterChange` when the chip is clicked. */ id: string; /** Chip text; `count` is appended in parentheses when set. */ label: string; /** Shown even when 0, unlike a zone count; capped at 99 like the badge. Omit for no suffix. */ count?: number; } /** @deprecated Use NotificationCenter for the bell panel and NotificationInbox for the page. */ export interface NotificationPanelProps extends NotificationListProps { /** Omit for a single-zone panel (crm, spare-cart). Two zones is the tickets/hrms shape. */ zones?: NotificationZone[]; /** Id of the selected zone; that zone's `render`, if set, replaces the panel body. */ activeZone?: string; /** Receives the clicked zone's id; the panel is controlled, so update `activeZone` yourself. */ onZoneChange?: (zoneId: string) => void; /** Filter chips under the zone row; the row is omitted when empty or unset. */ filters?: NotificationFilter[]; /** Id of the selected filter chip; controlled, so update it from `onFilterChange`. */ activeFilter?: string; /** Receives the clicked filter's id; update `activeFilter` yourself. */ onFilterChange?: (filterId: string) => void; /** Shows four skeleton rows in place of the list; beats the error and empty states, but not a zone's `render`. */ isLoading?: boolean; /** Anything non-nullish shows the error state — closes tickets' documented no-error-state gap. */ error?: unknown; /** Adds a Retry button to the error state; the button is omitted when unset. */ onRetry?: () => void; /** Props for the default EmptyState. */ empty?: EmptyStateProps; /** Panel header content; omit for the default title row. */ header?: ReactNode; /** Right side of the header, e.g. "Mark all as read". */ headerAction?: ReactNode; /** Footer links under the scrollable list. */ footer?: ReactNode; /** "popover" is height-bounded and scroll-locked; "page" grows with the document. Defaults to "popover". */ variant?: "popover" | "page"; /** Fixed max height; when unset the popover variant measures its own anchored height. */ maxH?: string | number; /** Fires from the "Load more" button, which renders only while `hasMore` is true. */ onLoadMore?: () => void; /** Shows the "Load more" button under the list. Defaults to false. */ hasMore?: boolean; /** Puts the "Load more" button into its loading state. Defaults to false. */ isLoadingMore?: boolean; /** Receives the panel root element; also the node whose top edge the popover height is measured from. */ panelRef?: RefObject; }