1
+ import { useState , useRef , useEffect } from 'react'
2
+ import { createPortal } from 'react-dom'
1
3
import { useSegmentTree , type SegmentTrieNode } from '../../segment-explorer'
2
4
import { css } from '../../utils/css'
3
5
import { cx } from '../../utils/cx'
@@ -125,6 +127,7 @@ function PageSegmentTreeLayerPresentation({
125
127
< span
126
128
key = { fileChildSegment }
127
129
onClick = { ( ) => {
130
+ if ( isBuiltin ) return
128
131
openInEditor ( { filePath } )
129
132
} }
130
133
className = { cx (
@@ -136,6 +139,7 @@ function PageSegmentTreeLayerPresentation({
136
139
{ fileName }
137
140
{ isBuiltin && (
138
141
< TooltipSpan
142
+ direction = "right"
139
143
title = { `The default Next.js not found is being shown. You can customize this page by adding your own ${ fileName } file to the app/ directory.` }
140
144
>
141
145
< InfoIcon />
@@ -175,6 +179,102 @@ function PageSegmentTreeLayerPresentation({
175
179
)
176
180
}
177
181
182
+ const tooltipStyles = `
183
+ .tooltip-wrapper {
184
+ position: relative;
185
+ display: inline-block;
186
+ }
187
+
188
+ .tooltip {
189
+ position: absolute;
190
+ background: var(--color-gray-1000);
191
+ color: var(--color-gray-100);
192
+ padding: 6px 12px;
193
+ border-radius: 8px;
194
+ font-size: 14px;
195
+ line-height: 1.4;
196
+ white-space: nowrap;
197
+ min-width: 200px;
198
+ white-space: normal;
199
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
200
+ pointer-events: none;
201
+ }
202
+
203
+ .tooltip-arrow {
204
+ position: absolute;
205
+ width: 0;
206
+ height: 0;
207
+ }
208
+
209
+ /* Top direction */
210
+ .tooltip--top {
211
+ bottom: 100%;
212
+ left: 50%;
213
+ transform: translateX(-50%);
214
+ margin-bottom: 8px;
215
+ }
216
+
217
+ .tooltip-arrow--top {
218
+ top: 100%;
219
+ left: 50%;
220
+ transform: translateX(-50%);
221
+ border-left: 6px solid transparent;
222
+ border-right: 6px solid transparent;
223
+ border-top: 6px solid var(--color-gray-1000);
224
+ }
225
+
226
+ /* Bottom direction */
227
+ .tooltip--bottom {
228
+ top: 100%;
229
+ left: 50%;
230
+ transform: translateX(-50%);
231
+ margin-top: 8px;
232
+ }
233
+
234
+ .tooltip-arrow--bottom {
235
+ bottom: 100%;
236
+ left: 50%;
237
+ transform: translateX(-50%);
238
+ border-left: 6px solid transparent;
239
+ border-right: 6px solid transparent;
240
+ border-bottom: 6px solid var(--color-gray-1000);
241
+ }
242
+
243
+ /* Left direction */
244
+ .tooltip--left {
245
+ right: 100%;
246
+ top: 50%;
247
+ transform: translateY(-50%);
248
+ margin-right: 8px;
249
+ }
250
+
251
+ .tooltip-arrow--left {
252
+ left: 100%;
253
+ top: 50%;
254
+ transform: translateY(-50%);
255
+ border-top: 6px solid transparent;
256
+ border-bottom: 6px solid transparent;
257
+ border-left: 6px solid var(--color-gray-1000);
258
+ }
259
+
260
+ /* Right direction */
261
+ .tooltip--right {
262
+ left: 100%;
263
+ top: 50%;
264
+ transform: translateY(-50%);
265
+ margin-left: 8px;
266
+ }
267
+
268
+ .tooltip-arrow--right {
269
+ right: 100%;
270
+ top: 50%;
271
+ transform: translateY(-50%);
272
+ border-top: 6px solid transparent;
273
+ border-bottom: 6px solid transparent;
274
+ border-right: 6px solid var(--color-gray-1000);
275
+ }
276
+ `
277
+
178
278
export const DEV_TOOLS_INFO_RENDER_FILES_STYLES = css `
179
279
.segment-explorer-content {
180
280
font-size : var (--size-14 );
@@ -268,12 +368,15 @@ export const DEV_TOOLS_INFO_RENDER_FILES_STYLES = css`
268
368
background-color : transparent;
269
369
color : var (--color-gray-900 );
270
370
border : 1px dashed var (--color-gray-500 );
371
+ cursor : default;
271
372
}
272
373
273
374
.segment-explorer-file-label--builtin svg {
274
375
margin-left : 4px ;
275
376
margin-right : -4px ;
276
377
}
378
+
379
+ ${ tooltipStyles }
277
380
`
278
381
279
382
function openInEditor ( { filePath } : { filePath : string } ) {
@@ -312,12 +415,88 @@ function InfoIcon(props: React.SVGProps<SVGSVGElement>) {
312
415
)
313
416
}
314
417
418
+ type TooltipDirection = 'top' | 'bottom' | 'left' | 'right'
419
+
315
420
function TooltipSpan ( {
316
421
children,
317
422
title,
423
+ direction = 'top' ,
318
424
} : {
319
425
children : React . ReactNode
320
426
title : string
427
+ direction : TooltipDirection
321
428
} ) {
322
- return < span title = { title } > { children } </ span >
429
+ const [ isVisible , setIsVisible ] = useState ( false )
430
+ const [ position , setPosition ] = useState ( { top : 0 , left : 0 } )
431
+ const wrapperRef = useRef < HTMLSpanElement > ( null )
432
+
433
+ useEffect ( ( ) => {
434
+ if ( isVisible && wrapperRef . current ) {
435
+ const rect = wrapperRef . current . getBoundingClientRect ( )
436
+ const scrollTop = window . scrollY || document . documentElement . scrollTop
437
+ const scrollLeft = window . scrollX || document . documentElement . scrollLeft
438
+
439
+ setPosition ( {
440
+ top : rect . top + scrollTop ,
441
+ left : rect . left + scrollLeft ,
442
+ } )
443
+ }
444
+ } , [ isVisible ] )
445
+
446
+ const handleMouseEnter = ( ) => {
447
+ setIsVisible ( true )
448
+ }
449
+
450
+ const handleMouseLeave = ( ) => {
451
+ setIsVisible ( false )
452
+ }
453
+
454
+ const tooltip = isVisible ? (
455
+ < div
456
+ className = "custom-tooltip-portal"
457
+ style = { {
458
+ position : 'absolute' ,
459
+ top : position . top ,
460
+ left : position . left ,
461
+ width : wrapperRef . current ?. offsetWidth || 0 ,
462
+ height : wrapperRef . current ?. offsetHeight || 0 ,
463
+ pointerEvents : 'none' ,
464
+ zIndex : 99999 ,
465
+ } }
466
+ >
467
+ < div className = { cx ( 'custom-tooltip' , `custom-tooltip--${ direction } ` ) } >
468
+ { title }
469
+ < div
470
+ className = { cx (
471
+ 'custom-tooltip-arrow' ,
472
+ `custom-tooltip-arrow--${ direction } `
473
+ ) }
474
+ />
475
+ </ div >
476
+ </ div >
477
+ ) : null
478
+
479
+ const [ shadowRootRef ] = useState < ShadowRoot | null > ( ( ) => {
480
+ const portal = document . querySelector ( 'nextjs-portal' )
481
+ if ( ! portal ) return null
482
+ return portal . shadowRoot as ShadowRoot
483
+ } )
484
+
485
+ if ( ! shadowRootRef ) return null
486
+
487
+ return (
488
+ < >
489
+ < span
490
+ ref = { wrapperRef }
491
+ className = "tooltip-wrapper"
492
+ onMouseEnter = { handleMouseEnter }
493
+ onMouseLeave = { handleMouseLeave }
494
+ >
495
+ { children }
496
+ </ span >
497
+ { typeof document !== 'undefined' &&
498
+ tooltip &&
499
+ createPortal ( tooltip , shadowRootRef ) }
500
+ </ >
501
+ )
323
502
}
0 commit comments