eidos-ui 4.1.1 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/chip/index.cjs +2 -2
  2. package/dist/chip/index.js +1 -1
  3. package/dist/{chunk-SOS4K7VC.js → chunk-45XFUEYG.js} +2 -2
  4. package/dist/{chunk-AWPJH3UB.js → chunk-4MURW4H3.js} +2 -2
  5. package/dist/{chunk-2WDL2ZVP.js → chunk-5TRO6CYR.js} +10 -4
  6. package/dist/chunk-5TRO6CYR.js.map +1 -0
  7. package/dist/{chunk-SJPCBRKR.cjs → chunk-5YTPZN6X.cjs} +3 -3
  8. package/dist/{chunk-SJPCBRKR.cjs.map → chunk-5YTPZN6X.cjs.map} +1 -1
  9. package/dist/{chunk-7VMF6LIC.cjs → chunk-6Z42NMP4.cjs} +9 -9
  10. package/dist/{chunk-7VMF6LIC.cjs.map → chunk-6Z42NMP4.cjs.map} +1 -1
  11. package/dist/{chunk-VGMRIXW4.cjs → chunk-7OWJQHHM.cjs} +25 -25
  12. package/dist/{chunk-VGMRIXW4.cjs.map → chunk-7OWJQHHM.cjs.map} +1 -1
  13. package/dist/{chunk-FSX7DDLO.js → chunk-BMRZHFDP.js} +97 -52
  14. package/dist/chunk-BMRZHFDP.js.map +1 -0
  15. package/dist/{chunk-43DJ3GGR.js → chunk-CEVMMYJ5.js} +2 -2
  16. package/dist/{chunk-DCUIT7SI.js → chunk-CIDHX2EW.js} +2 -2
  17. package/dist/{chunk-MKQVDEHI.cjs → chunk-CPRNLW4M.cjs} +13 -16
  18. package/dist/chunk-CPRNLW4M.cjs.map +1 -0
  19. package/dist/{chunk-6JYXGRJ3.cjs → chunk-EAUTJAKV.cjs} +3 -3
  20. package/dist/{chunk-6JYXGRJ3.cjs.map → chunk-EAUTJAKV.cjs.map} +1 -1
  21. package/dist/{chunk-OERK3T7E.js → chunk-ECP5J27K.js} +13 -16
  22. package/dist/chunk-ECP5J27K.js.map +1 -0
  23. package/dist/{chunk-5HRVHEHE.cjs → chunk-EJS22Y4B.cjs} +3 -3
  24. package/dist/{chunk-5HRVHEHE.cjs.map → chunk-EJS22Y4B.cjs.map} +1 -1
  25. package/dist/{chunk-HKLLKNRQ.cjs → chunk-EM4WYYFN.cjs} +3 -3
  26. package/dist/{chunk-HKLLKNRQ.cjs.map → chunk-EM4WYYFN.cjs.map} +1 -1
  27. package/dist/{chunk-Z3OEYDNI.js → chunk-EUQQG5WH.js} +2 -2
  28. package/dist/{chunk-FAGQGGT3.js → chunk-GVLQQGI3.js} +2 -2
  29. package/dist/{chunk-K6VEUJLY.cjs → chunk-I36GOOCM.cjs} +18 -18
  30. package/dist/{chunk-K6VEUJLY.cjs.map → chunk-I36GOOCM.cjs.map} +1 -1
  31. package/dist/{chunk-LXRNFQEI.js → chunk-JJJXVJUW.js} +3 -3
  32. package/dist/{chunk-FFYPSLQV.js → chunk-KUP3CP24.js} +8 -8
  33. package/dist/{chunk-UTOTOWPS.js → chunk-LPSRS5GI.js} +3 -3
  34. package/dist/{chunk-3QBMSZSE.cjs → chunk-NLTNIQG7.cjs} +3 -3
  35. package/dist/{chunk-3QBMSZSE.cjs.map → chunk-NLTNIQG7.cjs.map} +1 -1
  36. package/dist/{chunk-MUMTPMWD.cjs → chunk-NNNMXVHI.cjs} +3 -3
  37. package/dist/{chunk-MUMTPMWD.cjs.map → chunk-NNNMXVHI.cjs.map} +1 -1
  38. package/dist/{chunk-57BVERWD.js → chunk-NODLZFCN.js} +3 -3
  39. package/dist/{chunk-DHOBGYRD.js → chunk-NQ2IY4A2.js} +3 -3
  40. package/dist/{chunk-YHSCUPP6.cjs → chunk-QKCYXFET.cjs} +102 -57
  41. package/dist/chunk-QKCYXFET.cjs.map +1 -0
  42. package/dist/{chunk-SONL2RCW.js → chunk-QVUMYDRE.js} +5 -5
  43. package/dist/{chunk-SJKD3WCV.cjs → chunk-S3IZPWYP.cjs} +5 -5
  44. package/dist/{chunk-SJKD3WCV.cjs.map → chunk-S3IZPWYP.cjs.map} +1 -1
  45. package/dist/{chunk-GRKC4PIX.cjs → chunk-SHGRJX7B.cjs} +3 -3
  46. package/dist/{chunk-GRKC4PIX.cjs.map → chunk-SHGRJX7B.cjs.map} +1 -1
  47. package/dist/{chunk-BPVOQLCA.cjs → chunk-TFCOF5FC.cjs} +2 -2
  48. package/dist/chunk-TFCOF5FC.cjs.map +1 -0
  49. package/dist/{chunk-IJW6YCJB.cjs → chunk-TTFZKWPH.cjs} +5 -5
  50. package/dist/{chunk-IJW6YCJB.cjs.map → chunk-TTFZKWPH.cjs.map} +1 -1
  51. package/dist/{chunk-JO4NDJRA.cjs → chunk-U4B6C7OP.cjs} +6 -6
  52. package/dist/{chunk-JO4NDJRA.cjs.map → chunk-U4B6C7OP.cjs.map} +1 -1
  53. package/dist/{chunk-ADB7QRX5.cjs → chunk-U4MV7P4A.cjs} +5 -5
  54. package/dist/{chunk-ADB7QRX5.cjs.map → chunk-U4MV7P4A.cjs.map} +1 -1
  55. package/dist/{chunk-IBT535UH.js → chunk-UAAQVIZQ.js} +2 -2
  56. package/dist/chunk-UAAQVIZQ.js.map +1 -0
  57. package/dist/{chunk-VPN7AR7T.cjs → chunk-USGEUG2O.cjs} +4 -4
  58. package/dist/{chunk-VPN7AR7T.cjs.map → chunk-USGEUG2O.cjs.map} +1 -1
  59. package/dist/{chunk-TOLNPD7S.js → chunk-VA23PQTA.js} +2 -2
  60. package/dist/{chunk-BKUZF7CW.cjs → chunk-VY7DTPO3.cjs} +21 -15
  61. package/dist/chunk-VY7DTPO3.cjs.map +1 -0
  62. package/dist/{chunk-D2PXPX56.js → chunk-YAZSZFLB.js} +4 -4
  63. package/dist/{chunk-YYA5SWXD.js → chunk-ZBK4KYKJ.js} +2 -2
  64. package/dist/color-picker/index.cjs +3 -3
  65. package/dist/color-picker/index.js +2 -2
  66. package/dist/combobox/index.cjs +3 -3
  67. package/dist/combobox/index.js +2 -2
  68. package/dist/context-menu/index.cjs +4 -4
  69. package/dist/context-menu/index.js +3 -3
  70. package/dist/conversation/index.cjs +5 -5
  71. package/dist/conversation/index.js +4 -4
  72. package/dist/data-grid/index.cjs +10 -10
  73. package/dist/data-grid/index.js +9 -9
  74. package/dist/date-picker/index.cjs +4 -4
  75. package/dist/date-picker/index.d.cts +15 -1
  76. package/dist/date-picker/index.d.ts +15 -1
  77. package/dist/date-picker/index.js +3 -3
  78. package/dist/dropdown/index.cjs +2 -2
  79. package/dist/dropdown/index.d.cts +10 -0
  80. package/dist/dropdown/index.d.ts +10 -0
  81. package/dist/dropdown/index.js +1 -1
  82. package/dist/index.cjs +20 -20
  83. package/dist/index.css +1 -1
  84. package/dist/index.js +19 -19
  85. package/dist/menu/index.cjs +3 -3
  86. package/dist/menu/index.js +2 -2
  87. package/dist/page-layout/index.cjs +5 -5
  88. package/dist/page-layout/index.js +4 -4
  89. package/dist/pagination/index.cjs +4 -4
  90. package/dist/pagination/index.js +3 -3
  91. package/dist/select/index.cjs +3 -3
  92. package/dist/select/index.js +2 -2
  93. package/dist/split-button/index.cjs +4 -4
  94. package/dist/split-button/index.js +3 -3
  95. package/dist/split-chip/index.cjs +3 -3
  96. package/dist/split-chip/index.js +2 -2
  97. package/dist/table/index.cjs +9 -9
  98. package/dist/table/index.js +8 -8
  99. package/dist/tag-input/index.cjs +4 -4
  100. package/dist/tag-input/index.js +3 -3
  101. package/dist/theme-editor/index.cjs +5 -5
  102. package/dist/theme-editor/index.js +4 -4
  103. package/dist/toolbar/index.cjs +4 -4
  104. package/dist/toolbar/index.js +3 -3
  105. package/package.json +1 -1
  106. package/dist/chunk-2WDL2ZVP.js.map +0 -1
  107. package/dist/chunk-BKUZF7CW.cjs.map +0 -1
  108. package/dist/chunk-BPVOQLCA.cjs.map +0 -1
  109. package/dist/chunk-FSX7DDLO.js.map +0 -1
  110. package/dist/chunk-IBT535UH.js.map +0 -1
  111. package/dist/chunk-MKQVDEHI.cjs.map +0 -1
  112. package/dist/chunk-OERK3T7E.js.map +0 -1
  113. package/dist/chunk-YHSCUPP6.cjs.map +0 -1
  114. /package/dist/{chunk-SOS4K7VC.js.map → chunk-45XFUEYG.js.map} +0 -0
  115. /package/dist/{chunk-AWPJH3UB.js.map → chunk-4MURW4H3.js.map} +0 -0
  116. /package/dist/{chunk-43DJ3GGR.js.map → chunk-CEVMMYJ5.js.map} +0 -0
  117. /package/dist/{chunk-DCUIT7SI.js.map → chunk-CIDHX2EW.js.map} +0 -0
  118. /package/dist/{chunk-Z3OEYDNI.js.map → chunk-EUQQG5WH.js.map} +0 -0
  119. /package/dist/{chunk-FAGQGGT3.js.map → chunk-GVLQQGI3.js.map} +0 -0
  120. /package/dist/{chunk-LXRNFQEI.js.map → chunk-JJJXVJUW.js.map} +0 -0
  121. /package/dist/{chunk-FFYPSLQV.js.map → chunk-KUP3CP24.js.map} +0 -0
  122. /package/dist/{chunk-UTOTOWPS.js.map → chunk-LPSRS5GI.js.map} +0 -0
  123. /package/dist/{chunk-57BVERWD.js.map → chunk-NODLZFCN.js.map} +0 -0
  124. /package/dist/{chunk-DHOBGYRD.js.map → chunk-NQ2IY4A2.js.map} +0 -0
  125. /package/dist/{chunk-SONL2RCW.js.map → chunk-QVUMYDRE.js.map} +0 -0
  126. /package/dist/{chunk-TOLNPD7S.js.map → chunk-VA23PQTA.js.map} +0 -0
  127. /package/dist/{chunk-D2PXPX56.js.map → chunk-YAZSZFLB.js.map} +0 -0
  128. /package/dist/{chunk-YYA5SWXD.js.map → chunk-ZBK4KYKJ.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/eidos-ui/eidos-ui/dist/chunk-ADB7QRX5.cjs","../src/components/Conversation/Chat.component.tsx","../src/components/Conversation/MessageComposer.component.tsx","../src/components/Conversation/Conversation.hooks.ts","../src/components/Conversation/Conversation.utils.ts","../src/components/Conversation/MessageItem.component.tsx","../src/components/Conversation/CommentThread.component.tsx"],"names":["useRef","useCallback","Fragment","Paperclip","jsx","jsxs"],"mappings":"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","file":"/home/runner/work/eidos-ui/eidos-ui/dist/chunk-ADB7QRX5.cjs","sourcesContent":[null,"import React, { useCallback, useMemo } from 'react';\nimport { ArrowDown, MessagesSquare } from 'lucide-react';\nimport { Button } from '../Button';\nimport { EmptyState } from '../EmptyState';\nimport { Skeleton } from '../Skeleton';\nimport { Spinner } from '../Spinner';\nimport { MessageComposer } from './MessageComposer.component';\nimport { MessageItem } from './MessageItem.component';\nimport type { ChatProps, ConversationMessage } from './Conversation.types';\nimport {\n useLoadMoreOnScrollTop,\n useNewMessageIds,\n useScrollAnchor,\n useStickToBottom,\n} from './Conversation.hooks';\nimport { classNames, continuesRun, formatDateSeparator, isSameDay } from './Conversation.utils';\n\nconst ChatSkeleton: React.FC = () => (\n <div className=\"eidos-chat-skeleton\" aria-hidden=\"true\">\n {[0, 1, 2, 3].map((row) => (\n <div\n key={row}\n className={classNames(\n 'eidos-chat-skeleton-row',\n row % 2 === 1 && 'eidos-chat-skeleton-row--own',\n )}\n >\n <Skeleton variant=\"circular\" width={28} height={28} />\n <Skeleton variant=\"rounded\" height={48} />\n </div>\n ))}\n </div>\n);\n\n/**\n * A bottom-anchored message thread with a composer.\n *\n * Controlled and transport-agnostic: it renders the `messages` it is given and\n * reports intent through `onSend` / `onLoadMore`. It never fetches, so the\n * same array can be persisted and replayed verbatim.\n */\nexport const Chat: React.FC<ChatProps> = ({\n messages,\n currentUserId,\n onSend,\n onRetry,\n readOnly = false,\n readOnlyMessage,\n renderBody,\n messageActions,\n renderMessageFooter,\n loading = false,\n emptyContent,\n hasMore = false,\n loadingMore = false,\n onLoadMore,\n groupConsecutive = true,\n groupWindowMinutes = 5,\n showDateSeparators = true,\n formatTimestamp,\n animateNewMessages = true,\n allowAttachments = false,\n attachmentAccept,\n typing,\n unreadFromId,\n placeholder = 'Write a message...',\n sendOnEnter = true,\n ariaLabel = 'Conversation',\n className = '',\n}) => {\n const { scrollRef, contentRef, atBottom, scrollToBottom, handleScroll } =\n useStickToBottom(messages);\n const newIds = useNewMessageIds(messages, animateNewMessages);\n useScrollAnchor(scrollRef, messages);\n\n const loadMoreOnScroll = useLoadMoreOnScrollTop(scrollRef, { hasMore, loadingMore, onLoadMore });\n\n const onScroll = useCallback(() => {\n handleScroll();\n loadMoreOnScroll();\n }, [handleScroll, loadMoreOnScroll]);\n\n const byId = useMemo(() => new Map(messages.map((message) => [message.id, message])), [messages]);\n\n // An id that isn't in the current page must not draw a divider above\n // nothing - it would float at the top of whatever happens to be loaded.\n const unreadIndex = useMemo(() => {\n if (!unreadFromId) return -1;\n return messages.findIndex((message) => message.id === unreadFromId);\n }, [messages, unreadFromId]);\n\n const showComposer = !readOnly && Boolean(onSend);\n const isEmpty = !loading && messages.length === 0;\n\n const renderMessage = (message: ConversationMessage, index: number) => {\n const previous = messages[index - 1];\n const actions = readOnly ? undefined : messageActions?.(message);\n const quoted = message.parentId ? byId.get(message.parentId) : undefined;\n\n const nodes: React.ReactNode[] = [];\n\n if (showDateSeparators && (!previous || !isSameDay(previous.sentAt, message.sentAt))) {\n nodes.push(\n <li key={`date-${message.id}`} className=\"eidos-conversation-separator\">\n <span>{formatDateSeparator(message.sentAt)}</span>\n </li>,\n );\n }\n\n if (index === unreadIndex) {\n nodes.push(\n <li\n key={`unread-${message.id}`}\n className=\"eidos-conversation-separator eidos-conversation-separator--unread\"\n >\n <span>Unread messages</span>\n </li>,\n );\n }\n\n // A separator breaks a run: the message under it needs its own header.\n const grouped =\n groupConsecutive &&\n index !== unreadIndex &&\n nodes.length === 0 &&\n continuesRun(previous, message, groupWindowMinutes);\n\n nodes.push(\n <MessageItem\n key={message.id}\n message={message}\n layout=\"chat\"\n isOwn={Boolean(currentUserId) && message.author.id === currentUserId}\n grouped={grouped}\n isNew={newIds.has(message.id)}\n actions={actions}\n footer={renderMessageFooter?.(message)}\n body={renderBody?.(message)}\n onRetry={!readOnly && onRetry ? () => onRetry(message) : undefined}\n quoted={quoted}\n formatTimestamp={formatTimestamp}\n />,\n );\n\n return nodes;\n };\n\n return (\n <div className={classNames('eidos-chat', className)}>\n <div\n ref={scrollRef}\n className=\"eidos-chat-scroll\"\n onScroll={onScroll}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions\"\n aria-busy={loading}\n aria-label={ariaLabel}\n tabIndex={0}\n >\n {/* One wrapper whose height is the content height, so\n `useStickToBottom` can observe growth that doesn't change the\n message count - a streamed reply, the typing indicator. */}\n <div ref={contentRef} className=\"eidos-chat-scroll-content\">\n {hasMore && (\n <div className=\"eidos-chat-load-more\">\n {loadingMore ? (\n <span className=\"eidos-chat-loading-more\">\n <Spinner size=\"sm\" color=\"secondary\" />\n Loading earlier messages…\n </span>\n ) : (\n <Button variant=\"text\" color=\"secondary\" size=\"sm\" onClick={onLoadMore}>\n Load earlier messages\n </Button>\n )}\n </div>\n )}\n\n {loading && <ChatSkeleton />}\n\n {isEmpty &&\n (emptyContent ?? (\n <EmptyState\n icon={<MessagesSquare />}\n title=\"No messages yet\"\n description={\n showComposer ? 'Send the first message to start the conversation.' : undefined\n }\n size=\"sm\"\n />\n ))}\n\n {!loading && messages.length > 0 && (\n <ol className=\"eidos-conversation-list\">{messages.map(renderMessage)}</ol>\n )}\n\n {typing && typing.length > 0 && (\n <div className=\"eidos-chat-typing\" aria-live=\"polite\">\n <span className=\"eidos-chat-typing-dots\" aria-hidden=\"true\">\n <i />\n <i />\n <i />\n </span>\n {typing.length === 1\n ? `${typing[0].name} is typing…`\n : `${typing.length} people are typing…`}\n </div>\n )}\n </div>\n </div>\n\n {!atBottom && messages.length > 0 && (\n <div className=\"eidos-chat-jump\">\n <Button\n size=\"sm\"\n variant=\"filled\"\n color=\"secondary\"\n preIcon={ArrowDown}\n onClick={() => scrollToBottom('smooth')}\n >\n Latest\n </Button>\n </div>\n )}\n\n {showComposer && (\n <div className=\"eidos-chat-composer\">\n <MessageComposer\n onSubmit={onSend as NonNullable<typeof onSend>}\n placeholder={placeholder}\n sendOnEnter={sendOnEnter}\n allowAttachments={allowAttachments}\n attachmentAccept={attachmentAccept}\n />\n </div>\n )}\n\n {readOnly && readOnlyMessage && (\n <p className=\"eidos-conversation-readonly\">{readOnlyMessage}</p>\n )}\n </div>\n );\n};\n\nChat.displayName = 'Chat';\n","import React, { useCallback, useId, useRef, useState } from 'react';\nimport { Paperclip, Send, X } from 'lucide-react';\nimport { Avatar } from '../Avatar';\nimport { Button } from '../Button';\nimport { Chip } from '../Chip';\nimport type { MessageComposerProps } from './Conversation.types';\nimport { useAutoGrow } from './Conversation.hooks';\nimport { classNames, formatFileSize } from './Conversation.utils';\n\n/**\n * The authoring half of a conversation - a growing text field, optional\n * attachments, and a submit control.\n *\n * Exported on its own so a consumer building a bespoke list can still reuse\n * the input, and used internally by both `Chat` and `CommentThread`.\n */\nexport const MessageComposer: React.FC<MessageComposerProps> = ({\n value,\n onChange,\n onSubmit,\n onCancel,\n placeholder = 'Write a message...',\n submitLabel = 'Send',\n sendOnEnter = false,\n parentId,\n replyingTo,\n allowAttachments = false,\n attachmentAccept,\n author,\n disabled = false,\n requireBody = true,\n autoFocus = false,\n maxLength,\n className = '',\n}) => {\n const [internalBody, setInternalBody] = useState('');\n const [attachments, setAttachments] = useState<File[]>([]);\n const [pending, setPending] = useState(false);\n\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const fileInputId = useId();\n\n const isControlled = value !== undefined;\n const body = isControlled ? value : internalBody;\n\n useAutoGrow(textareaRef, body);\n\n const setBody = useCallback(\n (next: string) => {\n if (!isControlled) setInternalBody(next);\n onChange?.(next);\n },\n [isControlled, onChange],\n );\n\n const canSubmit = !disabled && !pending && (!requireBody || body.trim().length > 0);\n\n const submit = useCallback(async () => {\n if (!canSubmit) return;\n\n const draft = { body: body.trim(), attachments, parentId };\n const result = onSubmit(draft);\n\n // A synchronous handler clears immediately; an async one keeps the draft\n // until it resolves, so a rejected send never loses what was typed.\n if (!(result instanceof Promise)) {\n setBody('');\n setAttachments([]);\n return;\n }\n\n setPending(true);\n try {\n await result;\n setBody('');\n setAttachments([]);\n } catch {\n // Intentionally swallowed: the draft stays put and the consumer owns\n // error reporting (a Snackbar, an Alert, ...). Rethrowing here would\n // surface as an unhandled rejection from a keystroke.\n } finally {\n setPending(false);\n }\n }, [canSubmit, body, attachments, parentId, onSubmit, setBody]);\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (!sendOnEnter) return;\n // Shift+Enter always inserts a newline; the modifier check also leaves\n // IME composition (which fires Enter to commit) alone.\n if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing) return;\n event.preventDefault();\n void submit();\n };\n\n const addFiles = (list: FileList | null) => {\n if (!list?.length) return;\n setAttachments((current) => [...current, ...Array.from(list)]);\n // Reset so selecting the same file twice in a row still fires `change`.\n if (fileInputRef.current) fileInputRef.current.value = '';\n };\n\n const removeAttachment = (index: number) =>\n setAttachments((current) => current.filter((_, i) => i !== index));\n\n return (\n <form\n className={classNames('eidos-message-composer', className)}\n onSubmit={(event) => {\n event.preventDefault();\n void submit();\n }}\n >\n {replyingTo && (\n <div className=\"eidos-composer-reply-hint\">\n <span>\n Replying to <strong>{replyingTo}</strong>\n </span>\n {onCancel && (\n <Button\n variant=\"text\"\n color=\"secondary\"\n size=\"sm\"\n icon={X}\n onClick={onCancel}\n tooltip=\"Cancel reply\"\n aria-label=\"Cancel reply\"\n />\n )}\n </div>\n )}\n\n <div className=\"eidos-composer-row\">\n {author && (\n <div className=\"eidos-composer-avatar\">\n <Avatar\n src={author.avatarSrc}\n name={author.name}\n size=\"sm\"\n color={author.avatarColor ?? 'gray'}\n />\n </div>\n )}\n\n <div className=\"eidos-composer-field\">\n <textarea\n ref={textareaRef}\n className=\"eidos-composer-input\"\n value={body}\n onChange={(event) => setBody(event.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n disabled={disabled || pending}\n rows={1}\n maxLength={maxLength}\n // Opt-in and off by default. Only the reply composer sets it, where\n // the field appears in direct response to the user pressing Reply -\n // moving focus there is the expected outcome, not a hijack.\n autoFocus={autoFocus}\n aria-label={placeholder}\n />\n\n {attachments.length > 0 && (\n <ul className=\"eidos-composer-attachments\">\n {attachments.map((file, index) => (\n <li key={`${file.name}-${file.size}-${index}`}>\n <Chip\n size=\"sm\"\n variant=\"outlined\"\n color=\"secondary\"\n preIcon={Paperclip}\n onRemove={() => removeAttachment(index)}\n >\n {file.name}\n {file.size > 0 && ` (${formatFileSize(file.size)})`}\n </Chip>\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n\n <div className=\"eidos-composer-actions\">\n <div className=\"eidos-composer-actions-start\">\n {allowAttachments && (\n <>\n <input\n ref={fileInputRef}\n id={fileInputId}\n type=\"file\"\n multiple\n accept={attachmentAccept}\n // Hidden from both the tab order and the accessibility\n // tree. It is a proxy: the visible button above opens the\n // file dialog via `.click()`, so this input is an\n // implementation detail, not a control.\n //\n // `visually-hidden` keeps an element *focusable* - that is\n // the point of it, for skip links and screen-reader-only\n // text - so a keyboard user landed on an invisible control\n // and met the same \"Attach files\" action twice. `FileUpload`\n // already gets this right with `display: none` +\n // `tabIndex={-1}`; these two had drifted from it.\n //\n // `aria-hidden` is safe here only *because* of the\n // `tabIndex={-1}` beside it - hiding a focusable element is\n // its own violation.\n className=\"eidos-composer-file-input\"\n tabIndex={-1}\n aria-hidden=\"true\"\n onChange={(event) => addFiles(event.target.files)}\n disabled={disabled || pending}\n />\n <Button\n variant=\"text\"\n color=\"secondary\"\n size=\"sm\"\n icon={Paperclip}\n tooltip=\"Attach files\"\n aria-label=\"Attach files\"\n disabled={disabled || pending}\n onClick={() => fileInputRef.current?.click()}\n />\n </>\n )}\n {sendOnEnter && (\n <span className=\"eidos-composer-hint\">\n <kbd>Enter</kbd> to send · <kbd>Shift</kbd>+<kbd>Enter</kbd> for a new line\n </span>\n )}\n </div>\n\n <div className=\"eidos-composer-actions-end\">\n {onCancel && !replyingTo && (\n <Button\n variant=\"text\"\n color=\"secondary\"\n size=\"sm\"\n onClick={onCancel}\n disabled={pending}\n >\n Cancel\n </Button>\n )}\n <Button\n type=\"submit\"\n size=\"sm\"\n color=\"primary\"\n preIcon={Send}\n loading={pending}\n disabled={!canSubmit}\n >\n {submitLabel}\n </Button>\n </div>\n </div>\n </form>\n );\n};\n\nMessageComposer.displayName = 'MessageComposer';\n","import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport type { ConversationMessage } from './Conversation.types';\n\n/**\n * Ids that arrived *after* the first render.\n *\n * Seeded from the first commit so an initial page of history is never\n * animated - otherwise opening a thread fades forty messages in at once,\n * which reads as a glitch rather than as feedback. Only genuinely new ids get\n * the entrance class, and each one only once.\n */\nexport const useNewMessageIds = (\n messages: ConversationMessage[],\n enabled: boolean,\n): Set<string> => {\n const seen = useRef<Set<string> | null>(null);\n const [fresh, setFresh] = useState<Set<string>>(() => new Set());\n\n useEffect(() => {\n // First commit: everything already on screen counts as pre-existing.\n if (seen.current === null) {\n seen.current = new Set(messages.map((message) => message.id));\n return;\n }\n\n if (!enabled) {\n for (const message of messages) seen.current.add(message.id);\n return;\n }\n\n const added = messages.filter((message) => !seen.current?.has(message.id));\n if (!added.length) return;\n\n for (const message of added) seen.current.add(message.id);\n setFresh(new Set(added.map((message) => message.id)));\n }, [messages, enabled]);\n\n return fresh;\n};\n\ninterface StickToBottom {\n scrollRef: React.RefObject<HTMLDivElement | null>;\n /**\n * The single element wrapping everything inside the scroll region. Its\n * height *is* the content height, so observing it catches every way the\n * content can grow.\n */\n contentRef: React.RefObject<HTMLDivElement | null>;\n /** The viewport is at (or near) the newest message. */\n atBottom: boolean;\n scrollToBottom: (behavior?: ScrollBehavior) => void;\n handleScroll: () => void;\n}\n\n/** Distance from the bottom still counted as \"reading the newest message\". */\nconst BOTTOM_THRESHOLD_PX = 48;\n\n/**\n * Keeps a bottom-anchored list pinned to the newest message - but only while\n * the reader is already there.\n *\n * Scrolling someone back down while they are reading scrollback is the single\n * most common defect in chat UIs, so arrival of a new message only forces a\n * scroll when `atBottom` was already true. Otherwise the caller surfaces a\n * jump-to-latest affordance and leaves the viewport alone.\n */\nexport const useStickToBottom = (messages: ConversationMessage[]): StickToBottom => {\n const scrollRef = useRef<HTMLDivElement>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n const [atBottom, setAtBottom] = useState(true);\n // Read during a layout effect, so it must not be state - it has to be\n // correct for the commit that is happening right now, not the next render.\n const atBottomRef = useRef(true);\n const lastCount = useRef(messages.length);\n const lastScrollTop = useRef(0);\n\n const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {\n const node = scrollRef.current;\n if (!node) return;\n node.scrollTo({ top: node.scrollHeight, behavior });\n }, []);\n\n // Stickiness is released only by the *reader* moving up, never by distance\n // alone.\n //\n // Measuring distance alone is wrong the moment content can grow while a\n // smooth scroll is in flight: the scroll events fired mid-animation report\n // the gap to a bottom that has since moved further away, so the reader - who\n // never touched anything - is declared \"not at the bottom\" and the thread\n // stops following a streamed reply partway through. A programmatic scroll\n // to the bottom only ever *increases* `scrollTop`, and content growth does\n // not change it at all, so a decrease is a reliable signal of intent: wheel,\n // keyboard, scrollbar drag and touch all produce one.\n const handleScroll = useCallback(() => {\n const node = scrollRef.current;\n if (!node) return;\n const distance = node.scrollHeight - node.scrollTop - node.clientHeight;\n const movedUp = node.scrollTop < lastScrollTop.current;\n lastScrollTop.current = node.scrollTop;\n\n let next = atBottomRef.current;\n if (distance <= BOTTOM_THRESHOLD_PX) next = true;\n else if (movedUp) next = false;\n\n atBottomRef.current = next;\n setAtBottom((current) => (current === next ? current : next));\n }, []);\n\n // Jump (not animate) to the bottom on first paint so a thread opens on its\n // newest message instead of visibly scrolling there.\n useLayoutEffect(() => {\n scrollToBottom('auto');\n // eslint-disable-next-line react-hooks/exhaustive-deps -- mount only\n }, []);\n\n useLayoutEffect(() => {\n if (messages.length > lastCount.current && atBottomRef.current) {\n scrollToBottom('smooth');\n }\n lastCount.current = messages.length;\n }, [messages.length, scrollToBottom]);\n\n // The effect above only reacts to the *number* of messages. A reply that is\n // streamed into an existing message - or a typing indicator appearing, or\n // an image loading - grows the content without changing that number, and\n // used to push the newest text below the fold while the reader was sitting\n // at the bottom waiting for it.\n //\n // Observing the content wrapper covers every one of those, because its\n // height is the content height. The scroll container itself cannot be\n // observed for this: its own box never changes size, only its\n // `scrollHeight` does.\n //\n // `'auto'`, not `'smooth'`: a stream grows the content once per token, and\n // a smooth scroll restarted that often never catches up with the text.\n // `atBottomRef` still holds the reader's position from *before* the growth,\n // which is exactly the question - \"were they following along?\".\n //\n // Growth that coincides with a new message is left to the smooth scroll the\n // layout effect above already started; jumping here as well would cancel\n // that animation on every single arrival.\n useEffect(() => {\n const content = contentRef.current;\n if (!content || typeof ResizeObserver === 'undefined') return;\n\n let lastHeight = content.getBoundingClientRect().height;\n let observedCount = lastCount.current;\n const observer = new ResizeObserver(() => {\n const height = content.getBoundingClientRect().height;\n const countChanged = observedCount !== lastCount.current;\n observedCount = lastCount.current;\n if (height > lastHeight && !countChanged && atBottomRef.current) scrollToBottom('auto');\n lastHeight = height;\n });\n observer.observe(content);\n return () => observer.disconnect();\n }, [scrollToBottom]);\n\n return { scrollRef, contentRef, atBottom, scrollToBottom, handleScroll };\n};\n\n/**\n * Preserves the reader's position when older messages are prepended.\n *\n * Without this, inserting a page above the viewport pushes the content the\n * user was reading down by the height of everything added, which reads as the\n * list jumping to a random place. Capturing `scrollHeight` before the DOM\n * mutation and restoring the delta in a *layout* effect corrects it before the\n * browser paints, so there is no visible shift.\n */\nexport const useScrollAnchor = (\n scrollRef: React.RefObject<HTMLDivElement | null>,\n messages: ConversationMessage[],\n): void => {\n const previous = useRef({ height: 0, firstId: '', count: 0 });\n\n useLayoutEffect(() => {\n const node = scrollRef.current;\n if (!node) return;\n\n const firstId = messages[0]?.id ?? '';\n const before = previous.current;\n // A prepend is the only case where the first id changes while the list\n // grows - an append leaves it untouched, and a replacement shrinks it.\n const prepended =\n before.firstId !== '' && before.firstId !== firstId && messages.length > before.count;\n\n if (prepended) {\n node.scrollTop += node.scrollHeight - before.height;\n }\n\n previous.current = { height: node.scrollHeight, firstId, count: messages.length };\n }, [messages, scrollRef]);\n};\n\n/**\n * Fires `onLoadMore` once the reader approaches the top of the history.\n *\n * Latched on the way in and released only after they scroll back out of the\n * trigger zone, so a single pause near the top can't queue a dozen fetches.\n */\nexport const useLoadMoreOnScrollTop = (\n scrollRef: React.RefObject<HTMLDivElement | null>,\n {\n hasMore,\n loadingMore,\n onLoadMore,\n }: {\n hasMore?: boolean;\n loadingMore?: boolean;\n onLoadMore?: () => void;\n },\n thresholdPx = 80,\n): (() => void) => {\n const armed = useRef(true);\n\n return useCallback(() => {\n const node = scrollRef.current;\n if (!node) return;\n\n if (node.scrollTop > thresholdPx) {\n armed.current = true;\n return;\n }\n\n if (!armed.current || !hasMore || loadingMore || !onLoadMore) return;\n armed.current = false;\n onLoadMore();\n }, [scrollRef, hasMore, loadingMore, onLoadMore, thresholdPx]);\n};\n\n/**\n * Grows a textarea to fit its content, up to `maxHeight`.\n *\n * Height is written to `style.height` directly rather than through a React\n * inline style: it is measured from the live DOM after every keystroke, so it\n * cannot be expressed as a class, and routing it through state would add a\n * render per character. Assigning the property imperatively also keeps it out\n * of the server-rendered `style` attribute that CSP's `style-src-attr` gates.\n */\nexport const useAutoGrow = (\n ref: React.RefObject<HTMLTextAreaElement | null>,\n value: string,\n maxHeight = 200,\n): void => {\n useLayoutEffect(() => {\n const node = ref.current;\n if (!node) return;\n\n node.style.height = 'auto';\n const next = Math.min(node.scrollHeight, maxHeight);\n node.style.height = `${next}px`;\n node.style.overflowY = node.scrollHeight > maxHeight ? 'auto' : 'hidden';\n }, [ref, value, maxHeight]);\n};\n","import type { ConversationMessage } from './Conversation.types';\n\n/**\n * Timestamp formatting goes through `Intl` rather than a date library on\n * purpose: `dayjs` is currently bundled only into the `DatePicker` chunk, and\n * pulling it in here would add it to every consumer importing a conversation.\n * `Intl` is built in and already locale-aware.\n */\n\nconst timeFormat = new Intl.DateTimeFormat(undefined, {\n hour: '2-digit',\n minute: '2-digit',\n});\n\nconst dateFormat = new Intl.DateTimeFormat(undefined, {\n day: 'numeric',\n month: 'long',\n year: 'numeric',\n});\n\nconst dateFormatSameYear = new Intl.DateTimeFormat(undefined, {\n day: 'numeric',\n month: 'long',\n});\n\n/** Parses an ISO string, returning null rather than an `Invalid Date`. */\nexport const parseDate = (iso: string): Date | null => {\n const date = new Date(iso);\n return Number.isNaN(date.getTime()) ? null : date;\n};\n\nexport const formatMessageTime = (iso: string): string => {\n const date = parseDate(iso);\n return date ? timeFormat.format(date) : iso;\n};\n\nconst startOfDay = (date: Date): number =>\n new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();\n\nexport const isSameDay = (a: string, b: string): boolean => {\n const first = parseDate(a);\n const second = parseDate(b);\n if (!first || !second) return false;\n return startOfDay(first) === startOfDay(second);\n};\n\n/** `Today` / `Yesterday` / a full date, for the divider between days. */\nexport const formatDateSeparator = (iso: string): string => {\n const date = parseDate(iso);\n if (!date) return iso;\n\n const now = new Date();\n const dayDelta = Math.round((startOfDay(now) - startOfDay(date)) / 86_400_000);\n\n if (dayDelta === 0) return 'Today';\n if (dayDelta === 1) return 'Yesterday';\n return date.getFullYear() === now.getFullYear()\n ? dateFormatSameYear.format(date)\n : dateFormat.format(date);\n};\n\nconst FILE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];\n\nexport const formatFileSize = (bytes: number): string => {\n if (!Number.isFinite(bytes) || bytes < 0) return '';\n if (bytes < 1024) return `${bytes} B`;\n\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < FILE_UNITS.length - 1) {\n value /= 1024;\n unit += 1;\n }\n // One decimal below 10 (9.4 MB), none above it (24 MB) - the extra digit\n // stops carrying information once the integer part is two digits.\n return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${FILE_UNITS[unit]}`;\n};\n\n/**\n * Whether `message` continues a run started by `previous` - same author, same\n * day, and within the grouping window. A grouped message hides its avatar and\n * author line.\n */\nexport const continuesRun = (\n previous: ConversationMessage | undefined,\n message: ConversationMessage,\n windowMinutes: number,\n): boolean => {\n if (!previous) return false;\n if (previous.author.id !== message.author.id) return false;\n if (!isSameDay(previous.sentAt, message.sentAt)) return false;\n\n const before = parseDate(previous.sentAt);\n const after = parseDate(message.sentAt);\n if (!before || !after) return false;\n\n return Math.abs(after.getTime() - before.getTime()) <= windowMinutes * 60_000;\n};\n\nexport const sortBySentAt = (messages: ConversationMessage[]): ConversationMessage[] =>\n [...messages].sort((a, b) => {\n const first = parseDate(a.sentAt)?.getTime() ?? 0;\n const second = parseDate(b.sentAt)?.getTime() ?? 0;\n return first - second;\n });\n\nexport interface ThreadNode {\n message: ConversationMessage;\n replies: ConversationMessage[];\n}\n\n/**\n * Collapses a flat list into one level of nesting.\n *\n * Deeper chains are flattened onto the nearest **top-level** ancestor rather\n * than being dropped or nested further: indentation past one level is\n * unreadable on a phone, and silently discarding a reply-to-a-reply would lose\n * data the consumer stored. `replyingToName` on the rendered reply is what\n * preserves who was actually answered.\n *\n * A `parentId` pointing at a message that isn't present (paged out, deleted)\n * is treated as top-level, so nothing ever disappears from the list.\n */\nexport const buildThread = (messages: ConversationMessage[]): ThreadNode[] => {\n const byId = new Map(messages.map((message) => [message.id, message]));\n\n /** Walks up to the outermost ancestor, guarding against a parentId cycle. */\n const rootIdOf = (message: ConversationMessage): string => {\n const seen = new Set<string>([message.id]);\n let current = message;\n\n while (current.parentId) {\n const parent = byId.get(current.parentId);\n if (!parent || seen.has(parent.id)) break;\n seen.add(parent.id);\n current = parent;\n }\n\n return current.id;\n };\n\n const nodes = new Map<string, ThreadNode>();\n const roots: ThreadNode[] = [];\n\n for (const message of messages) {\n if (message.parentId && byId.has(message.parentId)) continue;\n const node: ThreadNode = { message, replies: [] };\n nodes.set(message.id, node);\n roots.push(node);\n }\n\n for (const message of messages) {\n if (!message.parentId || !byId.has(message.parentId)) continue;\n nodes.get(rootIdOf(message))?.replies.push(message);\n }\n\n for (const node of nodes.values()) {\n node.replies = sortBySentAt(node.replies);\n }\n\n return roots;\n};\n\n/** Name of the author a reply was addressed to, when it isn't its own parent. */\nexport const replyTargetName = (\n message: ConversationMessage,\n byId: Map<string, ConversationMessage>,\n rootId: string,\n): string | undefined => {\n if (!message.parentId || message.parentId === rootId) return undefined;\n return byId.get(message.parentId)?.author.name;\n};\n\n/** Trims a quoted excerpt so a long parent message can't dominate the reply. */\nexport const excerpt = (body: string, maxLength = 120): string => {\n const flat = body.replace(/\\s+/g, ' ').trim();\n return flat.length > maxLength ? `${flat.slice(0, maxLength - 1)}…` : flat;\n};\n\nexport const classNames = (...values: Array<string | false | null | undefined>): string =>\n values.filter(Boolean).join(' ');\n","import React from 'react';\nimport { AlertCircle, MoreHorizontal, Paperclip, RotateCw } from 'lucide-react';\nimport { Avatar } from '../Avatar';\nimport { Button } from '../Button';\nimport { Menu } from '../Menu';\nimport { Pill } from '../Pill';\nimport { Spinner } from '../Spinner';\nimport type { ConversationMessage, MessageAttachment } from './Conversation.types';\nimport type { MenuItemType } from '../Menu';\nimport { classNames, excerpt, formatFileSize, formatMessageTime } from './Conversation.utils';\n\nexport interface MessageItemProps {\n message: ConversationMessage;\n /** 'chat' right-aligns own messages into bubbles; 'comment' renders a flat card. */\n layout: 'chat' | 'comment';\n isOwn: boolean;\n /** Continues a run from the same author - hides the avatar and author line. */\n grouped: boolean;\n /** Plays the entrance animation. */\n isNew: boolean;\n actions?: MenuItemType[];\n footer?: React.ReactNode;\n body?: React.ReactNode;\n onRetry?: () => void;\n /** Author of the message being answered, for a flattened deep reply. */\n replyingToName?: string;\n /** Quoted parent, rendered above the body in chat layout. */\n quoted?: ConversationMessage;\n /** Reply affordance; omitted entirely when the viewer may not reply. */\n onReply?: () => void;\n formatTimestamp?: (iso: string) => string;\n}\n\nconst AttachmentList: React.FC<{ attachments: MessageAttachment[] }> = ({ attachments }) => (\n <ul className=\"eidos-message-attachments\">\n {attachments.map((attachment) => {\n const label = (\n <>\n <Paperclip aria-hidden=\"true\" />\n <span className=\"eidos-message-attachment-name\">{attachment.name}</span>\n {attachment.size !== undefined && (\n <span className=\"eidos-message-attachment-size\">{formatFileSize(attachment.size)}</span>\n )}\n </>\n );\n\n return (\n <li key={attachment.id} className=\"eidos-message-attachment\">\n {attachment.url ? (\n <a\n href={attachment.url}\n className=\"eidos-message-attachment-link\"\n target=\"_blank\"\n rel=\"noreferrer\"\n download={attachment.name}\n >\n {label}\n </a>\n ) : (\n <span className=\"eidos-message-attachment-link\">{label}</span>\n )}\n </li>\n );\n })}\n </ul>\n);\n\nexport const MessageItem: React.FC<MessageItemProps> = ({\n message,\n layout,\n isOwn,\n grouped,\n isNew,\n actions,\n footer,\n body,\n onRetry,\n replyingToName,\n quoted,\n onReply,\n formatTimestamp,\n}) => {\n const { author, status = 'sent' } = message;\n const timestamp = formatTimestamp\n ? formatTimestamp(message.sentAt)\n : formatMessageTime(message.sentAt);\n // An empty action list must not leave a dead trigger behind - see the\n // permissions note in Conversation.types.ts.\n const hasActions = Boolean(actions && actions.length > 0);\n\n // One placement per layout, rather than one per message.\n //\n // `chat` puts the time at the foot of the bubble, where a grouped message\n // already had it; it used to appear in the header for the first message of a\n // group and inside the bubble for the rest, so a single thread showed the\n // same fact in two places. `comment` keeps it in the header, where the row is\n // full width and has space for it.\n const timeInBubble = layout === 'chat';\n\n return (\n <li\n className={classNames(\n 'eidos-message',\n `eidos-message--${layout}`,\n isOwn && 'eidos-message--own',\n grouped && 'eidos-message--grouped',\n status !== 'sent' && `eidos-message--${status}`,\n isNew && 'eidos-message--enter',\n )}\n data-message-id={message.id}\n >\n <div className=\"eidos-message-avatar\">\n {!grouped && (\n <Avatar\n src={author.avatarSrc}\n name={author.name}\n size=\"sm\"\n color={author.avatarColor ?? 'gray'}\n />\n )}\n </div>\n\n <div className=\"eidos-message-main\">\n {!grouped && (\n <div className=\"eidos-message-header\">\n <span className=\"eidos-message-author\">{author.name}</span>\n {author.role && (\n <Pill size=\"sm\" variant=\"outlined\" color=\"secondary\">\n {author.role}\n </Pill>\n )}\n {!timeInBubble && (\n <time className=\"eidos-message-time\" dateTime={message.sentAt}>\n {timestamp}\n </time>\n )}\n {message.edited && <span className=\"eidos-message-edited\">edited</span>}\n </div>\n )}\n\n <div className=\"eidos-message-bubble\">\n {quoted && (\n <blockquote className=\"eidos-message-quote\">\n <span className=\"eidos-message-quote-author\">{quoted.author.name}</span>\n <span className=\"eidos-message-quote-body\">{excerpt(quoted.body)}</span>\n </blockquote>\n )}\n\n {replyingToName && (\n <span className=\"eidos-message-reply-target\">Replying to {replyingToName}</span>\n )}\n\n <div className=\"eidos-message-body\">{body ?? message.body}</div>\n\n {message.attachments && message.attachments.length > 0 && (\n <AttachmentList attachments={message.attachments} />\n )}\n\n {(timeInBubble || grouped) && (\n <time\n className=\"eidos-message-time eidos-message-time--inline\"\n dateTime={message.sentAt}\n >\n {timestamp}\n </time>\n )}\n </div>\n\n {status === 'sending' && (\n <span className=\"eidos-message-status\">\n <Spinner size=\"sm\" color=\"secondary\" />\n Sending…\n </span>\n )}\n\n {status === 'failed' && (\n <span className=\"eidos-message-status eidos-message-status--failed\">\n <AlertCircle aria-hidden=\"true\" />\n Not sent\n {onRetry && (\n <Button variant=\"text\" color=\"danger\" size=\"sm\" preIcon={RotateCw} onClick={onRetry}>\n Retry\n </Button>\n )}\n </span>\n )}\n\n {footer && <div className=\"eidos-message-footer\">{footer}</div>}\n\n {onReply && (\n <div className=\"eidos-message-reply\">\n <Button variant=\"text\" color=\"secondary\" size=\"sm\" onClick={onReply}>\n Reply\n </Button>\n </div>\n )}\n </div>\n\n {hasActions && (\n <div className=\"eidos-message-actions\">\n <Menu\n items={actions ?? []}\n trigger={\n <Button\n variant=\"text\"\n color=\"secondary\"\n size=\"sm\"\n icon={MoreHorizontal}\n aria-label={`Actions for ${author.name}'s message`}\n />\n }\n />\n </div>\n )}\n </li>\n );\n};\n\nMessageItem.displayName = 'MessageItem';\n","import React, { useCallback, useMemo, useState } from 'react';\nimport { MessageSquare } from 'lucide-react';\nimport { Button } from '../Button';\nimport { EmptyState } from '../EmptyState';\nimport { Skeleton } from '../Skeleton';\nimport { Spinner } from '../Spinner';\nimport { MessageComposer } from './MessageComposer.component';\nimport { MessageItem } from './MessageItem.component';\nimport type { CommentThreadProps, ConversationMessage, MessageDraft } from './Conversation.types';\nimport { useNewMessageIds } from './Conversation.hooks';\nimport {\n buildThread,\n classNames,\n continuesRun,\n replyTargetName,\n sortBySentAt,\n} from './Conversation.utils';\n\nconst CommentSkeleton: React.FC = () => (\n <div className=\"eidos-comment-thread-skeleton\" aria-hidden=\"true\">\n {[0, 1, 2].map((row) => (\n <div key={row} className=\"eidos-comment-thread-skeleton-row\">\n <Skeleton variant=\"circular\" width={28} height={28} />\n <Skeleton variant=\"text\" lines={2} />\n </div>\n ))}\n </div>\n);\n\n/**\n * A comment section: document flow, no internal scroll container, one level of\n * nesting.\n *\n * Shares its data model and message rendering with `Chat`, so history captured\n * in one can be displayed in the other. Unlike `Chat` it has no scroll region\n * of its own - it grows with the page and pages *downward* via `onLoadMore`.\n */\nexport const CommentThread: React.FC<CommentThreadProps> = ({\n messages,\n currentUserId,\n onSend,\n onReply,\n onRetry,\n readOnly = false,\n readOnlyMessage,\n renderBody,\n messageActions,\n renderMessageFooter,\n loading = false,\n emptyContent,\n hasMore = false,\n loadingMore = false,\n onLoadMore,\n groupConsecutive = true,\n groupWindowMinutes = 5,\n formatTimestamp,\n animateNewMessages = true,\n allowAttachments = false,\n attachmentAccept,\n order = 'oldest-first',\n composerPosition = 'bottom',\n placeholder = 'Add a comment...',\n sendOnEnter = false,\n title,\n ariaLabel = 'Comments',\n className = '',\n}) => {\n const [replyingToId, setReplyingToId] = useState<string | null>(null);\n const newIds = useNewMessageIds(messages, animateNewMessages);\n\n const byId = useMemo(() => new Map(messages.map((message) => [message.id, message])), [messages]);\n\n const roots = useMemo(() => {\n const thread = buildThread(messages);\n const sorted = sortBySentAt(thread.map((node) => node.message));\n const ordered = order === 'newest-first' ? [...sorted].reverse() : sorted;\n const nodeById = new Map(thread.map((node) => [node.message.id, node]));\n // Replies always read oldest-first, even when roots are reversed - a\n // conversation under a single comment is chronological by nature.\n return ordered.map((message) => nodeById.get(message.id)!);\n }, [messages, order]);\n\n const canReply = !readOnly && Boolean(onReply);\n const showComposer = !readOnly && Boolean(onSend);\n const isEmpty = !loading && messages.length === 0;\n const total = messages.length;\n\n const handleReply = useCallback(\n async (draft: MessageDraft) => {\n await onReply?.(draft);\n setReplyingToId(null);\n },\n [onReply],\n );\n\n const renderComment = (\n message: ConversationMessage,\n options: { rootId: string; previous?: ConversationMessage; isReply: boolean },\n ) => {\n const actions = readOnly ? undefined : messageActions?.(message);\n\n return (\n <MessageItem\n key={message.id}\n message={message}\n layout=\"comment\"\n isOwn={Boolean(currentUserId) && message.author.id === currentUserId}\n grouped={groupConsecutive && continuesRun(options.previous, message, groupWindowMinutes)}\n isNew={newIds.has(message.id)}\n actions={actions}\n footer={renderMessageFooter?.(message)}\n body={renderBody?.(message)}\n onRetry={!readOnly && onRetry ? () => onRetry(message) : undefined}\n replyingToName={\n options.isReply ? replyTargetName(message, byId, options.rootId) : undefined\n }\n onReply={canReply ? () => setReplyingToId(message.id) : undefined}\n formatTimestamp={formatTimestamp}\n />\n );\n };\n\n const composer = showComposer && (\n <MessageComposer\n onSubmit={onSend as NonNullable<typeof onSend>}\n placeholder={placeholder}\n submitLabel=\"Comment\"\n sendOnEnter={sendOnEnter}\n allowAttachments={allowAttachments}\n attachmentAccept={attachmentAccept}\n className=\"eidos-comment-thread-composer\"\n />\n );\n\n return (\n <section className={classNames('eidos-comment-thread', className)} aria-label={ariaLabel}>\n {(title || total > 0) && (\n <header className=\"eidos-comment-thread-header\">\n {title && <h3 className=\"eidos-comment-thread-title\">{title}</h3>}\n {total > 0 && (\n <span className=\"eidos-comment-thread-count\">\n {total} {total === 1 ? 'comment' : 'comments'}\n </span>\n )}\n </header>\n )}\n\n {composerPosition === 'top' && composer}\n\n {loading && <CommentSkeleton />}\n\n {isEmpty &&\n (emptyContent ?? (\n <EmptyState\n icon={<MessageSquare />}\n title=\"No comments yet\"\n description={showComposer ? 'Start the discussion by leaving a comment.' : undefined}\n size=\"sm\"\n />\n ))}\n\n {!loading && roots.length > 0 && (\n <ol className=\"eidos-conversation-list eidos-comment-thread-list\" aria-busy={loadingMore}>\n {roots.map((node) => (\n <li key={node.message.id} className=\"eidos-comment-thread-node\">\n <ol className=\"eidos-conversation-list\">\n {renderComment(node.message, { rootId: node.message.id, isReply: false })}\n </ol>\n\n {node.replies.length > 0 && (\n <ol className=\"eidos-conversation-list eidos-comment-thread-replies\">\n {node.replies.map((reply, index) =>\n renderComment(reply, {\n rootId: node.message.id,\n previous: node.replies[index - 1],\n isReply: true,\n }),\n )}\n </ol>\n )}\n\n {replyingToId &&\n (replyingToId === node.message.id ||\n node.replies.some((reply) => reply.id === replyingToId)) && (\n <div className=\"eidos-comment-thread-reply-composer\">\n <MessageComposer\n onSubmit={handleReply}\n onCancel={() => setReplyingToId(null)}\n parentId={replyingToId}\n replyingTo={byId.get(replyingToId)?.author.name}\n placeholder=\"Write a reply...\"\n submitLabel=\"Reply\"\n sendOnEnter={sendOnEnter}\n allowAttachments={allowAttachments}\n attachmentAccept={attachmentAccept}\n autoFocus\n />\n </div>\n )}\n </li>\n ))}\n </ol>\n )}\n\n {hasMore && (\n <div className=\"eidos-comment-thread-load-more\">\n {loadingMore ? (\n <span className=\"eidos-comment-thread-loading-more\">\n <Spinner size=\"sm\" color=\"secondary\" />\n Loading more comments…\n </span>\n ) : (\n <Button variant=\"outlined\" color=\"secondary\" size=\"sm\" onClick={onLoadMore}>\n Load more comments\n </Button>\n )}\n </div>\n )}\n\n {composerPosition === 'bottom' && composer}\n\n {readOnly && readOnlyMessage && (\n <p className=\"eidos-conversation-readonly\">{readOnlyMessage}</p>\n )}\n </section>\n );\n};\n\nCommentThread.displayName = 'CommentThread';\n"]}
1
+ {"version":3,"sources":["/home/runner/work/eidos-ui/eidos-ui/dist/chunk-U4MV7P4A.cjs","../src/components/Conversation/Chat.component.tsx","../src/components/Conversation/MessageComposer.component.tsx","../src/components/Conversation/Conversation.hooks.ts","../src/components/Conversation/Conversation.utils.ts","../src/components/Conversation/MessageItem.component.tsx","../src/components/Conversation/CommentThread.component.tsx"],"names":["useRef","useCallback","Fragment","Paperclip","jsx","jsxs"],"mappings":"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","file":"/home/runner/work/eidos-ui/eidos-ui/dist/chunk-U4MV7P4A.cjs","sourcesContent":[null,"import React, { useCallback, useMemo } from 'react';\nimport { ArrowDown, MessagesSquare } from 'lucide-react';\nimport { Button } from '../Button';\nimport { EmptyState } from '../EmptyState';\nimport { Skeleton } from '../Skeleton';\nimport { Spinner } from '../Spinner';\nimport { MessageComposer } from './MessageComposer.component';\nimport { MessageItem } from './MessageItem.component';\nimport type { ChatProps, ConversationMessage } from './Conversation.types';\nimport {\n useLoadMoreOnScrollTop,\n useNewMessageIds,\n useScrollAnchor,\n useStickToBottom,\n} from './Conversation.hooks';\nimport { classNames, continuesRun, formatDateSeparator, isSameDay } from './Conversation.utils';\n\nconst ChatSkeleton: React.FC = () => (\n <div className=\"eidos-chat-skeleton\" aria-hidden=\"true\">\n {[0, 1, 2, 3].map((row) => (\n <div\n key={row}\n className={classNames(\n 'eidos-chat-skeleton-row',\n row % 2 === 1 && 'eidos-chat-skeleton-row--own',\n )}\n >\n <Skeleton variant=\"circular\" width={28} height={28} />\n <Skeleton variant=\"rounded\" height={48} />\n </div>\n ))}\n </div>\n);\n\n/**\n * A bottom-anchored message thread with a composer.\n *\n * Controlled and transport-agnostic: it renders the `messages` it is given and\n * reports intent through `onSend` / `onLoadMore`. It never fetches, so the\n * same array can be persisted and replayed verbatim.\n */\nexport const Chat: React.FC<ChatProps> = ({\n messages,\n currentUserId,\n onSend,\n onRetry,\n readOnly = false,\n readOnlyMessage,\n renderBody,\n messageActions,\n renderMessageFooter,\n loading = false,\n emptyContent,\n hasMore = false,\n loadingMore = false,\n onLoadMore,\n groupConsecutive = true,\n groupWindowMinutes = 5,\n showDateSeparators = true,\n formatTimestamp,\n animateNewMessages = true,\n allowAttachments = false,\n attachmentAccept,\n typing,\n unreadFromId,\n placeholder = 'Write a message...',\n sendOnEnter = true,\n ariaLabel = 'Conversation',\n className = '',\n}) => {\n const { scrollRef, contentRef, atBottom, scrollToBottom, handleScroll } =\n useStickToBottom(messages);\n const newIds = useNewMessageIds(messages, animateNewMessages);\n useScrollAnchor(scrollRef, messages);\n\n const loadMoreOnScroll = useLoadMoreOnScrollTop(scrollRef, { hasMore, loadingMore, onLoadMore });\n\n const onScroll = useCallback(() => {\n handleScroll();\n loadMoreOnScroll();\n }, [handleScroll, loadMoreOnScroll]);\n\n const byId = useMemo(() => new Map(messages.map((message) => [message.id, message])), [messages]);\n\n // An id that isn't in the current page must not draw a divider above\n // nothing - it would float at the top of whatever happens to be loaded.\n const unreadIndex = useMemo(() => {\n if (!unreadFromId) return -1;\n return messages.findIndex((message) => message.id === unreadFromId);\n }, [messages, unreadFromId]);\n\n const showComposer = !readOnly && Boolean(onSend);\n const isEmpty = !loading && messages.length === 0;\n\n const renderMessage = (message: ConversationMessage, index: number) => {\n const previous = messages[index - 1];\n const actions = readOnly ? undefined : messageActions?.(message);\n const quoted = message.parentId ? byId.get(message.parentId) : undefined;\n\n const nodes: React.ReactNode[] = [];\n\n if (showDateSeparators && (!previous || !isSameDay(previous.sentAt, message.sentAt))) {\n nodes.push(\n <li key={`date-${message.id}`} className=\"eidos-conversation-separator\">\n <span>{formatDateSeparator(message.sentAt)}</span>\n </li>,\n );\n }\n\n if (index === unreadIndex) {\n nodes.push(\n <li\n key={`unread-${message.id}`}\n className=\"eidos-conversation-separator eidos-conversation-separator--unread\"\n >\n <span>Unread messages</span>\n </li>,\n );\n }\n\n // A separator breaks a run: the message under it needs its own header.\n const grouped =\n groupConsecutive &&\n index !== unreadIndex &&\n nodes.length === 0 &&\n continuesRun(previous, message, groupWindowMinutes);\n\n nodes.push(\n <MessageItem\n key={message.id}\n message={message}\n layout=\"chat\"\n isOwn={Boolean(currentUserId) && message.author.id === currentUserId}\n grouped={grouped}\n isNew={newIds.has(message.id)}\n actions={actions}\n footer={renderMessageFooter?.(message)}\n body={renderBody?.(message)}\n onRetry={!readOnly && onRetry ? () => onRetry(message) : undefined}\n quoted={quoted}\n formatTimestamp={formatTimestamp}\n />,\n );\n\n return nodes;\n };\n\n return (\n <div className={classNames('eidos-chat', className)}>\n <div\n ref={scrollRef}\n className=\"eidos-chat-scroll\"\n onScroll={onScroll}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions\"\n aria-busy={loading}\n aria-label={ariaLabel}\n tabIndex={0}\n >\n {/* One wrapper whose height is the content height, so\n `useStickToBottom` can observe growth that doesn't change the\n message count - a streamed reply, the typing indicator. */}\n <div ref={contentRef} className=\"eidos-chat-scroll-content\">\n {hasMore && (\n <div className=\"eidos-chat-load-more\">\n {loadingMore ? (\n <span className=\"eidos-chat-loading-more\">\n <Spinner size=\"sm\" color=\"secondary\" />\n Loading earlier messages…\n </span>\n ) : (\n <Button variant=\"text\" color=\"secondary\" size=\"sm\" onClick={onLoadMore}>\n Load earlier messages\n </Button>\n )}\n </div>\n )}\n\n {loading && <ChatSkeleton />}\n\n {isEmpty &&\n (emptyContent ?? (\n <EmptyState\n icon={<MessagesSquare />}\n title=\"No messages yet\"\n description={\n showComposer ? 'Send the first message to start the conversation.' : undefined\n }\n size=\"sm\"\n />\n ))}\n\n {!loading && messages.length > 0 && (\n <ol className=\"eidos-conversation-list\">{messages.map(renderMessage)}</ol>\n )}\n\n {typing && typing.length > 0 && (\n <div className=\"eidos-chat-typing\" aria-live=\"polite\">\n <span className=\"eidos-chat-typing-dots\" aria-hidden=\"true\">\n <i />\n <i />\n <i />\n </span>\n {typing.length === 1\n ? `${typing[0].name} is typing…`\n : `${typing.length} people are typing…`}\n </div>\n )}\n </div>\n </div>\n\n {!atBottom && messages.length > 0 && (\n <div className=\"eidos-chat-jump\">\n <Button\n size=\"sm\"\n variant=\"filled\"\n color=\"secondary\"\n preIcon={ArrowDown}\n onClick={() => scrollToBottom('smooth')}\n >\n Latest\n </Button>\n </div>\n )}\n\n {showComposer && (\n <div className=\"eidos-chat-composer\">\n <MessageComposer\n onSubmit={onSend as NonNullable<typeof onSend>}\n placeholder={placeholder}\n sendOnEnter={sendOnEnter}\n allowAttachments={allowAttachments}\n attachmentAccept={attachmentAccept}\n />\n </div>\n )}\n\n {readOnly && readOnlyMessage && (\n <p className=\"eidos-conversation-readonly\">{readOnlyMessage}</p>\n )}\n </div>\n );\n};\n\nChat.displayName = 'Chat';\n","import React, { useCallback, useId, useRef, useState } from 'react';\nimport { Paperclip, Send, X } from 'lucide-react';\nimport { Avatar } from '../Avatar';\nimport { Button } from '../Button';\nimport { Chip } from '../Chip';\nimport type { MessageComposerProps } from './Conversation.types';\nimport { useAutoGrow } from './Conversation.hooks';\nimport { classNames, formatFileSize } from './Conversation.utils';\n\n/**\n * The authoring half of a conversation - a growing text field, optional\n * attachments, and a submit control.\n *\n * Exported on its own so a consumer building a bespoke list can still reuse\n * the input, and used internally by both `Chat` and `CommentThread`.\n */\nexport const MessageComposer: React.FC<MessageComposerProps> = ({\n value,\n onChange,\n onSubmit,\n onCancel,\n placeholder = 'Write a message...',\n submitLabel = 'Send',\n sendOnEnter = false,\n parentId,\n replyingTo,\n allowAttachments = false,\n attachmentAccept,\n author,\n disabled = false,\n requireBody = true,\n autoFocus = false,\n maxLength,\n className = '',\n}) => {\n const [internalBody, setInternalBody] = useState('');\n const [attachments, setAttachments] = useState<File[]>([]);\n const [pending, setPending] = useState(false);\n\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const fileInputId = useId();\n\n const isControlled = value !== undefined;\n const body = isControlled ? value : internalBody;\n\n useAutoGrow(textareaRef, body);\n\n const setBody = useCallback(\n (next: string) => {\n if (!isControlled) setInternalBody(next);\n onChange?.(next);\n },\n [isControlled, onChange],\n );\n\n const canSubmit = !disabled && !pending && (!requireBody || body.trim().length > 0);\n\n const submit = useCallback(async () => {\n if (!canSubmit) return;\n\n const draft = { body: body.trim(), attachments, parentId };\n const result = onSubmit(draft);\n\n // A synchronous handler clears immediately; an async one keeps the draft\n // until it resolves, so a rejected send never loses what was typed.\n if (!(result instanceof Promise)) {\n setBody('');\n setAttachments([]);\n return;\n }\n\n setPending(true);\n try {\n await result;\n setBody('');\n setAttachments([]);\n } catch {\n // Intentionally swallowed: the draft stays put and the consumer owns\n // error reporting (a Snackbar, an Alert, ...). Rethrowing here would\n // surface as an unhandled rejection from a keystroke.\n } finally {\n setPending(false);\n }\n }, [canSubmit, body, attachments, parentId, onSubmit, setBody]);\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (!sendOnEnter) return;\n // Shift+Enter always inserts a newline; the modifier check also leaves\n // IME composition (which fires Enter to commit) alone.\n if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing) return;\n event.preventDefault();\n void submit();\n };\n\n const addFiles = (list: FileList | null) => {\n if (!list?.length) return;\n setAttachments((current) => [...current, ...Array.from(list)]);\n // Reset so selecting the same file twice in a row still fires `change`.\n if (fileInputRef.current) fileInputRef.current.value = '';\n };\n\n const removeAttachment = (index: number) =>\n setAttachments((current) => current.filter((_, i) => i !== index));\n\n return (\n <form\n className={classNames('eidos-message-composer', className)}\n onSubmit={(event) => {\n event.preventDefault();\n void submit();\n }}\n >\n {replyingTo && (\n <div className=\"eidos-composer-reply-hint\">\n <span>\n Replying to <strong>{replyingTo}</strong>\n </span>\n {onCancel && (\n <Button\n variant=\"text\"\n color=\"secondary\"\n size=\"sm\"\n icon={X}\n onClick={onCancel}\n tooltip=\"Cancel reply\"\n aria-label=\"Cancel reply\"\n />\n )}\n </div>\n )}\n\n <div className=\"eidos-composer-row\">\n {author && (\n <div className=\"eidos-composer-avatar\">\n <Avatar\n src={author.avatarSrc}\n name={author.name}\n size=\"sm\"\n color={author.avatarColor ?? 'gray'}\n />\n </div>\n )}\n\n <div className=\"eidos-composer-field\">\n <textarea\n ref={textareaRef}\n className=\"eidos-composer-input\"\n value={body}\n onChange={(event) => setBody(event.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n disabled={disabled || pending}\n rows={1}\n maxLength={maxLength}\n // Opt-in and off by default. Only the reply composer sets it, where\n // the field appears in direct response to the user pressing Reply -\n // moving focus there is the expected outcome, not a hijack.\n autoFocus={autoFocus}\n aria-label={placeholder}\n />\n\n {attachments.length > 0 && (\n <ul className=\"eidos-composer-attachments\">\n {attachments.map((file, index) => (\n <li key={`${file.name}-${file.size}-${index}`}>\n <Chip\n size=\"sm\"\n variant=\"outlined\"\n color=\"secondary\"\n preIcon={Paperclip}\n onRemove={() => removeAttachment(index)}\n >\n {file.name}\n {file.size > 0 && ` (${formatFileSize(file.size)})`}\n </Chip>\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n\n <div className=\"eidos-composer-actions\">\n <div className=\"eidos-composer-actions-start\">\n {allowAttachments && (\n <>\n <input\n ref={fileInputRef}\n id={fileInputId}\n type=\"file\"\n multiple\n accept={attachmentAccept}\n // Hidden from both the tab order and the accessibility\n // tree. It is a proxy: the visible button above opens the\n // file dialog via `.click()`, so this input is an\n // implementation detail, not a control.\n //\n // `visually-hidden` keeps an element *focusable* - that is\n // the point of it, for skip links and screen-reader-only\n // text - so a keyboard user landed on an invisible control\n // and met the same \"Attach files\" action twice. `FileUpload`\n // already gets this right with `display: none` +\n // `tabIndex={-1}`; these two had drifted from it.\n //\n // `aria-hidden` is safe here only *because* of the\n // `tabIndex={-1}` beside it - hiding a focusable element is\n // its own violation.\n className=\"eidos-composer-file-input\"\n tabIndex={-1}\n aria-hidden=\"true\"\n onChange={(event) => addFiles(event.target.files)}\n disabled={disabled || pending}\n />\n <Button\n variant=\"text\"\n color=\"secondary\"\n size=\"sm\"\n icon={Paperclip}\n tooltip=\"Attach files\"\n aria-label=\"Attach files\"\n disabled={disabled || pending}\n onClick={() => fileInputRef.current?.click()}\n />\n </>\n )}\n {sendOnEnter && (\n <span className=\"eidos-composer-hint\">\n <kbd>Enter</kbd> to send · <kbd>Shift</kbd>+<kbd>Enter</kbd> for a new line\n </span>\n )}\n </div>\n\n <div className=\"eidos-composer-actions-end\">\n {onCancel && !replyingTo && (\n <Button\n variant=\"text\"\n color=\"secondary\"\n size=\"sm\"\n onClick={onCancel}\n disabled={pending}\n >\n Cancel\n </Button>\n )}\n <Button\n type=\"submit\"\n size=\"sm\"\n color=\"primary\"\n preIcon={Send}\n loading={pending}\n disabled={!canSubmit}\n >\n {submitLabel}\n </Button>\n </div>\n </div>\n </form>\n );\n};\n\nMessageComposer.displayName = 'MessageComposer';\n","import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport type { ConversationMessage } from './Conversation.types';\n\n/**\n * Ids that arrived *after* the first render.\n *\n * Seeded from the first commit so an initial page of history is never\n * animated - otherwise opening a thread fades forty messages in at once,\n * which reads as a glitch rather than as feedback. Only genuinely new ids get\n * the entrance class, and each one only once.\n */\nexport const useNewMessageIds = (\n messages: ConversationMessage[],\n enabled: boolean,\n): Set<string> => {\n const seen = useRef<Set<string> | null>(null);\n const [fresh, setFresh] = useState<Set<string>>(() => new Set());\n\n useEffect(() => {\n // First commit: everything already on screen counts as pre-existing.\n if (seen.current === null) {\n seen.current = new Set(messages.map((message) => message.id));\n return;\n }\n\n if (!enabled) {\n for (const message of messages) seen.current.add(message.id);\n return;\n }\n\n const added = messages.filter((message) => !seen.current?.has(message.id));\n if (!added.length) return;\n\n for (const message of added) seen.current.add(message.id);\n setFresh(new Set(added.map((message) => message.id)));\n }, [messages, enabled]);\n\n return fresh;\n};\n\ninterface StickToBottom {\n scrollRef: React.RefObject<HTMLDivElement | null>;\n /**\n * The single element wrapping everything inside the scroll region. Its\n * height *is* the content height, so observing it catches every way the\n * content can grow.\n */\n contentRef: React.RefObject<HTMLDivElement | null>;\n /** The viewport is at (or near) the newest message. */\n atBottom: boolean;\n scrollToBottom: (behavior?: ScrollBehavior) => void;\n handleScroll: () => void;\n}\n\n/** Distance from the bottom still counted as \"reading the newest message\". */\nconst BOTTOM_THRESHOLD_PX = 48;\n\n/**\n * Keeps a bottom-anchored list pinned to the newest message - but only while\n * the reader is already there.\n *\n * Scrolling someone back down while they are reading scrollback is the single\n * most common defect in chat UIs, so arrival of a new message only forces a\n * scroll when `atBottom` was already true. Otherwise the caller surfaces a\n * jump-to-latest affordance and leaves the viewport alone.\n */\nexport const useStickToBottom = (messages: ConversationMessage[]): StickToBottom => {\n const scrollRef = useRef<HTMLDivElement>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n const [atBottom, setAtBottom] = useState(true);\n // Read during a layout effect, so it must not be state - it has to be\n // correct for the commit that is happening right now, not the next render.\n const atBottomRef = useRef(true);\n const lastCount = useRef(messages.length);\n const lastScrollTop = useRef(0);\n\n const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {\n const node = scrollRef.current;\n if (!node) return;\n node.scrollTo({ top: node.scrollHeight, behavior });\n }, []);\n\n // Stickiness is released only by the *reader* moving up, never by distance\n // alone.\n //\n // Measuring distance alone is wrong the moment content can grow while a\n // smooth scroll is in flight: the scroll events fired mid-animation report\n // the gap to a bottom that has since moved further away, so the reader - who\n // never touched anything - is declared \"not at the bottom\" and the thread\n // stops following a streamed reply partway through. A programmatic scroll\n // to the bottom only ever *increases* `scrollTop`, and content growth does\n // not change it at all, so a decrease is a reliable signal of intent: wheel,\n // keyboard, scrollbar drag and touch all produce one.\n const handleScroll = useCallback(() => {\n const node = scrollRef.current;\n if (!node) return;\n const distance = node.scrollHeight - node.scrollTop - node.clientHeight;\n const movedUp = node.scrollTop < lastScrollTop.current;\n lastScrollTop.current = node.scrollTop;\n\n let next = atBottomRef.current;\n if (distance <= BOTTOM_THRESHOLD_PX) next = true;\n else if (movedUp) next = false;\n\n atBottomRef.current = next;\n setAtBottom((current) => (current === next ? current : next));\n }, []);\n\n // Jump (not animate) to the bottom on first paint so a thread opens on its\n // newest message instead of visibly scrolling there.\n useLayoutEffect(() => {\n scrollToBottom('auto');\n // eslint-disable-next-line react-hooks/exhaustive-deps -- mount only\n }, []);\n\n useLayoutEffect(() => {\n if (messages.length > lastCount.current && atBottomRef.current) {\n scrollToBottom('smooth');\n }\n lastCount.current = messages.length;\n }, [messages.length, scrollToBottom]);\n\n // The effect above only reacts to the *number* of messages. A reply that is\n // streamed into an existing message - or a typing indicator appearing, or\n // an image loading - grows the content without changing that number, and\n // used to push the newest text below the fold while the reader was sitting\n // at the bottom waiting for it.\n //\n // Observing the content wrapper covers every one of those, because its\n // height is the content height. The scroll container itself cannot be\n // observed for this: its own box never changes size, only its\n // `scrollHeight` does.\n //\n // `'auto'`, not `'smooth'`: a stream grows the content once per token, and\n // a smooth scroll restarted that often never catches up with the text.\n // `atBottomRef` still holds the reader's position from *before* the growth,\n // which is exactly the question - \"were they following along?\".\n //\n // Growth that coincides with a new message is left to the smooth scroll the\n // layout effect above already started; jumping here as well would cancel\n // that animation on every single arrival.\n useEffect(() => {\n const content = contentRef.current;\n if (!content || typeof ResizeObserver === 'undefined') return;\n\n let lastHeight = content.getBoundingClientRect().height;\n let observedCount = lastCount.current;\n const observer = new ResizeObserver(() => {\n const height = content.getBoundingClientRect().height;\n const countChanged = observedCount !== lastCount.current;\n observedCount = lastCount.current;\n if (height > lastHeight && !countChanged && atBottomRef.current) scrollToBottom('auto');\n lastHeight = height;\n });\n observer.observe(content);\n return () => observer.disconnect();\n }, [scrollToBottom]);\n\n return { scrollRef, contentRef, atBottom, scrollToBottom, handleScroll };\n};\n\n/**\n * Preserves the reader's position when older messages are prepended.\n *\n * Without this, inserting a page above the viewport pushes the content the\n * user was reading down by the height of everything added, which reads as the\n * list jumping to a random place. Capturing `scrollHeight` before the DOM\n * mutation and restoring the delta in a *layout* effect corrects it before the\n * browser paints, so there is no visible shift.\n */\nexport const useScrollAnchor = (\n scrollRef: React.RefObject<HTMLDivElement | null>,\n messages: ConversationMessage[],\n): void => {\n const previous = useRef({ height: 0, firstId: '', count: 0 });\n\n useLayoutEffect(() => {\n const node = scrollRef.current;\n if (!node) return;\n\n const firstId = messages[0]?.id ?? '';\n const before = previous.current;\n // A prepend is the only case where the first id changes while the list\n // grows - an append leaves it untouched, and a replacement shrinks it.\n const prepended =\n before.firstId !== '' && before.firstId !== firstId && messages.length > before.count;\n\n if (prepended) {\n node.scrollTop += node.scrollHeight - before.height;\n }\n\n previous.current = { height: node.scrollHeight, firstId, count: messages.length };\n }, [messages, scrollRef]);\n};\n\n/**\n * Fires `onLoadMore` once the reader approaches the top of the history.\n *\n * Latched on the way in and released only after they scroll back out of the\n * trigger zone, so a single pause near the top can't queue a dozen fetches.\n */\nexport const useLoadMoreOnScrollTop = (\n scrollRef: React.RefObject<HTMLDivElement | null>,\n {\n hasMore,\n loadingMore,\n onLoadMore,\n }: {\n hasMore?: boolean;\n loadingMore?: boolean;\n onLoadMore?: () => void;\n },\n thresholdPx = 80,\n): (() => void) => {\n const armed = useRef(true);\n\n return useCallback(() => {\n const node = scrollRef.current;\n if (!node) return;\n\n if (node.scrollTop > thresholdPx) {\n armed.current = true;\n return;\n }\n\n if (!armed.current || !hasMore || loadingMore || !onLoadMore) return;\n armed.current = false;\n onLoadMore();\n }, [scrollRef, hasMore, loadingMore, onLoadMore, thresholdPx]);\n};\n\n/**\n * Grows a textarea to fit its content, up to `maxHeight`.\n *\n * Height is written to `style.height` directly rather than through a React\n * inline style: it is measured from the live DOM after every keystroke, so it\n * cannot be expressed as a class, and routing it through state would add a\n * render per character. Assigning the property imperatively also keeps it out\n * of the server-rendered `style` attribute that CSP's `style-src-attr` gates.\n */\nexport const useAutoGrow = (\n ref: React.RefObject<HTMLTextAreaElement | null>,\n value: string,\n maxHeight = 200,\n): void => {\n useLayoutEffect(() => {\n const node = ref.current;\n if (!node) return;\n\n node.style.height = 'auto';\n const next = Math.min(node.scrollHeight, maxHeight);\n node.style.height = `${next}px`;\n node.style.overflowY = node.scrollHeight > maxHeight ? 'auto' : 'hidden';\n }, [ref, value, maxHeight]);\n};\n","import type { ConversationMessage } from './Conversation.types';\n\n/**\n * Timestamp formatting goes through `Intl` rather than a date library on\n * purpose: `dayjs` is currently bundled only into the `DatePicker` chunk, and\n * pulling it in here would add it to every consumer importing a conversation.\n * `Intl` is built in and already locale-aware.\n */\n\nconst timeFormat = new Intl.DateTimeFormat(undefined, {\n hour: '2-digit',\n minute: '2-digit',\n});\n\nconst dateFormat = new Intl.DateTimeFormat(undefined, {\n day: 'numeric',\n month: 'long',\n year: 'numeric',\n});\n\nconst dateFormatSameYear = new Intl.DateTimeFormat(undefined, {\n day: 'numeric',\n month: 'long',\n});\n\n/** Parses an ISO string, returning null rather than an `Invalid Date`. */\nexport const parseDate = (iso: string): Date | null => {\n const date = new Date(iso);\n return Number.isNaN(date.getTime()) ? null : date;\n};\n\nexport const formatMessageTime = (iso: string): string => {\n const date = parseDate(iso);\n return date ? timeFormat.format(date) : iso;\n};\n\nconst startOfDay = (date: Date): number =>\n new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();\n\nexport const isSameDay = (a: string, b: string): boolean => {\n const first = parseDate(a);\n const second = parseDate(b);\n if (!first || !second) return false;\n return startOfDay(first) === startOfDay(second);\n};\n\n/** `Today` / `Yesterday` / a full date, for the divider between days. */\nexport const formatDateSeparator = (iso: string): string => {\n const date = parseDate(iso);\n if (!date) return iso;\n\n const now = new Date();\n const dayDelta = Math.round((startOfDay(now) - startOfDay(date)) / 86_400_000);\n\n if (dayDelta === 0) return 'Today';\n if (dayDelta === 1) return 'Yesterday';\n return date.getFullYear() === now.getFullYear()\n ? dateFormatSameYear.format(date)\n : dateFormat.format(date);\n};\n\nconst FILE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];\n\nexport const formatFileSize = (bytes: number): string => {\n if (!Number.isFinite(bytes) || bytes < 0) return '';\n if (bytes < 1024) return `${bytes} B`;\n\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < FILE_UNITS.length - 1) {\n value /= 1024;\n unit += 1;\n }\n // One decimal below 10 (9.4 MB), none above it (24 MB) - the extra digit\n // stops carrying information once the integer part is two digits.\n return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${FILE_UNITS[unit]}`;\n};\n\n/**\n * Whether `message` continues a run started by `previous` - same author, same\n * day, and within the grouping window. A grouped message hides its avatar and\n * author line.\n */\nexport const continuesRun = (\n previous: ConversationMessage | undefined,\n message: ConversationMessage,\n windowMinutes: number,\n): boolean => {\n if (!previous) return false;\n if (previous.author.id !== message.author.id) return false;\n if (!isSameDay(previous.sentAt, message.sentAt)) return false;\n\n const before = parseDate(previous.sentAt);\n const after = parseDate(message.sentAt);\n if (!before || !after) return false;\n\n return Math.abs(after.getTime() - before.getTime()) <= windowMinutes * 60_000;\n};\n\nexport const sortBySentAt = (messages: ConversationMessage[]): ConversationMessage[] =>\n [...messages].sort((a, b) => {\n const first = parseDate(a.sentAt)?.getTime() ?? 0;\n const second = parseDate(b.sentAt)?.getTime() ?? 0;\n return first - second;\n });\n\nexport interface ThreadNode {\n message: ConversationMessage;\n replies: ConversationMessage[];\n}\n\n/**\n * Collapses a flat list into one level of nesting.\n *\n * Deeper chains are flattened onto the nearest **top-level** ancestor rather\n * than being dropped or nested further: indentation past one level is\n * unreadable on a phone, and silently discarding a reply-to-a-reply would lose\n * data the consumer stored. `replyingToName` on the rendered reply is what\n * preserves who was actually answered.\n *\n * A `parentId` pointing at a message that isn't present (paged out, deleted)\n * is treated as top-level, so nothing ever disappears from the list.\n */\nexport const buildThread = (messages: ConversationMessage[]): ThreadNode[] => {\n const byId = new Map(messages.map((message) => [message.id, message]));\n\n /** Walks up to the outermost ancestor, guarding against a parentId cycle. */\n const rootIdOf = (message: ConversationMessage): string => {\n const seen = new Set<string>([message.id]);\n let current = message;\n\n while (current.parentId) {\n const parent = byId.get(current.parentId);\n if (!parent || seen.has(parent.id)) break;\n seen.add(parent.id);\n current = parent;\n }\n\n return current.id;\n };\n\n const nodes = new Map<string, ThreadNode>();\n const roots: ThreadNode[] = [];\n\n for (const message of messages) {\n if (message.parentId && byId.has(message.parentId)) continue;\n const node: ThreadNode = { message, replies: [] };\n nodes.set(message.id, node);\n roots.push(node);\n }\n\n for (const message of messages) {\n if (!message.parentId || !byId.has(message.parentId)) continue;\n nodes.get(rootIdOf(message))?.replies.push(message);\n }\n\n for (const node of nodes.values()) {\n node.replies = sortBySentAt(node.replies);\n }\n\n return roots;\n};\n\n/** Name of the author a reply was addressed to, when it isn't its own parent. */\nexport const replyTargetName = (\n message: ConversationMessage,\n byId: Map<string, ConversationMessage>,\n rootId: string,\n): string | undefined => {\n if (!message.parentId || message.parentId === rootId) return undefined;\n return byId.get(message.parentId)?.author.name;\n};\n\n/** Trims a quoted excerpt so a long parent message can't dominate the reply. */\nexport const excerpt = (body: string, maxLength = 120): string => {\n const flat = body.replace(/\\s+/g, ' ').trim();\n return flat.length > maxLength ? `${flat.slice(0, maxLength - 1)}…` : flat;\n};\n\nexport const classNames = (...values: Array<string | false | null | undefined>): string =>\n values.filter(Boolean).join(' ');\n","import React from 'react';\nimport { AlertCircle, MoreHorizontal, Paperclip, RotateCw } from 'lucide-react';\nimport { Avatar } from '../Avatar';\nimport { Button } from '../Button';\nimport { Menu } from '../Menu';\nimport { Pill } from '../Pill';\nimport { Spinner } from '../Spinner';\nimport type { ConversationMessage, MessageAttachment } from './Conversation.types';\nimport type { MenuItemType } from '../Menu';\nimport { classNames, excerpt, formatFileSize, formatMessageTime } from './Conversation.utils';\n\nexport interface MessageItemProps {\n message: ConversationMessage;\n /** 'chat' right-aligns own messages into bubbles; 'comment' renders a flat card. */\n layout: 'chat' | 'comment';\n isOwn: boolean;\n /** Continues a run from the same author - hides the avatar and author line. */\n grouped: boolean;\n /** Plays the entrance animation. */\n isNew: boolean;\n actions?: MenuItemType[];\n footer?: React.ReactNode;\n body?: React.ReactNode;\n onRetry?: () => void;\n /** Author of the message being answered, for a flattened deep reply. */\n replyingToName?: string;\n /** Quoted parent, rendered above the body in chat layout. */\n quoted?: ConversationMessage;\n /** Reply affordance; omitted entirely when the viewer may not reply. */\n onReply?: () => void;\n formatTimestamp?: (iso: string) => string;\n}\n\nconst AttachmentList: React.FC<{ attachments: MessageAttachment[] }> = ({ attachments }) => (\n <ul className=\"eidos-message-attachments\">\n {attachments.map((attachment) => {\n const label = (\n <>\n <Paperclip aria-hidden=\"true\" />\n <span className=\"eidos-message-attachment-name\">{attachment.name}</span>\n {attachment.size !== undefined && (\n <span className=\"eidos-message-attachment-size\">{formatFileSize(attachment.size)}</span>\n )}\n </>\n );\n\n return (\n <li key={attachment.id} className=\"eidos-message-attachment\">\n {attachment.url ? (\n <a\n href={attachment.url}\n className=\"eidos-message-attachment-link\"\n target=\"_blank\"\n rel=\"noreferrer\"\n download={attachment.name}\n >\n {label}\n </a>\n ) : (\n <span className=\"eidos-message-attachment-link\">{label}</span>\n )}\n </li>\n );\n })}\n </ul>\n);\n\nexport const MessageItem: React.FC<MessageItemProps> = ({\n message,\n layout,\n isOwn,\n grouped,\n isNew,\n actions,\n footer,\n body,\n onRetry,\n replyingToName,\n quoted,\n onReply,\n formatTimestamp,\n}) => {\n const { author, status = 'sent' } = message;\n const timestamp = formatTimestamp\n ? formatTimestamp(message.sentAt)\n : formatMessageTime(message.sentAt);\n // An empty action list must not leave a dead trigger behind - see the\n // permissions note in Conversation.types.ts.\n const hasActions = Boolean(actions && actions.length > 0);\n\n // One placement per layout, rather than one per message.\n //\n // `chat` puts the time at the foot of the bubble, where a grouped message\n // already had it; it used to appear in the header for the first message of a\n // group and inside the bubble for the rest, so a single thread showed the\n // same fact in two places. `comment` keeps it in the header, where the row is\n // full width and has space for it.\n const timeInBubble = layout === 'chat';\n\n return (\n <li\n className={classNames(\n 'eidos-message',\n `eidos-message--${layout}`,\n isOwn && 'eidos-message--own',\n grouped && 'eidos-message--grouped',\n status !== 'sent' && `eidos-message--${status}`,\n isNew && 'eidos-message--enter',\n )}\n data-message-id={message.id}\n >\n <div className=\"eidos-message-avatar\">\n {!grouped && (\n <Avatar\n src={author.avatarSrc}\n name={author.name}\n size=\"sm\"\n color={author.avatarColor ?? 'gray'}\n />\n )}\n </div>\n\n <div className=\"eidos-message-main\">\n {!grouped && (\n <div className=\"eidos-message-header\">\n <span className=\"eidos-message-author\">{author.name}</span>\n {author.role && (\n <Pill size=\"sm\" variant=\"outlined\" color=\"secondary\">\n {author.role}\n </Pill>\n )}\n {!timeInBubble && (\n <time className=\"eidos-message-time\" dateTime={message.sentAt}>\n {timestamp}\n </time>\n )}\n {message.edited && <span className=\"eidos-message-edited\">edited</span>}\n </div>\n )}\n\n <div className=\"eidos-message-bubble\">\n {quoted && (\n <blockquote className=\"eidos-message-quote\">\n <span className=\"eidos-message-quote-author\">{quoted.author.name}</span>\n <span className=\"eidos-message-quote-body\">{excerpt(quoted.body)}</span>\n </blockquote>\n )}\n\n {replyingToName && (\n <span className=\"eidos-message-reply-target\">Replying to {replyingToName}</span>\n )}\n\n <div className=\"eidos-message-body\">{body ?? message.body}</div>\n\n {message.attachments && message.attachments.length > 0 && (\n <AttachmentList attachments={message.attachments} />\n )}\n\n {(timeInBubble || grouped) && (\n <time\n className=\"eidos-message-time eidos-message-time--inline\"\n dateTime={message.sentAt}\n >\n {timestamp}\n </time>\n )}\n </div>\n\n {status === 'sending' && (\n <span className=\"eidos-message-status\">\n <Spinner size=\"sm\" color=\"secondary\" />\n Sending…\n </span>\n )}\n\n {status === 'failed' && (\n <span className=\"eidos-message-status eidos-message-status--failed\">\n <AlertCircle aria-hidden=\"true\" />\n Not sent\n {onRetry && (\n <Button variant=\"text\" color=\"danger\" size=\"sm\" preIcon={RotateCw} onClick={onRetry}>\n Retry\n </Button>\n )}\n </span>\n )}\n\n {footer && <div className=\"eidos-message-footer\">{footer}</div>}\n\n {onReply && (\n <div className=\"eidos-message-reply\">\n <Button variant=\"text\" color=\"secondary\" size=\"sm\" onClick={onReply}>\n Reply\n </Button>\n </div>\n )}\n </div>\n\n {hasActions && (\n <div className=\"eidos-message-actions\">\n <Menu\n items={actions ?? []}\n trigger={\n <Button\n variant=\"text\"\n color=\"secondary\"\n size=\"sm\"\n icon={MoreHorizontal}\n aria-label={`Actions for ${author.name}'s message`}\n />\n }\n />\n </div>\n )}\n </li>\n );\n};\n\nMessageItem.displayName = 'MessageItem';\n","import React, { useCallback, useMemo, useState } from 'react';\nimport { MessageSquare } from 'lucide-react';\nimport { Button } from '../Button';\nimport { EmptyState } from '../EmptyState';\nimport { Skeleton } from '../Skeleton';\nimport { Spinner } from '../Spinner';\nimport { MessageComposer } from './MessageComposer.component';\nimport { MessageItem } from './MessageItem.component';\nimport type { CommentThreadProps, ConversationMessage, MessageDraft } from './Conversation.types';\nimport { useNewMessageIds } from './Conversation.hooks';\nimport {\n buildThread,\n classNames,\n continuesRun,\n replyTargetName,\n sortBySentAt,\n} from './Conversation.utils';\n\nconst CommentSkeleton: React.FC = () => (\n <div className=\"eidos-comment-thread-skeleton\" aria-hidden=\"true\">\n {[0, 1, 2].map((row) => (\n <div key={row} className=\"eidos-comment-thread-skeleton-row\">\n <Skeleton variant=\"circular\" width={28} height={28} />\n <Skeleton variant=\"text\" lines={2} />\n </div>\n ))}\n </div>\n);\n\n/**\n * A comment section: document flow, no internal scroll container, one level of\n * nesting.\n *\n * Shares its data model and message rendering with `Chat`, so history captured\n * in one can be displayed in the other. Unlike `Chat` it has no scroll region\n * of its own - it grows with the page and pages *downward* via `onLoadMore`.\n */\nexport const CommentThread: React.FC<CommentThreadProps> = ({\n messages,\n currentUserId,\n onSend,\n onReply,\n onRetry,\n readOnly = false,\n readOnlyMessage,\n renderBody,\n messageActions,\n renderMessageFooter,\n loading = false,\n emptyContent,\n hasMore = false,\n loadingMore = false,\n onLoadMore,\n groupConsecutive = true,\n groupWindowMinutes = 5,\n formatTimestamp,\n animateNewMessages = true,\n allowAttachments = false,\n attachmentAccept,\n order = 'oldest-first',\n composerPosition = 'bottom',\n placeholder = 'Add a comment...',\n sendOnEnter = false,\n title,\n ariaLabel = 'Comments',\n className = '',\n}) => {\n const [replyingToId, setReplyingToId] = useState<string | null>(null);\n const newIds = useNewMessageIds(messages, animateNewMessages);\n\n const byId = useMemo(() => new Map(messages.map((message) => [message.id, message])), [messages]);\n\n const roots = useMemo(() => {\n const thread = buildThread(messages);\n const sorted = sortBySentAt(thread.map((node) => node.message));\n const ordered = order === 'newest-first' ? [...sorted].reverse() : sorted;\n const nodeById = new Map(thread.map((node) => [node.message.id, node]));\n // Replies always read oldest-first, even when roots are reversed - a\n // conversation under a single comment is chronological by nature.\n return ordered.map((message) => nodeById.get(message.id)!);\n }, [messages, order]);\n\n const canReply = !readOnly && Boolean(onReply);\n const showComposer = !readOnly && Boolean(onSend);\n const isEmpty = !loading && messages.length === 0;\n const total = messages.length;\n\n const handleReply = useCallback(\n async (draft: MessageDraft) => {\n await onReply?.(draft);\n setReplyingToId(null);\n },\n [onReply],\n );\n\n const renderComment = (\n message: ConversationMessage,\n options: { rootId: string; previous?: ConversationMessage; isReply: boolean },\n ) => {\n const actions = readOnly ? undefined : messageActions?.(message);\n\n return (\n <MessageItem\n key={message.id}\n message={message}\n layout=\"comment\"\n isOwn={Boolean(currentUserId) && message.author.id === currentUserId}\n grouped={groupConsecutive && continuesRun(options.previous, message, groupWindowMinutes)}\n isNew={newIds.has(message.id)}\n actions={actions}\n footer={renderMessageFooter?.(message)}\n body={renderBody?.(message)}\n onRetry={!readOnly && onRetry ? () => onRetry(message) : undefined}\n replyingToName={\n options.isReply ? replyTargetName(message, byId, options.rootId) : undefined\n }\n onReply={canReply ? () => setReplyingToId(message.id) : undefined}\n formatTimestamp={formatTimestamp}\n />\n );\n };\n\n const composer = showComposer && (\n <MessageComposer\n onSubmit={onSend as NonNullable<typeof onSend>}\n placeholder={placeholder}\n submitLabel=\"Comment\"\n sendOnEnter={sendOnEnter}\n allowAttachments={allowAttachments}\n attachmentAccept={attachmentAccept}\n className=\"eidos-comment-thread-composer\"\n />\n );\n\n return (\n <section className={classNames('eidos-comment-thread', className)} aria-label={ariaLabel}>\n {(title || total > 0) && (\n <header className=\"eidos-comment-thread-header\">\n {title && <h3 className=\"eidos-comment-thread-title\">{title}</h3>}\n {total > 0 && (\n <span className=\"eidos-comment-thread-count\">\n {total} {total === 1 ? 'comment' : 'comments'}\n </span>\n )}\n </header>\n )}\n\n {composerPosition === 'top' && composer}\n\n {loading && <CommentSkeleton />}\n\n {isEmpty &&\n (emptyContent ?? (\n <EmptyState\n icon={<MessageSquare />}\n title=\"No comments yet\"\n description={showComposer ? 'Start the discussion by leaving a comment.' : undefined}\n size=\"sm\"\n />\n ))}\n\n {!loading && roots.length > 0 && (\n <ol className=\"eidos-conversation-list eidos-comment-thread-list\" aria-busy={loadingMore}>\n {roots.map((node) => (\n <li key={node.message.id} className=\"eidos-comment-thread-node\">\n <ol className=\"eidos-conversation-list\">\n {renderComment(node.message, { rootId: node.message.id, isReply: false })}\n </ol>\n\n {node.replies.length > 0 && (\n <ol className=\"eidos-conversation-list eidos-comment-thread-replies\">\n {node.replies.map((reply, index) =>\n renderComment(reply, {\n rootId: node.message.id,\n previous: node.replies[index - 1],\n isReply: true,\n }),\n )}\n </ol>\n )}\n\n {replyingToId &&\n (replyingToId === node.message.id ||\n node.replies.some((reply) => reply.id === replyingToId)) && (\n <div className=\"eidos-comment-thread-reply-composer\">\n <MessageComposer\n onSubmit={handleReply}\n onCancel={() => setReplyingToId(null)}\n parentId={replyingToId}\n replyingTo={byId.get(replyingToId)?.author.name}\n placeholder=\"Write a reply...\"\n submitLabel=\"Reply\"\n sendOnEnter={sendOnEnter}\n allowAttachments={allowAttachments}\n attachmentAccept={attachmentAccept}\n autoFocus\n />\n </div>\n )}\n </li>\n ))}\n </ol>\n )}\n\n {hasMore && (\n <div className=\"eidos-comment-thread-load-more\">\n {loadingMore ? (\n <span className=\"eidos-comment-thread-loading-more\">\n <Spinner size=\"sm\" color=\"secondary\" />\n Loading more comments…\n </span>\n ) : (\n <Button variant=\"outlined\" color=\"secondary\" size=\"sm\" onClick={onLoadMore}>\n Load more comments\n </Button>\n )}\n </div>\n )}\n\n {composerPosition === 'bottom' && composer}\n\n {readOnly && readOnlyMessage && (\n <p className=\"eidos-conversation-readonly\">{readOnlyMessage}</p>\n )}\n </section>\n );\n};\n\nCommentThread.displayName = 'CommentThread';\n"]}
@@ -54,7 +54,7 @@ var Chip = ({
54
54
  const hasBothControls = isClickable && isRemovable;
55
55
  const body = /* @__PURE__ */ jsxs(Fragment, { children: [
56
56
  preIcon && renderIcon(preIcon, "eidos-chip--pre-icon"),
57
- /* @__PURE__ */ jsx("span", { className: "eidos-chip--copy", children }),
57
+ children && /* @__PURE__ */ jsx("span", { className: "eidos-chip--copy", children }),
58
58
  posIcon && renderIcon(posIcon, "eidos-chip--pos-icon")
59
59
  ] });
60
60
  const renderLink = (linkHref, className2, attributes) => /* @__PURE__ */ jsx(
@@ -113,4 +113,4 @@ var Chip = ({
113
113
  export {
114
114
  Chip
115
115
  };
116
- //# sourceMappingURL=chunk-IBT535UH.js.map
116
+ //# sourceMappingURL=chunk-UAAQVIZQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/Chip/Chip.component.tsx"],"sourcesContent":["import React from 'react';\nimport { X } from 'lucide-react';\nimport type { ChipProps } from './Chip.types';\nimport { Tooltip } from '../Tooltip/Tooltip.component';\nimport { renderIcon } from '../../utils';\nimport { EidosLink } from '../LinkProvider/Link.component';\n\nexport const Chip: React.FC<ChipProps> = ({\n variant = 'filled',\n color = 'primary',\n size = 'md',\n disabled = false,\n fullWidth = false,\n tooltip,\n className = '',\n preIcon,\n posIcon,\n children,\n onClick,\n onRemove,\n href,\n target,\n rel,\n ...chipProps\n}) => {\n const isClickable = !!onClick || href !== undefined;\n const isRemovable = !!onRemove;\n\n // Build CSS classes\n const chipClasses = [\n 'eidos-chip',\n `eidos-chip--${variant}`,\n `eidos-chip--${color}`,\n `eidos-chip--${size}`,\n disabled && 'eidos-chip--disabled',\n isClickable && 'eidos-chip--clickable',\n isRemovable && 'eidos-chip--removable',\n fullWidth && 'eidos-chip--full-width',\n className,\n ]\n .filter(Boolean)\n .join(' ');\n\n const handleClick = () => {\n if (disabled || !onClick) return;\n onClick();\n };\n\n const handleRemove = (e: React.MouseEvent) => {\n e.stopPropagation(); // Prevent chip click when removing\n if (disabled || !onRemove) return;\n onRemove();\n };\n\n // A chip that is both clickable and removable holds *two* controls, so it\n // has to render them as siblings.\n //\n // The previous shape switched the outer element to a `<div>` when\n // removable - the comment said \"to avoid nested buttons\" - and then gave\n // that div `role=\"button\"` and `tabIndex={0}`. That is the same defect\n // wearing a different hat: a button containing a button, which is invalid\n // (`nested-interactive`) and leaves a screen reader announcing a control\n // inside a control. It is the same shape already fixed on `Dropdown` and\n // `DataGrid`'s rows.\n //\n // Now the outer element is a plain container whenever there is a remove\n // button, and the chip's own action moves to an inner `<button>` wrapping\n // just the content. Each control is separately reachable and separately\n // named.\n const hasBothControls = isClickable && isRemovable;\n\n const body = (\n <>\n {preIcon && renderIcon(preIcon, 'eidos-chip--pre-icon')}\n\n {children && <span className=\"eidos-chip--copy\">{children}</span>}\n\n {posIcon && renderIcon(posIcon, 'eidos-chip--pos-icon')}\n </>\n );\n\n // A link chip follows the same two-control rule: alone, the chip *is* the\n // link; with a remove button, the link moves to the inner action slot so\n // the two stay siblings.\n const renderLink = (\n linkHref: string,\n className: string,\n attributes?: React.AnchorHTMLAttributes<HTMLAnchorElement>,\n ) => (\n <EidosLink\n {...attributes}\n href={linkHref}\n target={target}\n rel={rel}\n className={className}\n disabled={disabled}\n onClick={onClick ? handleClick : undefined}\n >\n {body}\n </EidosLink>\n );\n\n if (href !== undefined && !isRemovable) {\n const link = renderLink(href, chipClasses, chipProps);\n return tooltip ? <Tooltip message={tooltip}>{link}</Tooltip> : link;\n }\n\n const ChipElement = isClickable && !isRemovable ? 'button' : 'div';\n\n const chipContent = (\n <ChipElement\n className={chipClasses}\n disabled={ChipElement === 'button' ? disabled : undefined}\n onClick={isClickable && !hasBothControls ? handleClick : undefined}\n {...chipProps}\n >\n {hasBothControls ? (\n href !== undefined ? (\n renderLink(href, 'eidos-chip--action')\n ) : (\n <button\n type=\"button\"\n className=\"eidos-chip--action\"\n onClick={handleClick}\n disabled={disabled}\n >\n {body}\n </button>\n )\n ) : (\n body\n )}\n\n {isRemovable && (\n <button\n className=\"eidos-chip--remove-button\"\n onClick={handleRemove}\n disabled={disabled}\n aria-label=\"Remove\"\n type=\"button\"\n >\n <X className=\"eidos-chip--remove-icon\" />\n </button>\n )}\n </ChipElement>\n );\n\n return tooltip ? <Tooltip message={tooltip}>{chipContent}</Tooltip> : chipContent;\n};\n"],"mappings":";;;;;;;;;;;AACA,SAAS,SAAS;AAuEd,mBAGe,KAHf;AAjEG,IAAM,OAA4B,CAAC;AAAA,EACxC,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,WAAW;AAAA,EACX,YAAY;AAAA,EACZ;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAAM;AACJ,QAAM,cAAc,CAAC,CAAC,WAAW,SAAS;AAC1C,QAAM,cAAc,CAAC,CAAC;AAGtB,QAAM,cAAc;AAAA,IAClB;AAAA,IACA,eAAe,OAAO;AAAA,IACtB,eAAe,KAAK;AAAA,IACpB,eAAe,IAAI;AAAA,IACnB,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,eAAe;AAAA,IACf,aAAa;AAAA,IACb;AAAA,EACF,EACG,OAAO,OAAO,EACd,KAAK,GAAG;AAEX,QAAM,cAAc,MAAM;AACxB,QAAI,YAAY,CAAC,QAAS;AAC1B,YAAQ;AAAA,EACV;AAEA,QAAM,eAAe,CAAC,MAAwB;AAC5C,MAAE,gBAAgB;AAClB,QAAI,YAAY,CAAC,SAAU;AAC3B,aAAS;AAAA,EACX;AAiBA,QAAM,kBAAkB,eAAe;AAEvC,QAAM,OACJ,iCACG;AAAA,eAAW,WAAW,SAAS,sBAAsB;AAAA,IAErD,YAAY,oBAAC,UAAK,WAAU,oBAAoB,UAAS;AAAA,IAEzD,WAAW,WAAW,SAAS,sBAAsB;AAAA,KACxD;AAMF,QAAM,aAAa,CACjB,UACAA,YACA,eAEA;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA,WAAWA;AAAA,MACX;AAAA,MACA,SAAS,UAAU,cAAc;AAAA,MAEhC;AAAA;AAAA,EACH;AAGF,MAAI,SAAS,UAAa,CAAC,aAAa;AACtC,UAAM,OAAO,WAAW,MAAM,aAAa,SAAS;AACpD,WAAO,UAAU,oBAAC,WAAQ,SAAS,SAAU,gBAAK,IAAa;AAAA,EACjE;AAEA,QAAM,cAAc,eAAe,CAAC,cAAc,WAAW;AAE7D,QAAM,cACJ;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,MACX,UAAU,gBAAgB,WAAW,WAAW;AAAA,MAChD,SAAS,eAAe,CAAC,kBAAkB,cAAc;AAAA,MACxD,GAAG;AAAA,MAEH;AAAA,0BACC,SAAS,SACP,WAAW,MAAM,oBAAoB,IAErC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAU;AAAA,YACV,SAAS;AAAA,YACT;AAAA,YAEC;AAAA;AAAA,QACH,IAGF;AAAA,QAGD,eACC;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,SAAS;AAAA,YACT;AAAA,YACA,cAAW;AAAA,YACX,MAAK;AAAA,YAEL,8BAAC,KAAE,WAAU,2BAA0B;AAAA;AAAA,QACzC;AAAA;AAAA;AAAA,EAEJ;AAGF,SAAO,UAAU,oBAAC,WAAQ,SAAS,SAAU,uBAAY,IAAa;AACxE;","names":["className"]}
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
- var _chunkMKQVDEHIcjs = require('./chunk-MKQVDEHI.cjs');
3
+ var _chunkCPRNLW4Mcjs = require('./chunk-CPRNLW4M.cjs');
4
4
 
5
5
 
6
6
  var _chunkKVIL4EP4cjs = require('./chunk-KVIL4EP4.cjs');
@@ -151,7 +151,7 @@ var MenuPanel = ({
151
151
  }
152
152
  );
153
153
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "li", { role: "none", className: `eidos-menu-nested-item`, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
154
- _chunkMKQVDEHIcjs.Dropdown,
154
+ _chunkCPRNLW4Mcjs.Dropdown,
155
155
  {
156
156
  open: openSubmenuId === item.id,
157
157
  onOpenChange: (next) => setOpenSubmenuId(next ? item.id : null),
@@ -201,7 +201,7 @@ var Menu = ({
201
201
  { "aria-haspopup": "menu", "aria-expanded": isOpen }
202
202
  ) : trigger;
203
203
  const dropdown = /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
204
- _chunkMKQVDEHIcjs.Dropdown,
204
+ _chunkCPRNLW4Mcjs.Dropdown,
205
205
  {
206
206
  open: isOpen,
207
207
  onOpenChange: setIsOpen,
@@ -222,4 +222,4 @@ var Menu = ({
222
222
 
223
223
 
224
224
  exports.MenuPanel = MenuPanel; exports.Menu = Menu;
225
- //# sourceMappingURL=chunk-VPN7AR7T.cjs.map
225
+ //# sourceMappingURL=chunk-USGEUG2O.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/eidos-ui/eidos-ui/dist/chunk-VPN7AR7T.cjs","../src/components/Menu/Menu.component.tsx"],"names":[],"mappings":"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","file":"/home/runner/work/eidos-ui/eidos-ui/dist/chunk-VPN7AR7T.cjs","sourcesContent":[null,"import React, { useCallback, useMemo, useRef, useState } from 'react';\nimport { ChevronRight } from 'lucide-react';\nimport type { MenuProps, MenuItemType, MenuItem, MenuWrapperProps } from './Menu.types';\nimport { Dropdown } from '../Dropdown/Dropdown.component';\nimport { Tooltip } from '../Tooltip/Tooltip.component';\nimport { Kbd } from '../Kbd/Kbd.component';\nimport { renderIcon, useDialogFocus } from '../../utils';\n\n/**\n * MenuPanel - the bare menu list, without any trigger or dropdown wrapper.\n * Exported for use in SplitButton, ContextMenu, and other consumers that need\n * to render the menu content inside their own Dropdown / portal.\n *\n * ## This element owns the menu semantics, not the overlay around it\n *\n * `role=\"menu\"` belongs on the element that directly contains the items, which\n * is this `<ul>`. It used to sit on `Dropdown`'s content `<div>` instead, one\n * level further out, so the menu's only child was a list and the items were\n * `listitem`s - `aria-required-children`, and a structure no screen reader\n * could present as a menu.\n *\n * ## The keyboard model is not optional\n *\n * The items were plain `<li onClick>`: no role, no `tabIndex`, no key handler.\n * Every menu in the library - `Menu`, `ContextMenu`, `SplitButton`, `Table` and\n * `DataGrid` row actions - was therefore **operable by mouse only**, which is\n * WCAG 2.1.1 (Level A). Axe reported none of it, because every menu story\n * renders closed and axe only sees what is on screen.\n *\n * A `menuitem` role is a promise that arrow keys work, so the two land\n * together: roving `tabIndex` (one tab stop for the whole menu), Up/Down to\n * move, Home/End to jump, Enter/Space to activate, ArrowRight to open a\n * submenu. Focus enters on open and returns to the trigger on close.\n */\nexport const MenuPanel: React.FC<MenuProps> = ({\n items,\n className = '',\n onItemClick,\n onRequestClose,\n}) => {\n const listRef = useRef<HTMLUListElement>(null);\n const itemRefs = useRef(new Map<string, HTMLElement>());\n\n const registerItem = useCallback((id: string, element: HTMLElement | null) => {\n if (element) itemRefs.current.set(id, element);\n else itemRefs.current.delete(id);\n }, []);\n\n // Moves focus into the menu when it opens and back to the trigger when it\n // closes. `trapTab: false` - a menu is not a dialog, and confining Tab to it\n // would strand the user; the hook's frame-retry loop is what matters here,\n // because `Dropdown`'s content is `visibility: hidden` until it has been\n // positioned and `.focus()` on a hidden element is a silent no-op.\n //\n // `active` is unconditionally true: this panel is only ever mounted by an\n // open overlay (`Dropdown` and `ContextMenu` both render content\n // conditionally), so mounting *is* opening.\n useDialogFocus(true, listRef, { trapTab: false });\n\n // Handle menu item click\n const handleItemClick = useCallback(\n (item: MenuItem) => {\n if (item.disabled) return;\n\n // Execute the item's onClick handler first\n if (item.onClick) {\n item.onClick();\n }\n\n // Then call the onItemClick callback (which will close the dropdown)\n if (onItemClick) {\n onItemClick(item);\n }\n },\n [onItemClick],\n );\n\n // The items arrow keys move between, in visual order. Separators are not\n // stops, `component` items own their own focusable content, and a disabled\n // item is skipped rather than being a dead stop.\n const navigableIds = useMemo(\n () =>\n items\n .filter((item) => (item.type === 'item' || item.type === 'nested') && !item.disabled)\n .map((item) => item.id),\n [items],\n );\n\n const [focusedId, setFocusedId] = useState<string | null>(null);\n // Which submenu is open, owned here rather than inside each nested overlay:\n // the trigger has to advertise `aria-expanded`, and `ArrowLeft` in the child\n // has to be able to close it. Both were impossible while the overlay kept\n // that state to itself.\n const [openSubmenuId, setOpenSubmenuId] = useState<string | null>(null);\n // Validated rather than trusted: `items` can change while the menu is open\n // (a filtered list, an action that disables its neighbour), which would\n // otherwise leave the roving tabIndex on an id that no longer renders - and\n // therefore no tab stop in the menu at all.\n const activeId =\n focusedId && navigableIds.includes(focusedId) ? focusedId : (navigableIds[0] ?? null);\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLUListElement>) => {\n // A `component` item may hold a real control - a text field, a switch -\n // whose own arrow keys must not be hijacked for menu navigation. Only\n // keys pressed on a menuitem drive the menu.\n if (!(event.target as HTMLElement).closest('[role=\"menuitem\"]')) return;\n if (navigableIds.length === 0) return;\n\n const focusAt = (index: number) => {\n const id = navigableIds[(index + navigableIds.length) % navigableIds.length];\n setFocusedId(id);\n itemRefs.current.get(id)?.focus();\n };\n const current = Math.max(0, navigableIds.indexOf(activeId ?? ''));\n\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault();\n focusAt(current + 1);\n break;\n case 'ArrowUp':\n event.preventDefault();\n focusAt(current - 1);\n break;\n case 'Home':\n event.preventDefault();\n focusAt(0);\n break;\n case 'End':\n event.preventDefault();\n focusAt(navigableIds.length - 1);\n break;\n case 'ArrowLeft':\n // Only meaningful in a submenu. `stopPropagation` keeps the parent\n // panel - which is a React ancestor, portal notwithstanding - from\n // treating the same keypress as its own.\n if (onRequestClose) {\n event.preventDefault();\n event.stopPropagation();\n onRequestClose();\n }\n break;\n default:\n break;\n }\n },\n [navigableIds, activeId, onRequestClose],\n );\n\n // Render individual menu item\n const renderMenuItem = (item: MenuItemType): React.ReactNode => {\n switch (item.type) {\n case 'item':\n return (\n <li\n key={item.id}\n ref={(element) => registerItem(item.id, element)}\n role=\"menuitem\"\n // Roving tabIndex: the menu is one tab stop and arrows move within\n // it. Making every item a tab stop would bury whatever follows a\n // ten-item menu ten presses deep.\n tabIndex={!item.disabled && item.id === activeId ? 0 : -1}\n // `aria-disabled`, not the `disabled` attribute, which an `li` does\n // not have. The click handler already returns early.\n aria-disabled={item.disabled || undefined}\n className={[\n 'eidos-menu-item',\n item.disabled ? 'eidos-menu-item--disabled' : '',\n item.color ? `eidos-menu-item--${item.color}` : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onClick={() => handleItemClick(item)}\n onKeyDown={(event) => {\n // An `li` is not a button, so Enter and Space produce no click of\n // their own - activation has to be wired by hand. Space is\n // prevented to stop the page scrolling underneath.\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n handleItemClick(item);\n }\n }}\n >\n {item.icon && renderIcon(item.icon, 'eidos-menu-icon')}\n <span className={`eidos-menu-label`}>{item.label}</span>\n {item.shortcut && (\n <Kbd size=\"sm\" className=\"eidos-menu-shortcut\">\n {item.shortcut}\n </Kbd>\n )}\n </li>\n );\n\n case 'component':\n // `role=\"none\"` because the `li` is a wrapper: whatever the consumer\n // renders owns its own semantics, and calling it a `menuitem` would nest\n // that control inside an interactive role.\n return (\n <li key={item.id} role=\"none\" className={`eidos-menu-component-item`}>\n {item.component}\n </li>\n );\n\n case 'separator':\n return <li key={item.id} role=\"separator\" className={`eidos-menu-separator`} />;\n\n case 'nested': {\n const triggerElement = (\n <div\n ref={(element) => registerItem(item.id, element)}\n role=\"menuitem\"\n aria-haspopup=\"menu\"\n aria-expanded={openSubmenuId === item.id}\n aria-disabled={item.disabled || undefined}\n tabIndex={!item.disabled && item.id === activeId ? 0 : -1}\n className={[\n 'eidos-menu-nested-trigger',\n item.disabled ? 'eidos-menu-nested-trigger--disabled' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onKeyDown={(event) => {\n if (item.disabled) return;\n // ArrowRight is the menu idiom for \"open this submenu\"; Enter and\n // Space do the same. This used to synthesise a click on itself so\n // the event would reach the overlay's trigger wrapper; the state\n // is now right here.\n if (event.key === 'ArrowRight' || event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n setOpenSubmenuId(item.id);\n }\n }}\n >\n {item.icon && renderIcon(item.icon, 'eidos-menu-icon')}\n <span className={`eidos-menu-label`}>{item.label}</span>\n <ChevronRight className={`eidos-menu-nested-arrow`} />\n </div>\n );\n\n return (\n <li key={item.id} role=\"none\" className={`eidos-menu-nested-item`}>\n <Dropdown\n open={openSubmenuId === item.id}\n onOpenChange={(next) => setOpenSubmenuId(next ? item.id : null)}\n trigger={triggerElement}\n content={\n <MenuPanel\n items={item.items}\n onItemClick={onItemClick}\n // Lets `ArrowLeft` in the child close this level. Focus\n // returns to the trigger on its own - `useDialogFocus`\n // restores it when the panel unmounts.\n onRequestClose={() => setOpenSubmenuId(null)}\n />\n }\n placement=\"right\"\n isNested={true}\n fullWidth\n // `disabled` was accepted on a nested item and then ignored, so a\n // disabled submenu opened like any other.\n disabled={item.disabled}\n />\n </li>\n );\n }\n\n default:\n return null;\n }\n };\n\n return (\n <ul ref={listRef} role=\"menu\" className={`eidos-menu ${className}`} onKeyDown={handleKeyDown}>\n {items.map(renderMenuItem)}\n </ul>\n );\n};\n\nexport const Menu: React.FC<MenuWrapperProps> = ({\n trigger,\n items,\n minWidth,\n maxWidth,\n minHeight,\n maxHeight,\n autoWidth = true,\n triggerRef,\n closeOnItemClick = true,\n tooltip,\n}) => {\n const [isOpen, setIsOpen] = useState(false);\n\n const handleItemClick = useCallback(\n (_item: MenuItem) => {\n // `item.onClick` has already run, in `MenuPanel`. All that is left is\n // closing.\n //\n // This used to dispatch a synthetic `mousedown` at `document` on a 10ms\n // timer - a counterfeit \"click outside\" - because the overlay owned its\n // open state privately and that was the only lever available. It worked by\n // being indistinguishable from the click a mouse user had just made, and\n // it broke the moment nobody had clicked: activating an item with the\n // keyboard closed every other dropdown on the page that dismisses on\n // outside clicks.\n if (closeOnItemClick) setIsOpen(false);\n },\n [closeOnItemClick],\n );\n\n // The trigger opens a menu, and nothing said so: `Dropdown` adds no ARIA to\n // its trigger (deliberately - it does not know what its content is), so a\n // screen reader announced a plain button. `Menu` does know, and now that it\n // owns the open state it can describe that too (SC 4.1.2), which was the\n // missing half while the state was private.\n const describedTrigger = React.isValidElement(trigger)\n ? React.cloneElement(\n trigger as React.ReactElement<{ 'aria-haspopup'?: string; 'aria-expanded'?: boolean }>,\n { 'aria-haspopup': 'menu', 'aria-expanded': isOpen },\n )\n : trigger;\n\n const dropdown = (\n <Dropdown\n open={isOpen}\n onOpenChange={setIsOpen}\n trigger={describedTrigger}\n content={<MenuPanel items={items} onItemClick={handleItemClick} />}\n minWidth={minWidth}\n maxWidth={maxWidth}\n minHeight={minHeight}\n maxHeight={maxHeight}\n autoWidth={autoWidth}\n triggerRef={triggerRef}\n />\n );\n\n return tooltip ? <Tooltip message={tooltip}>{dropdown}</Tooltip> : dropdown;\n};\n"]}
1
+ {"version":3,"sources":["/home/runner/work/eidos-ui/eidos-ui/dist/chunk-USGEUG2O.cjs","../src/components/Menu/Menu.component.tsx"],"names":[],"mappings":"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","file":"/home/runner/work/eidos-ui/eidos-ui/dist/chunk-USGEUG2O.cjs","sourcesContent":[null,"import React, { useCallback, useMemo, useRef, useState } from 'react';\nimport { ChevronRight } from 'lucide-react';\nimport type { MenuProps, MenuItemType, MenuItem, MenuWrapperProps } from './Menu.types';\nimport { Dropdown } from '../Dropdown/Dropdown.component';\nimport { Tooltip } from '../Tooltip/Tooltip.component';\nimport { Kbd } from '../Kbd/Kbd.component';\nimport { renderIcon, useDialogFocus } from '../../utils';\n\n/**\n * MenuPanel - the bare menu list, without any trigger or dropdown wrapper.\n * Exported for use in SplitButton, ContextMenu, and other consumers that need\n * to render the menu content inside their own Dropdown / portal.\n *\n * ## This element owns the menu semantics, not the overlay around it\n *\n * `role=\"menu\"` belongs on the element that directly contains the items, which\n * is this `<ul>`. It used to sit on `Dropdown`'s content `<div>` instead, one\n * level further out, so the menu's only child was a list and the items were\n * `listitem`s - `aria-required-children`, and a structure no screen reader\n * could present as a menu.\n *\n * ## The keyboard model is not optional\n *\n * The items were plain `<li onClick>`: no role, no `tabIndex`, no key handler.\n * Every menu in the library - `Menu`, `ContextMenu`, `SplitButton`, `Table` and\n * `DataGrid` row actions - was therefore **operable by mouse only**, which is\n * WCAG 2.1.1 (Level A). Axe reported none of it, because every menu story\n * renders closed and axe only sees what is on screen.\n *\n * A `menuitem` role is a promise that arrow keys work, so the two land\n * together: roving `tabIndex` (one tab stop for the whole menu), Up/Down to\n * move, Home/End to jump, Enter/Space to activate, ArrowRight to open a\n * submenu. Focus enters on open and returns to the trigger on close.\n */\nexport const MenuPanel: React.FC<MenuProps> = ({\n items,\n className = '',\n onItemClick,\n onRequestClose,\n}) => {\n const listRef = useRef<HTMLUListElement>(null);\n const itemRefs = useRef(new Map<string, HTMLElement>());\n\n const registerItem = useCallback((id: string, element: HTMLElement | null) => {\n if (element) itemRefs.current.set(id, element);\n else itemRefs.current.delete(id);\n }, []);\n\n // Moves focus into the menu when it opens and back to the trigger when it\n // closes. `trapTab: false` - a menu is not a dialog, and confining Tab to it\n // would strand the user; the hook's frame-retry loop is what matters here,\n // because `Dropdown`'s content is `visibility: hidden` until it has been\n // positioned and `.focus()` on a hidden element is a silent no-op.\n //\n // `active` is unconditionally true: this panel is only ever mounted by an\n // open overlay (`Dropdown` and `ContextMenu` both render content\n // conditionally), so mounting *is* opening.\n useDialogFocus(true, listRef, { trapTab: false });\n\n // Handle menu item click\n const handleItemClick = useCallback(\n (item: MenuItem) => {\n if (item.disabled) return;\n\n // Execute the item's onClick handler first\n if (item.onClick) {\n item.onClick();\n }\n\n // Then call the onItemClick callback (which will close the dropdown)\n if (onItemClick) {\n onItemClick(item);\n }\n },\n [onItemClick],\n );\n\n // The items arrow keys move between, in visual order. Separators are not\n // stops, `component` items own their own focusable content, and a disabled\n // item is skipped rather than being a dead stop.\n const navigableIds = useMemo(\n () =>\n items\n .filter((item) => (item.type === 'item' || item.type === 'nested') && !item.disabled)\n .map((item) => item.id),\n [items],\n );\n\n const [focusedId, setFocusedId] = useState<string | null>(null);\n // Which submenu is open, owned here rather than inside each nested overlay:\n // the trigger has to advertise `aria-expanded`, and `ArrowLeft` in the child\n // has to be able to close it. Both were impossible while the overlay kept\n // that state to itself.\n const [openSubmenuId, setOpenSubmenuId] = useState<string | null>(null);\n // Validated rather than trusted: `items` can change while the menu is open\n // (a filtered list, an action that disables its neighbour), which would\n // otherwise leave the roving tabIndex on an id that no longer renders - and\n // therefore no tab stop in the menu at all.\n const activeId =\n focusedId && navigableIds.includes(focusedId) ? focusedId : (navigableIds[0] ?? null);\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLUListElement>) => {\n // A `component` item may hold a real control - a text field, a switch -\n // whose own arrow keys must not be hijacked for menu navigation. Only\n // keys pressed on a menuitem drive the menu.\n if (!(event.target as HTMLElement).closest('[role=\"menuitem\"]')) return;\n if (navigableIds.length === 0) return;\n\n const focusAt = (index: number) => {\n const id = navigableIds[(index + navigableIds.length) % navigableIds.length];\n setFocusedId(id);\n itemRefs.current.get(id)?.focus();\n };\n const current = Math.max(0, navigableIds.indexOf(activeId ?? ''));\n\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault();\n focusAt(current + 1);\n break;\n case 'ArrowUp':\n event.preventDefault();\n focusAt(current - 1);\n break;\n case 'Home':\n event.preventDefault();\n focusAt(0);\n break;\n case 'End':\n event.preventDefault();\n focusAt(navigableIds.length - 1);\n break;\n case 'ArrowLeft':\n // Only meaningful in a submenu. `stopPropagation` keeps the parent\n // panel - which is a React ancestor, portal notwithstanding - from\n // treating the same keypress as its own.\n if (onRequestClose) {\n event.preventDefault();\n event.stopPropagation();\n onRequestClose();\n }\n break;\n default:\n break;\n }\n },\n [navigableIds, activeId, onRequestClose],\n );\n\n // Render individual menu item\n const renderMenuItem = (item: MenuItemType): React.ReactNode => {\n switch (item.type) {\n case 'item':\n return (\n <li\n key={item.id}\n ref={(element) => registerItem(item.id, element)}\n role=\"menuitem\"\n // Roving tabIndex: the menu is one tab stop and arrows move within\n // it. Making every item a tab stop would bury whatever follows a\n // ten-item menu ten presses deep.\n tabIndex={!item.disabled && item.id === activeId ? 0 : -1}\n // `aria-disabled`, not the `disabled` attribute, which an `li` does\n // not have. The click handler already returns early.\n aria-disabled={item.disabled || undefined}\n className={[\n 'eidos-menu-item',\n item.disabled ? 'eidos-menu-item--disabled' : '',\n item.color ? `eidos-menu-item--${item.color}` : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onClick={() => handleItemClick(item)}\n onKeyDown={(event) => {\n // An `li` is not a button, so Enter and Space produce no click of\n // their own - activation has to be wired by hand. Space is\n // prevented to stop the page scrolling underneath.\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n handleItemClick(item);\n }\n }}\n >\n {item.icon && renderIcon(item.icon, 'eidos-menu-icon')}\n <span className={`eidos-menu-label`}>{item.label}</span>\n {item.shortcut && (\n <Kbd size=\"sm\" className=\"eidos-menu-shortcut\">\n {item.shortcut}\n </Kbd>\n )}\n </li>\n );\n\n case 'component':\n // `role=\"none\"` because the `li` is a wrapper: whatever the consumer\n // renders owns its own semantics, and calling it a `menuitem` would nest\n // that control inside an interactive role.\n return (\n <li key={item.id} role=\"none\" className={`eidos-menu-component-item`}>\n {item.component}\n </li>\n );\n\n case 'separator':\n return <li key={item.id} role=\"separator\" className={`eidos-menu-separator`} />;\n\n case 'nested': {\n const triggerElement = (\n <div\n ref={(element) => registerItem(item.id, element)}\n role=\"menuitem\"\n aria-haspopup=\"menu\"\n aria-expanded={openSubmenuId === item.id}\n aria-disabled={item.disabled || undefined}\n tabIndex={!item.disabled && item.id === activeId ? 0 : -1}\n className={[\n 'eidos-menu-nested-trigger',\n item.disabled ? 'eidos-menu-nested-trigger--disabled' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onKeyDown={(event) => {\n if (item.disabled) return;\n // ArrowRight is the menu idiom for \"open this submenu\"; Enter and\n // Space do the same. This used to synthesise a click on itself so\n // the event would reach the overlay's trigger wrapper; the state\n // is now right here.\n if (event.key === 'ArrowRight' || event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n setOpenSubmenuId(item.id);\n }\n }}\n >\n {item.icon && renderIcon(item.icon, 'eidos-menu-icon')}\n <span className={`eidos-menu-label`}>{item.label}</span>\n <ChevronRight className={`eidos-menu-nested-arrow`} />\n </div>\n );\n\n return (\n <li key={item.id} role=\"none\" className={`eidos-menu-nested-item`}>\n <Dropdown\n open={openSubmenuId === item.id}\n onOpenChange={(next) => setOpenSubmenuId(next ? item.id : null)}\n trigger={triggerElement}\n content={\n <MenuPanel\n items={item.items}\n onItemClick={onItemClick}\n // Lets `ArrowLeft` in the child close this level. Focus\n // returns to the trigger on its own - `useDialogFocus`\n // restores it when the panel unmounts.\n onRequestClose={() => setOpenSubmenuId(null)}\n />\n }\n placement=\"right\"\n isNested={true}\n fullWidth\n // `disabled` was accepted on a nested item and then ignored, so a\n // disabled submenu opened like any other.\n disabled={item.disabled}\n />\n </li>\n );\n }\n\n default:\n return null;\n }\n };\n\n return (\n <ul ref={listRef} role=\"menu\" className={`eidos-menu ${className}`} onKeyDown={handleKeyDown}>\n {items.map(renderMenuItem)}\n </ul>\n );\n};\n\nexport const Menu: React.FC<MenuWrapperProps> = ({\n trigger,\n items,\n minWidth,\n maxWidth,\n minHeight,\n maxHeight,\n autoWidth = true,\n triggerRef,\n closeOnItemClick = true,\n tooltip,\n}) => {\n const [isOpen, setIsOpen] = useState(false);\n\n const handleItemClick = useCallback(\n (_item: MenuItem) => {\n // `item.onClick` has already run, in `MenuPanel`. All that is left is\n // closing.\n //\n // This used to dispatch a synthetic `mousedown` at `document` on a 10ms\n // timer - a counterfeit \"click outside\" - because the overlay owned its\n // open state privately and that was the only lever available. It worked by\n // being indistinguishable from the click a mouse user had just made, and\n // it broke the moment nobody had clicked: activating an item with the\n // keyboard closed every other dropdown on the page that dismisses on\n // outside clicks.\n if (closeOnItemClick) setIsOpen(false);\n },\n [closeOnItemClick],\n );\n\n // The trigger opens a menu, and nothing said so: `Dropdown` adds no ARIA to\n // its trigger (deliberately - it does not know what its content is), so a\n // screen reader announced a plain button. `Menu` does know, and now that it\n // owns the open state it can describe that too (SC 4.1.2), which was the\n // missing half while the state was private.\n const describedTrigger = React.isValidElement(trigger)\n ? React.cloneElement(\n trigger as React.ReactElement<{ 'aria-haspopup'?: string; 'aria-expanded'?: boolean }>,\n { 'aria-haspopup': 'menu', 'aria-expanded': isOpen },\n )\n : trigger;\n\n const dropdown = (\n <Dropdown\n open={isOpen}\n onOpenChange={setIsOpen}\n trigger={describedTrigger}\n content={<MenuPanel items={items} onItemClick={handleItemClick} />}\n minWidth={minWidth}\n maxWidth={maxWidth}\n minHeight={minHeight}\n maxHeight={maxHeight}\n autoWidth={autoWidth}\n triggerRef={triggerRef}\n />\n );\n\n return tooltip ? <Tooltip message={tooltip}>{dropdown}</Tooltip> : dropdown;\n};\n"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Dropdown
3
- } from "./chunk-OERK3T7E.js";
3
+ } from "./chunk-ECP5J27K.js";
4
4
  import {
5
5
  Kbd
6
6
  } from "./chunk-LYH3EKKQ.js";
@@ -222,4 +222,4 @@ export {
222
222
  MenuPanel,
223
223
  Menu
224
224
  };
225
- //# sourceMappingURL=chunk-TOLNPD7S.js.map
225
+ //# sourceMappingURL=chunk-VA23PQTA.js.map