@@ -7,18 +7,17 @@ import { Modal, ModalContent } from '@components/ui/Dialog'
77import UnreadBadge from '@components/ui/UnreadBadge'
88import { useBottomSheet } from '@hooks/useBottomSheet'
99import { useNotificationCount } from '@hooks/useNotificationCount'
10- import { useStore } from '@stores '
11- import { useAuthStore } from '@stores'
10+ import useUpdateDocMetadata from '@hooks/useUpdateDocMetadata '
11+ import { useAuthStore , useStore } from '@stores'
1212import dynamic from 'next/dynamic'
13- import React , { useState } from 'react'
13+ import React , { useEffect , useRef , useState } from 'react'
1414import { BiCheck , BiRedo , BiUndo } from 'react-icons/bi'
1515import { MdMenu , MdNotifications } from 'react-icons/md'
1616
1717const SettingsPanel = dynamic ( ( ) => import ( '@components/settings/SettingsPanel' ) , {
1818 loading : ( ) => < SettingsPanelSkeleton />
1919} )
2020
21- import DocTitle from '../DocTitle'
2221import FilterBar from './FilterBar'
2322import ReadOnlyIndicator from './ReadOnlyIndicator'
2423
@@ -128,13 +127,131 @@ const UndoRedoButtons = ({ editor, className }: UndoRedoButtonsProps) => {
128127 )
129128}
130129
130+ // ---------------------------------------------------------------------------
131+ // TitleEditContent – rendered inside the global dialog via openDialog()
132+ // ---------------------------------------------------------------------------
133+
134+ const TitleEditContent = ( ) => {
135+ const { metadata, hocuspocusProvider } = useStore ( ( state ) => state . settings )
136+ const setWorkspaceSetting = useStore ( ( state ) => state . setWorkspaceSetting )
137+ const closeDialog = useStore ( ( state ) => state . closeDialog )
138+ const { isLoading, mutate } = useUpdateDocMetadata ( )
139+ const [ value , setValue ] = useState ( '' )
140+ const inputRef = useRef < HTMLInputElement > ( null )
141+
142+ // Populate + auto-select on mount (dialog just opened)
143+ useEffect ( ( ) => {
144+ setValue ( metadata ?. title || '' )
145+ const timer = setTimeout ( ( ) => inputRef . current ?. select ( ) , 120 )
146+ return ( ) => clearTimeout ( timer )
147+ } , [ ] )
148+
149+ const handleSave = ( ) => {
150+ const trimmed = value . trim ( )
151+ if ( ! trimmed || trimmed === metadata ?. title ) {
152+ closeDialog ( )
153+ return
154+ }
155+
156+ mutate (
157+ { title : trimmed , documentId : metadata . documentId } ,
158+ {
159+ onSuccess : ( responseData ) => {
160+ const updated = ( responseData as any ) . data ?? responseData
161+ setWorkspaceSetting ( 'metadata' , { ...metadata , title : updated . title } )
162+ hocuspocusProvider ?. sendStateless ( JSON . stringify ( { type : 'docTitle' , state : updated } ) )
163+ closeDialog ( )
164+ }
165+ }
166+ )
167+ }
168+
169+ return (
170+ < div className = "p-5" >
171+ < label
172+ htmlFor = "mobile-doc-title-input"
173+ className = "text-base-content mb-3 block text-base font-semibold" >
174+ Rename Document Title
175+ </ label >
176+
177+ < input
178+ ref = { inputRef }
179+ id = "mobile-doc-title-input"
180+ type = "text"
181+ className = "input input-bordered w-full"
182+ value = { value }
183+ onChange = { ( e ) => setValue ( e . target . value ) }
184+ onKeyDown = { ( e ) => {
185+ if ( e . key === 'Enter' ) handleSave ( )
186+ if ( e . key === 'Escape' ) closeDialog ( )
187+ } }
188+ placeholder = "Document title"
189+ autoComplete = "off"
190+ maxLength = { 200 }
191+ />
192+
193+ < div className = "mt-4 flex justify-end gap-2" >
194+ < Button variant = "ghost" size = "sm" onClick = { closeDialog } >
195+ Cancel
196+ </ Button >
197+ < Button
198+ variant = "primary"
199+ size = "sm"
200+ onClick = { handleSave }
201+ disabled = { isLoading || ! value . trim ( ) } >
202+ { isLoading ? 'Saving…' : 'Save' }
203+ </ Button >
204+ </ div >
205+ </ div >
206+ )
207+ }
208+
209+ // ---------------------------------------------------------------------------
210+ // MobilePadTitle – sticky header for the mobile document view
211+ // ---------------------------------------------------------------------------
212+
131213const MobilePadTitle = ( ) => {
132214 const user = useAuthStore ( ( state ) => state . profile )
133215 const {
134- editor : { isEditable, instance : editor }
216+ editor : { isEditable, instance : editor } ,
217+ metadata,
218+ hocuspocusProvider
135219 } = useStore ( ( state ) => state . settings )
220+ const setWorkspaceSetting = useStore ( ( state ) => state . setWorkspaceSetting )
221+ const openDialog = useStore ( ( state ) => state . openDialog )
136222 const [ isProfileModalOpen , setProfileModalOpen ] = useState ( false )
137223
224+ // Keep the store title in sync with remote changes from other users.
225+ // On desktop this is handled by the always-mounted DocTitle component;
226+ // on mobile we need our own listener since DocTitle is not rendered.
227+ //
228+ // A ref is used so the handler always reads the latest metadata without
229+ // causing the effect to re-subscribe on every metadata change.
230+ const metadataRef = useRef ( metadata )
231+ metadataRef . current = metadata
232+
233+ useEffect ( ( ) => {
234+ if ( ! hocuspocusProvider ) return
235+
236+ const handler = ( { payload } : any ) => {
237+ try {
238+ const msg = JSON . parse ( payload )
239+ if ( msg . type === 'docTitle' ) {
240+ setWorkspaceSetting ( 'metadata' , { ...metadataRef . current , title : msg . state . title } )
241+ }
242+ } catch {
243+ /* ignore malformed payloads */
244+ }
245+ }
246+
247+ hocuspocusProvider . on ( 'stateless' , handler )
248+ return ( ) => hocuspocusProvider . off ( 'stateless' , handler )
249+ } , [ hocuspocusProvider , setWorkspaceSetting ] )
250+
251+ const handleTitleClick = ( ) => {
252+ openDialog ( < TitleEditContent /> , { size : 'sm' , align : 'top' , className : 'mt-14' } )
253+ }
254+
138255 return (
139256 < >
140257 { /* Sticky mobile header - theme-aware */ }
@@ -149,9 +266,12 @@ const MobilePadTitle = () => {
149266 { isEditable ? (
150267 < UndoRedoButtons editor = { editor } className = "ml-2" />
151268 ) : (
152- < div className = "min-w-0 flex-1 overflow-hidden" >
153- < DocTitle className = "truncate text-sm font-medium" />
154- </ div >
269+ < button
270+ type = "button"
271+ className = "min-w-0 flex-1 truncate text-left text-lg font-semibold"
272+ onClick = { handleTitleClick } >
273+ { metadata ?. title || 'Untitled' }
274+ </ button >
155275 ) }
156276 </ div >
157277
0 commit comments