@@ -2,7 +2,7 @@ import type { Theme } from "vitepress";
22import type { ThemeContext } from "@voidzero-dev/vitepress-theme" ;
33import VoidZeroTheme from "@voidzero-dev/vitepress-theme" ;
44import { themeContextKey } from "@voidzero-dev/vitepress-theme" ;
5- import { onMounted , watch , nextTick } from "vue" ;
5+ import { onMounted , onUnmounted , watch , nextTick } from "vue" ;
66import { useRoute } from "vitepress" ;
77import { enhanceAppWithTabs } from "vitepress-plugin-tabs/client" ;
88import mediumZoom from "medium-zoom" ;
@@ -93,45 +93,101 @@ function updateHashOnTabClick(event: Event) {
9393/**
9494 * Move "Sign in" CTA to the right utility area (between search and theme toggle).
9595 * The upstream OSSHeader renders nav links on the left; we remap only this CTA.
96+ * Returns true if the link was found and moved (or already in the target region).
9697 */
97- function moveSignInToUtilityArea ( ) {
98- if ( typeof document === "undefined" ) return ;
98+ function moveSignInToUtilityArea ( ) : boolean {
99+ if ( typeof document === "undefined" ) return false ;
99100
100101 const signInLink = document . querySelector (
101- '.docs-layout header .VPNavBarMenu a.VPLink[href*="sign-in"]' ,
102+ '.docs-layout header a.VPLink[href*="sign-in"]' ,
102103 ) as HTMLAnchorElement | null ;
103- if ( ! signInLink ) return ;
104+ if ( ! signInLink ) return false ;
105+
106+ if ( signInLink . classList . contains ( "sign-in-relocated" ) ) {
107+ return true ;
108+ }
104109
105110 const appearanceToggle = document . querySelector (
106111 ".docs-layout header .VPNavBarAppearance" ,
107112 ) as HTMLElement | null ;
108113
114+ if (
115+ appearanceToggle ?. parentElement &&
116+ signInLink . parentElement === appearanceToggle . parentElement
117+ ) {
118+ signInLink . classList . add ( "sign-in-relocated" ) ;
119+ return true ;
120+ }
121+
122+ const extraMenu = document . querySelector (
123+ ".docs-layout header .VPNavBarExtra" ,
124+ ) as HTMLElement | null ;
125+ if ( extraMenu ?. parentElement && signInLink . parentElement === extraMenu . parentElement ) {
126+ signInLink . classList . add ( "sign-in-relocated" ) ;
127+ return true ;
128+ }
129+
109130 // Desktop (xl+): insert before theme toggle inside the utilities row.
110131 if (
111132 appearanceToggle ?. parentElement &&
112133 signInLink . parentElement !== appearanceToggle . parentElement
113134 ) {
114135 signInLink . classList . add ( "sign-in-relocated" ) ;
115136 appearanceToggle . parentElement . insertBefore ( signInLink , appearanceToggle ) ;
116- return ;
137+ return true ;
117138 }
118139
119140 // Tablet fallback (lg-xl): keep it in right controls row before the extra menu.
120- const extraMenu = document . querySelector (
121- ".docs-layout header .VPNavBarExtra" ,
122- ) as HTMLElement | null ;
123141 if ( extraMenu ?. parentElement && signInLink . parentElement !== extraMenu . parentElement ) {
124142 signInLink . classList . add ( "sign-in-relocated" ) ;
125143 extraMenu . parentElement . insertBefore ( signInLink , extraMenu ) ;
144+ return true ;
126145 }
146+
147+ return false ;
148+ }
149+
150+ let signInRelocateWarned = false ;
151+
152+ function runSignInRelocationWithRetries ( ) {
153+ if ( typeof document === "undefined" || ! document . querySelector ( ".docs-layout" ) ) {
154+ return ;
155+ }
156+
157+ const tryOnce = ( attempt : number ) => {
158+ if ( moveSignInToUtilityArea ( ) ) {
159+ return ;
160+ }
161+ if ( attempt < 40 ) {
162+ window . setTimeout ( ( ) => tryOnce ( attempt + 1 ) , 75 ) ;
163+ } else if ( ! signInRelocateWarned ) {
164+ const link = document . querySelector ( '.docs-layout header a.VPLink[href*="sign-in"]' ) ;
165+ if ( link && ! link . classList . contains ( "sign-in-relocated" ) ) {
166+ signInRelocateWarned = true ;
167+ console . warn (
168+ "[plane-docs] Sign-in could not be relocated (header not ready or selectors changed)." ,
169+ ) ;
170+ }
171+ }
172+ } ;
173+
174+ tryOnce ( 0 ) ;
175+ }
176+
177+ function debounce ( fn : ( ) => void , ms : number ) {
178+ let t : ReturnType < typeof setTimeout > | undefined ;
179+ return ( ) => {
180+ if ( t ) clearTimeout ( t ) ;
181+ t = setTimeout ( fn , ms ) ;
182+ } ;
127183}
128184
129185export default {
130186 extends : VoidZeroTheme ,
131187 Layout,
132188 enhanceApp ( ctx ) {
189+ VoidZeroTheme . enhanceApp ?.( ctx ) ;
133190 ctx . app . provide ( themeContextKey , planeThemeContext ) ;
134- VoidZeroTheme . enhanceApp ( ctx ) ;
135191 enhanceAppWithTabs ( ctx . app ) ;
136192 ctx . app . component ( "Card" , Card ) ;
137193 ctx . app . component ( "CardGroup" , CardGroup ) ;
@@ -146,20 +202,59 @@ export default {
146202 background : "rgba(0, 0, 0, 0.8)" ,
147203 } ) ;
148204
205+ let headerObserver : MutationObserver | null = null ;
206+ let onResize : ( ( ) => void ) | null = null ;
207+
149208 onMounted ( ( ) => {
150209 // Delay tab hash handling to ensure tabs are rendered
151210 setTimeout ( ( ) => {
152211 handleTabHash ( ) ;
153212 setupTabHashUpdates ( ) ;
154- moveSignInToUtilityArea ( ) ;
213+ runSignInRelocationWithRetries ( ) ;
214+ } , 100 ) ;
215+
216+ const onHeaderMutations = debounce ( ( ) => {
217+ runSignInRelocationWithRetries ( ) ;
155218 } , 100 ) ;
156219
220+ const tryAttachHeaderObserver = ( ) => {
221+ if ( headerObserver ) return ;
222+ const h = document . querySelector ( ".docs-layout header" ) ;
223+ if ( ! h ) return ;
224+ headerObserver = new MutationObserver ( onHeaderMutations ) ;
225+ headerObserver . observe ( h , { childList : true , subtree : true } ) ;
226+ } ;
227+ tryAttachHeaderObserver ( ) ;
228+ if ( ! headerObserver ) {
229+ const id = window . setInterval ( ( ) => {
230+ tryAttachHeaderObserver ( ) ;
231+ if ( headerObserver ) {
232+ clearInterval ( id ) ;
233+ }
234+ } , 120 ) ;
235+ window . setTimeout ( ( ) => clearInterval ( id ) , 5000 ) ;
236+ }
237+
238+ onResize = debounce ( ( ) => {
239+ runSignInRelocationWithRetries ( ) ;
240+ } , 150 ) ;
241+ window . addEventListener ( "resize" , onResize ) ;
242+
157243 // Listen for hash changes
158244 window . addEventListener ( "hashchange" , ( ) => {
159245 nextTick ( handleTabHash ) ;
160246 } ) ;
161247 } ) ;
162248
249+ onUnmounted ( ( ) => {
250+ headerObserver ?. disconnect ( ) ;
251+ headerObserver = null ;
252+ if ( onResize ) {
253+ window . removeEventListener ( "resize" , onResize ) ;
254+ onResize = null ;
255+ }
256+ } ) ;
257+
163258 // Watch for route changes
164259 watch (
165260 ( ) => route . path ,
@@ -169,7 +264,7 @@ export default {
169264 zoom . attach ( ":not(a) > img:not(.VPImage)" ) ;
170265 handleTabHash ( ) ;
171266 setupTabHashUpdates ( ) ;
172- moveSignInToUtilityArea ( ) ;
267+ runSignInRelocationWithRetries ( ) ;
173268 } ) ;
174269 } ,
175270 ) ;
0 commit comments