@@ -25,59 +25,13 @@ export default function PageView() {
2525 const [ backlinks , setBacklinks ] = useState ( [ ] )
2626 const [ menuOpen , setMenuOpen ] = useState ( false )
2727 const menuRef = useRef ( null )
28- const [ publicMenuOpen , setPublicMenuOpen ] = useState ( false )
29- const publicMenuRef = useRef ( null )
3028 const [ publicConfirmOpen , setPublicConfirmOpen ] = useState ( false )
3129 const [ deleteConfirmOpen , setDeleteConfirmOpen ] = useState ( false )
3230 const [ toast , setToast ] = useState ( '' )
3331 const [ headings , setHeadings ] = useState ( [ ] )
34- const [ scrolledPastTitle , setScrolledPastTitle ] = useState ( false )
35- const [ isDesktop , setIsDesktop ] = useState (
36- typeof window !== 'undefined' ? window . matchMedia ( '(min-width: 1024px)' ) . matches : true ,
37- )
38- const titleRef = useRef ( null )
3932
4033 const handleHeadings = useCallback ( ( items ) => setHeadings ( items ) , [ ] )
4134
42- // Track breakpoint so we know when the right rail is available.
43- useEffect ( ( ) => {
44- const mq = window . matchMedia ( '(min-width: 1024px)' )
45- const update = ( ) => setIsDesktop ( mq . matches )
46- update ( )
47- mq . addEventListener ( 'change' , update )
48- return ( ) => mq . removeEventListener ( 'change' , update )
49- } , [ ] )
50-
51- // Observe the title row; once it scrolls out of view, morph actions to the right rail dock.
52- useEffect ( ( ) => {
53- const el = titleRef . current
54- if ( ! el ) return
55- const io = new IntersectionObserver (
56- ( [ entry ] ) => setScrolledPastTitle ( ! entry . isIntersecting ) ,
57- { threshold : 0 } ,
58- )
59- io . observe ( el )
60- return ( ) => io . disconnect ( )
61- } , [ page ] )
62-
63- // Close the action menu when the actions morph between inline and dock,
64- // otherwise the outside-click listener chases a stale ref.
65- useEffect ( ( ) => {
66- setMenuOpen ( false )
67- } , [ scrolledPastTitle , isDesktop ] )
68-
69- // Close public menu on outside click
70- useEffect ( ( ) => {
71- if ( ! publicMenuOpen ) return
72- const handleClick = ( e ) => {
73- if ( publicMenuRef . current && ! publicMenuRef . current . contains ( e . target ) ) {
74- setPublicMenuOpen ( false )
75- }
76- }
77- document . addEventListener ( 'mousedown' , handleClick )
78- return ( ) => document . removeEventListener ( 'mousedown' , handleClick )
79- } , [ publicMenuOpen ] )
80-
8135 // Auto-dismiss toast after 2.5s
8236 useEffect ( ( ) => {
8337 if ( ! toast ) return
@@ -177,7 +131,7 @@ export default function PageView() {
177131 } catch {
178132 setToast ( link )
179133 }
180- setPublicMenuOpen ( false )
134+ setMenuOpen ( false )
181135 }
182136
183137 const handleMakePrivate = async ( ) => {
@@ -189,7 +143,7 @@ export default function PageView() {
189143 console . error ( 'Failed to make private:' , err )
190144 setToast ( 'Failed to update visibility' )
191145 }
192- setPublicMenuOpen ( false )
146+ setMenuOpen ( false )
193147 }
194148
195149 const handleMakePublic = async ( ) => {
@@ -216,9 +170,6 @@ export default function PageView() {
216170 if ( loading ) return < div className = "text-text-secondary" > Loading...</ div >
217171 if ( ! page ) return null
218172
219- const showInline = ! isDesktop || ! scrolledPastTitle
220- const showDock = isDesktop && scrolledPastTitle
221-
222173 const renderActions = ( variant ) => (
223174 < div className = { `page-actions page-actions-${ variant } ` } >
224175 < button
@@ -255,7 +206,41 @@ export default function PageView() {
255206 </ svg >
256207 History
257208 </ button >
258- { ! page . is_public && (
209+ < button
210+ onClick = { ( ) => { setMenuOpen ( false ) ; handleToggleWatch ( ) } }
211+ className = "w-full text-left px-3 py-2 text-sm text-text hover:bg-surface-hover flex items-center gap-2"
212+ >
213+ < svg className = "w-4 h-4 text-text-secondary" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" viewBox = "0 0 24 24" >
214+ < path d = "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
215+ < circle cx = "12" cy = "12" r = "3" />
216+ </ svg >
217+ { watching ? 'Stop watching' : 'Watch page' }
218+ </ button >
219+ < div className = "border-t border-border my-1" />
220+ { page . is_public ? (
221+ < >
222+ < button
223+ onClick = { handleCopyPublicLink }
224+ className = "w-full text-left px-3 py-2 text-sm text-text hover:bg-surface-hover flex items-center gap-2"
225+ >
226+ < svg className = "w-4 h-4 text-text-secondary" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" viewBox = "0 0 24 24" >
227+ < path d = "M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71" />
228+ < path d = "M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71" />
229+ </ svg >
230+ Copy public link
231+ </ button >
232+ < button
233+ onClick = { handleMakePrivate }
234+ className = "w-full text-left px-3 py-2 text-sm text-text hover:bg-surface-hover flex items-center gap-2"
235+ >
236+ < svg className = "w-4 h-4 text-text-secondary" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" viewBox = "0 0 24 24" >
237+ < rect x = "3" y = "11" width = "18" height = "11" rx = "2" />
238+ < path d = "M7 11V7a5 5 0 0110 0v4" />
239+ </ svg >
240+ Make private
241+ </ button >
242+ </ >
243+ ) : (
259244 < button
260245 onClick = { ( ) => { setMenuOpen ( false ) ; setPublicConfirmOpen ( true ) } }
261246 className = "w-full text-left px-3 py-2 text-sm text-text hover:bg-surface-hover flex items-center gap-2"
@@ -311,69 +296,21 @@ export default function PageView() {
311296 return (
312297 < div className = "max-w-6xl mx-auto lg:grid lg:grid-cols-[minmax(0,1fr)_220px] lg:gap-8" >
313298 < article >
314- < div ref = { titleRef } className = "flex items-center gap-3 mb-4 flex-wrap" >
299+ < div className = "flex items-center gap-3 mb-4 flex-wrap" >
315300 < h1 className = "text-3xl font-bold text-text" > { page . title } </ h1 >
316- { page . is_public && (
317- < div className = "relative" ref = { publicMenuRef } >
318- < button
319- onClick = { ( ) => setPublicMenuOpen ( ( v ) => ! v ) }
320- className = "inline-flex items-center gap-1 text-xs px-2.5 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 border border-green-300 dark:border-green-700 rounded-full hover:brightness-95"
321- title = "This page is public — click for options"
322- >
323- < span role = "img" aria-hidden = "true" > 🌐</ span > Public
324- < svg className = "w-3 h-3 opacity-70" fill = "none" stroke = "currentColor" strokeWidth = "2" viewBox = "0 0 24 24" >
325- < path d = "M6 9l6 6 6-6" />
326- </ svg >
327- </ button >
328- { publicMenuOpen && (
329- < div className = "absolute left-0 top-full mt-1 w-48 bg-surface border border-border rounded-lg shadow-lg py-1 z-50" >
330- < button
331- onClick = { handleCopyPublicLink }
332- className = "w-full text-left px-3 py-2 text-sm text-text hover:bg-surface-hover"
333- >
334- Copy public link
335- </ button >
336- < button
337- onClick = { handleMakePrivate }
338- className = "w-full text-left px-3 py-2 text-sm text-text hover:bg-surface-hover"
339- >
340- Make private
341- </ button >
342- </ div >
343- ) }
344- </ div >
345- ) }
346301 < button
347302 onClick = { handleToggleBookmark }
348303 className = { `text-xl transition-colors ${ bookmarked ? 'text-yellow-500' : 'text-gray-300 hover:text-yellow-400' } ` }
349304 title = { bookmarked ? 'Remove bookmark' : 'Add bookmark' }
350305 >
351306 { bookmarked ? '\u2605' : '\u2606' }
352307 </ button >
353- < button
354- onClick = { handleToggleWatch }
355- className = { `flex items-center gap-1 text-xs px-2 py-1 rounded-full border transition-colors ${
356- watching
357- ? 'bg-primary-soft text-primary border-primary/40'
358- : 'text-text-secondary border-border hover:border-text-secondary'
359- } `}
360- title = { watching ? 'Unwatch this page' : 'Watch this page for changes' }
361- >
362- < svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" >
363- < path d = "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
364- < circle cx = "12" cy = "12" r = "3" />
365- </ svg >
366- { watching ? 'Watching' : 'Watch' }
367- { watcherCount > 0 && < span className = "text-text-secondary" > ({ watcherCount } )</ span > }
368- </ button >
369308 </ div >
370309
371- { /* Inline action row — visible on mobile always, and on desktop until the title scrolls out */ }
372- { showInline && (
373- < div className = "mb-4" >
374- { renderActions ( 'inline' ) }
375- </ div >
376- ) }
310+ { /* Mobile: inline actions under the title. Desktop uses the right-rail dock. */ }
311+ < div className = "mb-4 lg:hidden" >
312+ { renderActions ( 'inline' ) }
313+ </ div >
377314
378315 { /* Tags */ }
379316 < div className = "flex flex-wrap items-center gap-2 mb-4" >
@@ -413,6 +350,8 @@ export default function PageView() {
413350 < div className = "text-sm text-text-secondary mb-6" >
414351 { page . author_name && < > { page . author_name } · </ > }
415352 /{ page . slug } · { page . view_count } views · Updated { new Date ( page . updated_at ) . toLocaleString ( ) }
353+ { page . is_public && < > · < span title = "This page is public" > 🌐 Public</ span > </ > }
354+ { watcherCount > 0 && < > · { watcherCount } watching</ > }
416355 </ div >
417356 < div className = "bg-surface rounded-xl shadow-sm border border-border p-8" >
418357 < MarkdownViewer content = { page . content_md } onHeadings = { handleHeadings } />
@@ -442,15 +381,13 @@ export default function PageView() {
442381 < Comments slug = { slug } />
443382 </ article >
444383
445- { /* Right rail: TOC + scroll-aware action dock */ }
384+ { /* Right rail: actions pinned above TOC so Edit has one consistent home */ }
446385 < aside className = "hidden lg:block" >
447386 < div className = "page-right-rail" >
387+ < div className = "page-action-dock-wrap" >
388+ { renderActions ( 'dock' ) }
389+ </ div >
448390 < TableOfContents headings = { headings } />
449- { showDock && (
450- < div className = "page-action-dock-wrap" >
451- { renderActions ( 'dock' ) }
452- </ div >
453- ) }
454391 </ div >
455392 </ aside >
456393
0 commit comments