import type { ElementType, KeyboardEvent, MouseEvent, ReactNode } from "react"; import type { Tone } from "../../../Theme/tokens/builders/tone"; /** A run of title text: a plain string, or `{ b }` for a bold run. Always rendered as text, never as markup. */ export type NotificationTitlePart = string | { b: string; }; /** The person behind an entry; shown as an avatar with the category glyph on its corner. */ export interface NotificationActor { /** Initials, the avatar colour and the alt text come from this. */ name: string; /** Avatar image; initials show until it loads, or when it fails. */ avatarUrl?: string; } /** How a decision button reads: positive gets a check, negative asks in red, primary is the solid button in the detail. */ export type NotificationActionIntent = "positive" | "negative" | "primary" | "neutral"; /** One button on an entry: a decision sent through `onAction`, or a link (with `href`) opened through `onOpen`. */ export interface NotificationAction { /** Stable id passed to `onAction`; a bulk decision needs every selected entry to carry it. */ id: string; /** Button text, e.g. "Approve"; the accessible name adds the entry's subject. */ label: string; /** Past tense for the status line and the toast, e.g. "Approved". Defaults to `label`. */ doneLabel?: string; /** Defaults to "neutral". */ intent?: NotificationActionIntent; /** "required" asks inline and blocks an empty send; "optional" asks but allows one. Omit to decide in one click. */ reason?: "required" | "optional"; /** Label over the inline reason box; defaults to `labels.reasonLabel`. */ reasonLabel?: string; /** Makes this a link: the row calls `onOpen` with this href instead of deciding. */ href?: string; } /** One notification as the app passes it in; each app writes one adapter from its rows to this shape. */ export interface NotificationEntry { /** Unique across everything the app passes; duplicates are dropped, first one wins. */ id: string; /** Key into `categories`; an unknown key renders as a neutral Activity entry. */ category: string; /** Bold parts as `{ b }`, so apps can translate templates without markup. */ title: NotificationTitlePart[]; /** Short name for button labels and toasts, e.g. "Priya's leave". */ subject: string; /** Epoch ms. */ createdAt: number; /** Leave out when the entry has no read state (most decisions). */ isRead?: boolean; /** Shows an avatar with the category glyph; without it the row shows the category tile. */ actor?: NotificationActor; /** A short code before the title, e.g. "PT-231". */ eyebrow?: string; /** One muted line under the title. */ context?: string; /** A quoted excerpt, clamped to two lines in rows. */ preview?: string; /** Full text for the detail view; falls back to `preview`. */ body?: string; /** Label/value facts for the detail view. */ fields?: Array<[label: string, value: string]>; /** A from-to change, shown as two chips. */ change?: { from: string; to: string; }; /** The app route; the row renders a real link, so a modifier-click opens a new tab. */ href?: string; /** Entries sharing a key collapse into one thread in Activity. */ groupKey?: string; /** Decisions and links shown on the row; decisions also need `onAction`. */ actions?: NotificationAction[]; /** Why it can't be opened any more; the detail shows this instead of the facts. */ blocked?: string; /** True for entries in the Archived list; only meaningful with `onArchive`. */ archived?: boolean; /** Test id root: the row gets it, its controls get `${testId}-link`, `-read`, `-archive`, `-restore`, `-more`, `-`. */ testId?: string; } /** Where a category's entries land: "always" in Needs you (decisions), "untilRead" while unread (mentions, assignments). */ export type NotificationNeedsYouRule = "always" | "untilRead"; /** How one category looks and which list its entries land in. */ export interface NotificationCategory { /** Group heading in Needs you, the type filter and the detail kicker. */ label: string; /** A lucide-react icon component; pass the component, not an element. */ icon: ElementType; /** Tints the icon tile; defaults to "neutral". */ tone?: Tone; /** Omit to land in Activity. */ needsYou?: NotificationNeedsYouRule; } /** The app's category map; its key order is the group order in Needs you. */ export type NotificationCategories = Record; /** Server counts, never computed from loaded rows; the library moves them optimistically and never below 0. */ export interface NotificationCounts { /** Open decisions plus unread `untilRead` entries; the bell number. */ needsYou: number; /** Unread entries in Activity; the bell dot and the Activity tab count. */ unread: number; } /** Why a decision did not go through: conflict = already decided, forbidden = no longer the approver, reasonRequired = the server wants a comment. */ export type NotificationActionErrorKind = "conflict" | "forbidden" | "reasonRequired" | "failed"; /** What `onAction` rejects with; build it with `notificationActionError`. An HTTP 409 maps to conflict and 403 to forbidden. */ export interface NotificationActionError { /** Picks the row state: denied for conflict and forbidden, the reason form for reasonRequired, retry for failed. */ kind: NotificationActionErrorKind; /** Shown in the row and the toast; defaults per kind from `labels`. */ message?: string; } /** The lists the panel and the inbox show. */ export type NotificationZoneKey = "needsYou" | "activity" | "archived"; /** What "Mark all read" covers: one list plus the inbox filters. No category and no query means the whole list, loaded or not. */ export interface NotificationMarkAllScope { /** The list the button sits in. */ zone: Exclude; /** The inbox type filter, a key of `categories`. */ category?: string; /** The inbox search text. */ query?: string; } /** An app-specific row action, e.g. tickets' "Stop watching PT-108"; shown on hover and in the phone menu. */ export interface NotificationExtraAction { /** Unique within the entry; also the test id suffix. */ id: string; /** Accessible name and tooltip. */ label: string; /** A lucide-react icon component. */ icon: ElementType; /** Shown as a success toast once `onSelect` resolves. */ doneLabel?: string; /** Runs the action; a rejected promise shows "Couldn't …" in a toast. */ onSelect: (entry: NotificationEntry) => void | Promise; } /** The list's load state; "noAccess" means the person's role can't see notifications at all. */ export type NotificationLoadStatus = "loading" | "ready" | "error" | "noAccess"; /** Every string the shared parts render; English defaults in `DEFAULT_NOTIFICATION_LABELS`. Lane components add their own. */ export interface NotificationLabels { /** Screen-reader prefix of an unread row, and the detail's Unread chip. */ unread: string; /** A decision button's accessible name, e.g. "Approve: Priya's leave". */ actionName: (actionLabel: string, subject: string) => string; markRead: string; markUnread: string; archive: string; restore: string; /** The phone row's menu button. */ moreActions: (title: string) => string; /** The inbox row checkbox. */ select: (title: string) => string; /** `count` is how many older rows sit behind the thread's newest. */ threadShow: (count: number) => string; threadHide: (count: number) => string; /** Chip on a decision waiting 3 days or more. */ waiting: (days: number) => string; /** Chip on a row whose entry is `blocked`. */ noAccess: string; /** Spoken between the two change chips. */ changedTo: string; /** Beside the disabled buttons while offline. */ offlineNote: string; reasonLabel: string; reasonOptional: string; /** Placeholder of the reason box on a negative action. */ reasonPlaceholder: string; /** A required reason left empty on a negative action. */ reasonMissingNegative: string; /** A required reason left empty on any other action. */ reasonMissing: string; keyCtrl: string; keyEnter: string; keyEsc: string; toSend: string; toCancel: string; cancel: string; undo: string; /** Status line while a decision is on its way. */ sending: string; /** Status line lead-in when a decision failed, e.g. "Couldn't approve." */ couldNot: (actionLabel: string) => string; retry: string; dismiss: string; conflictMessage: string; forbiddenMessage: string; failedMessage: string; reasonRequiredMessage: string; offlineMessage: string; /** Toast title of a decision, e.g. "Approved · Priya's leave". */ decided: (doneLabel: string, subject: string) => string; /** Spoken when a decision is made, with the undo hint. */ decidedAnnouncement: (doneLabel: string, subject: string) => string; /** The subject of a bulk decision, e.g. "3 requests". */ requests: (count: number) => string; /** Toast description when a reason was given. */ reasonQuote: (reason: string) => string; /** Toast title when decisions failed; `count` is above 1 for a bulk decision. */ decisionFailed: (actionLabel: string, count: number) => string; /** Toast title when someone else decided first. */ conflicted: (count: number) => string; /** Toast title when the person may no longer decide. */ forbidden: (count: number) => string; /** Toast title when the server asked for a comment. */ reasonNeeded: (actionLabel: string) => string; archived: (count: number) => string; restored: string; markedRead: (count: number) => string; markAllFailed: string; readFailed: (read: boolean) => string; archiveFailed: string; restoreFailed: string; /** Toast title when an extra action rejects. */ extraFailed: (actionLabel: string) => string; undone: string; decisionUndone: string; arrivedOne: (title: string) => string; arrivedMany: (count: number) => string; /** The detail's footer button; `appName` comes from the source. */ openIn: (appName?: string) => string; blockedTitle: string; back: string; /** Category label of an entry whose key is not in `categories`. */ otherCategory: string; now: string; justNow: string; todayAt: (time: string) => string; yesterdayAt: (time: string) => string; today: string; yesterday: string; thisWeek: string; older: string; } /** Second argument of `onOpen`: the click or key press that opened the entry. */ export type NotificationOpenEvent = MouseEvent | KeyboardEvent; /** The data and callbacks every notification component on a page shares; a feature appears only when its callback is passed. */ export interface NotificationSourceProps { /** All loaded entries, any order; the app's cache stays the source of truth. */ entries: NotificationEntry[]; /** From the server, never counted from `entries`. */ counts: NotificationCounts; /** Label, icon, tone and list rule per category key; keep it stable, a new object re-derives every row. */ categories: NotificationCategories; /** Row click and "Open in …"; the app navigates. An unread entry is marked read first. For a link action, `entry.href` is the action's. */ onOpen: (entry: NotificationEntry, event?: NotificationOpenEvent, action?: NotificationAction) => void; /** Sends a decision once its 6 s undo window ends. Reject with a `NotificationActionError` or an HTTP error. Omit for no inline decisions. */ onAction?: (entry: NotificationEntry, action: NotificationAction, reason?: string) => Promise; /** Read and unread, one or many, sent at once. Omit for no read toggles. */ onMarkRead?: (ids: string[], read: boolean) => Promise; /** Everything in the scope, loaded or not; sent after the undo window. Omit to hide "Mark all read". */ onMarkAllRead?: (scope: NotificationMarkAllScope) => Promise; /** Sent after the undo window; Needs-you entries can't be archived. Omit for no archive and no Archived tab. */ onArchive?: (ids: string[]) => Promise; /** Moves archived entries back to Activity, sent at once. Omit to hide "Move back". */ onRestore?: (ids: string[]) => Promise; /** Next page by cursor. Omit and the list ends at what is loaded. */ onLoadMore?: () => void; /** Shows the load-more control. */ hasMore?: boolean; /** Puts the load-more control in its busy state. */ isLoadingMore?: boolean; /** App-specific row actions for one entry. */ extraActions?: (entry: NotificationEntry) => NotificationExtraAction[]; /** Defaults to "ready". Pass "loading" until the first fetch resolves, so it is not announced as new arrivals. */ status?: NotificationLoadStatus; /** Retries a failed load from the error state. */ onRetry?: () => void; /** Disables every change and shows the offline banner. */ isOffline?: boolean; /** When the loaded entries were fetched, epoch ms; the offline banner names it. */ loadedAt?: number; /** As in "Open in hrms". */ appName?: string; /** Overrides for the shared strings, anything left out stays English; keep it stable across renders. */ labels?: Partial; /** BCP 47 tag for dates, relative times and numbers. Defaults to "en-IN". */ locale?: string; } /** Shares one overlay (undo windows, sends, optimistic counts, the announcer) across every notification component below it. Needs a `ToasterProvider` above. */ export interface NotificationCenterProviderProps extends NotificationSourceProps { /** The bell, panel, inbox or custom layout that reads `useNotificationCenter()`. */ children: ReactNode; } /** No source props at all: the component reads the nearest `NotificationCenterProvider`. `labels` stays free for the component's own strings. */ export type NotificationSourceOmitted = { [K in Exclude]?: undefined; }; /** Pass every source prop when there is no provider above, or none inside one; a mix is a type error. */ export type NotificationSourceBinding = NotificationSourceProps | NotificationSourceOmitted;