1- import { ChevronLeft } from 'lucide-react' ;
1+ import { ChevronLeft , MoreHorizontal } from 'lucide-react' ;
22
33import { ChatDetailView } from '@ui/conversations/views/ChatDetailView' ;
44import { ArticleReaderView } from '@ui/conversations/views/ArticleReaderView' ;
@@ -9,7 +9,8 @@ import { useChatOutlineActiveIndex } from '@ui/conversations/chat-outline/useCha
99import { t , formatConversationTitle } from '@i18n' ;
1010import { useConversationsApp } from '@viewmodels/conversations/conversations-context' ;
1111import { DetailHeaderActionBar } from '@ui/conversations/DetailHeaderActionBar' ;
12- import { buttonTintClassName , headerButtonClassName } from '@ui/shared/button-styles' ;
12+ import { buttonMenuItemClassName , headerButtonClassName } from '@ui/shared/button-styles' ;
13+ import { MenuPopover } from '@ui/shared/MenuPopover' ;
1314import { tooltipAttrs } from '@ui/shared/AppTooltip' ;
1415import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
1516import { countWordsFromMessages } from '@services/shared/word-count' ;
@@ -61,7 +62,6 @@ export function ConversationDetailPane({
6162 const openActions = safeActions . filter ( ( action ) => action . slot === 'open' ) ;
6263 const toolActions = safeActions . filter ( ( action ) => action . slot === 'tools' ) ;
6364
64- const outlineButtonClass = buttonTintClassName ( ) ;
6565 const headerIconButtonClass = headerButtonClassName ( ) ;
6666 const kindView = conversationKinds . pick ( selected as any ) ?. view ?? DEFAULT_VIEW ;
6767 const isArticleRenderer = kindView . renderer === 'article' ;
@@ -160,6 +160,7 @@ export function ConversationDetailPane({
160160 const [ urlEditing , setUrlEditing ] = useState ( false ) ;
161161 const [ urlDraft , setUrlDraft ] = useState ( '' ) ;
162162 const [ urlCleaning , setUrlCleaning ] = useState ( false ) ;
163+ const [ moreMenuOpen , setMoreMenuOpen ] = useState ( false ) ;
163164 const urlInputRef = useRef < HTMLInputElement | null > ( null ) ;
164165 const displayedUrl = String ( ( selected as any ) ?. url || '' ) . trim ( ) ;
165166 const wordCount = useMemo ( ( ) => {
@@ -172,12 +173,32 @@ export function ConversationDetailPane({
172173 wordCount != null && Number . isFinite ( wordCount )
173174 ? `${ wordCountLabel } ${ Math . max ( 0 , Math . floor ( wordCount ) ) . toLocaleString ( ) } `
174175 : '' ;
176+ const hasReaderMoreMenuContent =
177+ readerFeatures . textLayout || readerFeatures . theme || readerFeatures . narration ;
178+ const hasMoreMenuContent = Boolean ( wordCountText ) || toolActions . length > 0 || hasReaderMoreMenuContent ;
179+ const moreMenuPanelClassName =
180+ 'tw-w-[214px] tw-max-w-[min(214px,calc(100vw-28px))] tw-text-[var(--text-primary)]' ;
181+ const closeMoreMenu = useCallback ( ( ) => {
182+ setMoreMenuOpen ( false ) ;
183+ setReaderToolbarPortalTarget ( null ) ;
184+ } , [ ] ) ;
185+ const handleMoreMenuOpenChange = useCallback (
186+ ( next : boolean ) => {
187+ if ( next ) {
188+ setMoreMenuOpen ( true ) ;
189+ return ;
190+ }
191+ closeMoreMenu ( ) ;
192+ } ,
193+ [ closeMoreMenu ] ,
194+ ) ;
175195
176196 useEffect ( ( ) => {
177197 setUrlEditing ( false ) ;
178198 setUrlDraft ( '' ) ;
179199 setUrlCleaning ( false ) ;
180- } , [ activeId ] ) ;
200+ closeMoreMenu ( ) ;
201+ } , [ activeId , selected ?. id , closeMoreMenu ] ) ;
181202
182203 useEffect ( ( ) => {
183204 userMessageElByIdRef . current . clear ( ) ;
@@ -353,18 +374,9 @@ export function ConversationDetailPane({
353374 } }
354375 aria-label = { displayedUrl ? 'Edit URL' : 'Set URL' }
355376 title = { displayedUrl || t ( 'noLinkAvailable' ) }
356- >
377+ >
357378 { displayedUrl || t ( 'noLinkAvailable' ) }
358379 </ button >
359- { wordCountText ? (
360- < span
361- className = "tw-inline-flex tw-items-center tw-rounded-[var(--radius-chip)] tw-border tw-border-[var(--border)] tw-px-2 tw-py-0.5 tw-text-[10px] tw-font-extrabold tw-text-[var(--text-secondary)]"
362- aria-label = { wordCountText }
363- { ...tooltipAttrs ( wordCountText ) }
364- >
365- { wordCountText }
366- </ span >
367- ) : null }
368380 </ >
369381 ) }
370382 </ div >
@@ -376,27 +388,14 @@ export function ConversationDetailPane({
376388 </ div >
377389 </ div >
378390 < div className = "tw-flex tw-shrink-0 tw-items-center tw-justify-end tw-gap-2 tw-whitespace-nowrap" >
379- { isArticleRenderer ? (
380- < div
381- ref = { setReaderToolbarPortalTarget }
382- className = "tw-flex tw-items-center tw-gap-2"
383- data-reader-header-toolbar-slot = "true"
384- />
385- ) : null }
386- < DetailHeaderActionBar
387- actions = { toolActions }
388- buttonClassName = { outlineButtonClass }
389- menuTriggerLabel = { t ( 'detailHeaderToolsMenuLabel' ) }
390- menuTriggerAriaLabel = { t ( 'detailHeaderToolsMenuAria' ) }
391- menuAriaLabel = { t ( 'detailHeaderToolsMenuAria' ) }
392- />
393391 < DetailHeaderActionBar
394392 actions = { openActions }
395393 buttonClassName = { headerIconButtonClass }
396394 iconOnly
397395 menuTriggerLabel = { t ( 'detailHeaderOpenInMenuLabel' ) }
398396 menuTriggerAriaLabel = { t ( 'detailHeaderOpenInMenuAria' ) }
399397 menuAriaLabel = { t ( 'detailHeaderOpenInMenuAria' ) }
398+ className = "tw-order-1"
400399 />
401400
402401 { canOpenCommentsSidebar ? (
@@ -405,7 +404,7 @@ export function ConversationDetailPane({
405404 onClick = { ( ) => {
406405 onTriggerCommentsSidebar ?.( ) ;
407406 } }
408- className = { headerButtonClassName ( ) }
407+ className = { [ headerButtonClassName ( ) , 'tw-order-2' ] . join ( ' ' ) }
409408 aria-label = { commentsSidebarLabel }
410409 { ...tooltipAttrs ( commentsSidebarLabel ) }
411410 aria-pressed = { commentsSidebarOpen ? 'true' : 'false' }
@@ -424,6 +423,75 @@ export function ConversationDetailPane({
424423 < span className = "tw-sr-only" > { commentsSidebarLabel } </ span >
425424 </ button >
426425 ) : null }
426+ { hasMoreMenuContent ? (
427+ < MenuPopover
428+ open = { moreMenuOpen }
429+ onOpenChange = { handleMoreMenuOpenChange }
430+ ariaLabel = { t ( 'moreButton' ) }
431+ side = "bottom"
432+ align = "end"
433+ panelMinWidth = { 214 }
434+ panelClassName = { moreMenuPanelClassName }
435+ className = "tw-order-3"
436+ trigger = { ( triggerProps ) => (
437+ < button
438+ { ...triggerProps }
439+ data-detail-header-more-trigger = "true"
440+ aria-label = { t ( 'moreButton' ) }
441+ className = { headerIconButtonClass }
442+ >
443+ < MoreHorizontal size = { 14 } strokeWidth = { 2 } aria-hidden = "true" />
444+ < span className = "tw-sr-only" > { t ( 'moreButton' ) } </ span >
445+ </ button >
446+ ) }
447+ >
448+ { moreMenuOpen ? (
449+ < div className = "tw-flex tw-flex-col tw-gap-1" >
450+ { hasReaderMoreMenuContent ? (
451+ < div
452+ ref = { setReaderToolbarPortalTarget }
453+ className = "tw-flex tw-flex-col tw-gap-1"
454+ data-reader-header-toolbar-slot = "true"
455+ />
456+ ) : null }
457+
458+ { toolActions . length ? (
459+ < div
460+ className = { [
461+ hasReaderMoreMenuContent ? 'tw-border-t tw-border-[var(--border)] tw-pt-1' : '' ,
462+ 'tw-flex tw-flex-col tw-gap-1' ,
463+ ]
464+ . filter ( Boolean )
465+ . join ( ' ' ) }
466+ >
467+ < DetailHeaderActionBar
468+ actions = { toolActions }
469+ buttonClassName = { buttonMenuItemClassName ( ) }
470+ showLabelAlways
471+ closeMenuOnActionTrigger = { closeMoreMenu }
472+ className = "tw-w-full"
473+ />
474+ </ div >
475+ ) : null }
476+
477+ { wordCountText ? (
478+ < div
479+ className = { [
480+ hasReaderMoreMenuContent || toolActions . length ? 'tw-border-t tw-border-[var(--border)] tw-pt-1' : '' ,
481+ 'tw-px-2 tw-py-1 tw-text-[11px] tw-font-semibold tw-text-[var(--text-secondary)]' ,
482+ ]
483+ . filter ( Boolean )
484+ . join ( ' ' ) }
485+ data-detail-word-count-row = "true"
486+ { ...tooltipAttrs ( wordCountText ) }
487+ >
488+ { wordCountText }
489+ </ div >
490+ ) : null }
491+ </ div >
492+ ) : null }
493+ </ MenuPopover >
494+ ) : null }
427495 </ div >
428496
429497 { isChatRenderer ? (
@@ -446,18 +514,6 @@ export function ConversationDetailPane({
446514 'tw-relative tw-pb-3 md:tw-pb-4' ,
447515 ] . join ( ' ' ) }
448516 >
449- { wordCountText && urlEditing ? (
450- < div className = "tw-flex tw-justify-end" >
451- < span
452- className = "tw-inline-flex tw-items-center tw-rounded-[var(--radius-chip)] tw-border tw-border-[var(--border)] tw-px-2 tw-py-0.5 tw-text-[10px] tw-font-extrabold tw-text-[var(--text-secondary)]"
453- aria-label = { wordCountText }
454- { ...tooltipAttrs ( wordCountText ) }
455- >
456- { wordCountText }
457- </ span >
458- </ div >
459- ) : null }
460-
461517 { isArticleRenderer ? (
462518 < ArticleReaderView
463519 selected = { selected }
0 commit comments