{"version":3,"file":"Chat.cjs","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — message grouping needs\n * more context than a list does: currentUserId to decide sides, groupWindowMs and\n * now to decide where a bubble group breaks, typing for the indicator. The remaining\n * props are the composer's (placeholder, composerActions, composerDisabled, onSend,\n * onSendError) and the slots (renderAvatar, header, emptyState).\n */\nimport { useEffect, useLayoutEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport type { ContextMenuItem } from \"../ContextMenu\";\nimport { EmptyState } from \"../EmptyState\";\nimport { ChatBubble } from \"./ChatBubble\";\nimport { ChatComposer, type ChatComposerHandle } from \"./ChatComposer\";\nimport { chatStrings, dayLabel, groupMessages, typingLabel, type ChatMessage } from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface ChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n    /** The thread, **oldest first**. Never reordered by the component. */\n    messages: readonly ChatMessage[];\n    /** Author id treated as \"own\" — decides side, colour and status ticks. */\n    currentUserId?: string;\n    /** Renders the composer when given. Receives the trimmed text. */\n    onSend?: (text: string) => void | Promise<void>;\n    /** Enables the retry control on a `\"failed\"` message. */\n    onRetry?: (message: ChatMessage) => void;\n    /** Names currently typing. One, two or a count is phrased for you. */\n    typing?: readonly string[];\n    /** Avatar for the first message of a run — an `<Avatar>`, an `<Icon>`. */\n    renderAvatar?: (message: ChatMessage) => ReactNode;\n    /** Rendered above the thread, inside the panel. */\n    header?: ReactNode;\n    /** Shown when there are no messages. */\n    emptyState?: ReactNode;\n    /** Gap that still keeps consecutive messages in one run. Default 5 min. */\n    groupWindowMs?: number;\n    /** Locale for labels. Default `\"pt-BR\"`. */\n    locale?: \"pt-BR\" | \"en\";\n    /** Reference instant for \"Hoje\"/\"Ontem\". Default: now, at render time. */\n    now?: number;\n    /** Placeholder for the composer. */\n    placeholder?: string;\n    /** Extra controls inside the composer, before the send button. */\n    composerActions?: ReactNode;\n    /** Disable the composer — no permission, thread archived, offline. */\n    composerDisabled?: boolean;\n    /** Called when `onSend` rejects. The draft stays in the field either way. */\n    onSendError?: (error: unknown) => void;\n    /**\n     * Toggles a reaction on a message. Enables the reaction chips.\n     *\n     * Called with the emoji that was pressed, including one the user already\n     * reacted with: reacting again clears it, rather than stacking a second copy.\n     * The app owns that rule — the component only reports the press.\n     */\n    onReact?: (message: ChatMessage, emoji: string) => void;\n    /**\n     * Per-message actions: reply, forward, star, edit, delete.\n     *\n     * Returns the items for one message, opened by right click, by long press on\n     * touch, and by a `⋮` button on the bubble. Buttons painted on every bubble\n     * turn a conversation into a toolbar, which is why this is a menu.\n     */\n    messageActions?: (message: ChatMessage) => ContextMenuItem[];\n    /** Called when a reply stub is activated — scroll to the quoted message. */\n    onQuoteClick?: (message: ChatMessage) => void;\n}\n\n/**\n * A message thread: grouped by author and by day, own messages on one side, with\n * delivery state, a typing indicator and an optional composer.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and\n * emits intent (`onSend`, `onRetry`). Where messages come from — REST, the SDK's\n * `createWebSocket`, an SSE stream — and how an optimistic insert is done stay with\n * the app, because those differ per backend and a component that assumed one would\n * be wrong for most.\n *\n * It works as a comment thread too: that is the same component with\n * `currentUserId` set and no `typing`.\n *\n * @example\n * <Chat\n *     messages={messages}\n *     currentUserId={me.id}\n *     typing={typingNames}\n *     onSend={(text) => send({ text })}\n *     onRetry={(message) => resend(message.id)}\n * />\n */\nexport function Chat({\n    messages,\n    currentUserId,\n    onSend,\n    onRetry,\n    typing = [],\n    renderAvatar,\n    header,\n    emptyState,\n    groupWindowMs,\n    locale = \"pt-BR\",\n    now,\n    placeholder,\n    composerActions,\n    composerDisabled,\n    onSendError,\n    onReact,\n    messageActions,\n    onQuoteClick,\n    className,\n    ...rest\n}: ChatProps) {\n    const strings = chatStrings(locale);\n    const sections = groupMessages({ messages, currentUserId, windowMs: groupWindowMs });\n    const composer = useRef<ChatComposerHandle | null>(null);\n    const thread = useRef<HTMLDivElement | null>(null);\n    const stuckToBottom = useRef(true);\n\n    /**\n     * Remember whether the reader is at the bottom, before the next batch lands.\n     *\n     * A thread that always scrolls to the newest message yanks somebody out of the\n     * history they were reading, every time anyone types. So the jump only happens\n     * when they were already at the bottom — the same rule every chat app converges\n     * on. The 48px slack covers a partially visible last row.\n     */\n    const trackPosition = (): void => {\n        const node = thread.current;\n        if (!node) return;\n        const distance = node.scrollHeight - node.scrollTop - node.clientHeight;\n        stuckToBottom.current = distance < 48;\n    };\n\n    useLayoutEffect(() => {\n        const node = thread.current;\n        if (!node || !stuckToBottom.current) return;\n        node.scrollTop = node.scrollHeight;\n    }, [messages, typing]);\n\n    useEffect(() => {\n        const node = thread.current;\n        if (!node) return;\n        node.scrollTop = node.scrollHeight;\n        // Mount lands on the newest message; from then on `trackPosition` decides.\n    }, []);\n\n    const typingText = typingLabel(typing, locale);\n\n    return (\n        <div className={cn(styles.panel, className)} {...rest}>\n            {header && <header className={styles.header}>{header}</header>}\n\n            <div\n                ref={thread}\n                className={styles.thread}\n                onScroll={trackPosition}\n                role=\"log\"\n                aria-live=\"polite\"\n                aria-relevant=\"additions text\"\n                tabIndex={0}\n                aria-label={strings.thread}\n            >\n                {messages.length === 0\n                    ? (emptyState ?? <EmptyState title={strings.empty} />)\n                    : sections.map((section) =>\n                          section.kind === \"day\" ? (\n                              <div key={section.key} className={styles.day}>\n                                  <span className={styles.dayLabel}>\n                                      {dayLabel(section.date, { locale, now })}\n                                  </span>\n                              </div>\n                          ) : (\n                              <div\n                                  key={section.key}\n                                  className={cn(styles.run, section.own && styles.ownRun)}\n                              >\n                                  {renderAvatar && (\n                                      <div className={styles.avatar} aria-hidden=\"true\">\n                                          {renderAvatar(section.messages[0])}\n                                      </div>\n                                  )}\n                                  <div className={styles.runBody}>\n                                      <span className={styles.author}>\n                                          {section.own\n                                              ? strings.you\n                                              : (section.authorName ?? section.authorId)}\n                                      </span>\n                                      <ul className={styles.bubbles}>\n                                          {section.messages.map((message) => (\n                                              <ChatBubble\n                                                  key={message.id}\n                                                  message={message}\n                                                  own={section.own}\n                                                  locale={locale}\n                                                  onRetry={onRetry}\n                                                  onReact={onReact}\n                                                  messageActions={messageActions}\n                                                  onQuoteClick={onQuoteClick}\n                                              />\n                                          ))}\n                                      </ul>\n                                  </div>\n                              </div>\n                          ),\n                      )}\n            </div>\n\n            {typingText && (\n                <p className={styles.typing} aria-live=\"polite\">\n                    {typingText}\n                </p>\n            )}\n\n            {onSend && (\n                <ChatComposer\n                    ref={composer}\n                    onSend={onSend}\n                    locale={locale}\n                    placeholder={placeholder}\n                    actions={composerActions}\n                    disabled={composerDisabled}\n                    onError={onSendError}\n                />\n            )}\n        </div>\n    );\n}\n"],"mappings":"kQA8FA,SAAgB,EAAK,CACjB,WACA,gBACA,SACA,UACA,SAAS,CAAC,EACV,eACA,SACA,aACA,gBACA,SAAS,QACT,MACA,cACA,kBACA,mBACA,cACA,UACA,iBACA,eACA,YACA,GAAG,GACO,CACV,IAAM,EAAU,EAAA,YAAY,CAAM,EAC5B,EAAW,EAAA,cAAc,CAAE,WAAU,gBAAe,SAAU,CAAc,CAAC,EAC7E,GAAA,EAAW,EAAA,OAAA,CAAkC,IAAI,EACjD,GAAA,EAAS,EAAA,OAAA,CAA8B,IAAI,EAC3C,GAAA,EAAgB,EAAA,OAAA,CAAO,EAAI,EAU3B,MAA4B,CAC9B,IAAM,EAAO,EAAO,QACpB,GAAI,CAAC,EAAM,OACX,IAAM,EAAW,EAAK,aAAe,EAAK,UAAY,EAAK,aAC3D,EAAc,QAAU,EAAW,EACvC,GAEA,EAAA,EAAA,gBAAA,KAAsB,CAClB,IAAM,EAAO,EAAO,QACf,GAAS,EAAc,UAC5B,EAAK,UAAY,EAAK,aAC1B,EAAG,CAAC,EAAU,CAAM,CAAC,GAErB,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAM,EAAO,EAAO,QACf,IACL,EAAK,UAAY,EAAK,aAE1B,EAAG,CAAC,CAAC,EAEL,IAAM,EAAa,EAAA,YAAY,EAAQ,CAAM,EAE7C,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,CAAS,EAAG,GAAI,EAAjD,SAAA,CACK,IAAU,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,OAAS,SAAA,CAAe,CAAA,GAE7D,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,IAAK,EACL,UAAW,EAAA,QAAO,OAClB,SAAU,EACV,KAAK,MACL,YAAU,SACV,gBAAc,iBACd,SAAU,EACV,aAAY,EAAQ,OAEnB,SAAA,EAAS,SAAW,EACd,IAAc,EAAA,EAAA,IAAA,CAAC,EAAA,WAAD,CAAY,MAAO,EAAQ,KAAQ,CAAA,EAClD,EAAS,IAAK,GACV,EAAQ,OAAS,OACb,EAAA,EAAA,IAAA,CAAC,MAAD,CAAuB,UAAW,EAAA,QAAO,IACrC,UAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,SACnB,SAAA,EAAA,SAAS,EAAQ,KAAM,CAAE,SAAQ,KAAI,CAAC,CACrC,CAAA,CACL,EAJK,EAAQ,GAIb,GAEL,EAAA,EAAA,KAAA,CAAC,MAAD,CAEI,UAAW,EAAA,GAAG,EAAA,QAAO,IAAK,EAAQ,KAAO,EAAA,QAAO,MAAM,EAF1D,SAAA,CAIK,IACG,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,OAAQ,cAAY,OACtC,SAAA,EAAa,EAAQ,SAAS,EAAE,CAChC,CAAA,GAET,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,OACnB,SAAA,EAAQ,IACH,EAAQ,IACP,EAAQ,YAAc,EAAQ,QACnC,CAAA,GACN,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,QACjB,SAAA,EAAQ,SAAS,IAAK,IACnB,EAAA,EAAA,IAAA,CAAC,EAAA,WAAD,CAEa,UACT,IAAK,EAAQ,IACL,SACC,UACA,UACO,iBACF,cACjB,EARQ,EAAQ,EAQhB,CACJ,CACD,CAAA,CACH,CACJ,CAAA,CAAA,CA7BI,EAAA,EAAQ,GA6BZ,CAEb,CACL,CAAA,EAEJ,IACG,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAW,EAAA,QAAO,OAAQ,YAAU,SAClC,SAAA,CACF,CAAA,EAGN,IACG,EAAA,EAAA,IAAA,CAAC,EAAA,aAAD,CACI,IAAK,EACG,SACA,SACK,cACb,QAAS,EACT,SAAU,EACV,QAAS,CACZ,CAAA,CAEJ,GAEb"}