@@ -17,9 +17,13 @@ import type {
1717 ClientSiteSections ,
1818} from './encodeClientSiteSections' ;
1919
20+ const DESKTOP_BREAKPOINT = 768 ;
2021const SCREEN_OFFSET = 16 ; // 1rem
2122const MAX_ITEMS_PER_COLUMN = 10 ; // number of items per column
22- const GROUP_MASONRY_THRESHOLD = 6 ; // if a section group has more than this many child groups, it will be shown in a masonry grid
23+ const GROUP_MASONRY_THRESHOLD = 3 ; // if a section group has more than this many child groups, it will be shown in a masonry grid
24+ const COLUMN_WIDTH = '18rem' ;
25+ const COLUMN_GAP = '2rem' ;
26+ const MAX_MASONRY_COLUMNS = 4 ;
2327
2428/**
2529 * A set of navigational links representing site sections for multi-section sites
@@ -37,24 +41,19 @@ export function SiteSectionTabs(props: {
3741
3842 const containerRef = React . useRef < HTMLDivElement > ( null ) ;
3943 const currentTriggerRef = React . useRef < HTMLButtonElement | null > ( null ) ;
40- const [ offset , setOffset ] = React . useState < number | null > ( null ) ;
4144 const [ value , setValue ] = React . useState < string | undefined > ( ) ;
4245
43- const isMobile = useIsMobile ( 768 ) ;
44-
45- React . useEffect ( ( ) => {
46- const trigger = currentTriggerRef . current ;
47- const container = containerRef . current ;
48- if ( ! value || ! trigger || ! container ) {
49- return ;
50- }
51-
52- const triggerWidth = trigger . getBoundingClientRect ( ) . width - SCREEN_OFFSET ;
53- const triggerLeft =
54- trigger . getBoundingClientRect ( ) . left -
55- ( window . innerWidth - container . getBoundingClientRect ( ) . width ) / 2 ;
56- setOffset ( triggerLeft + triggerWidth / 2 ) ;
57- } , [ value ] ) ;
46+ const isMobile = useIsMobile ( DESKTOP_BREAKPOINT ) ;
47+ const offset = useNavigationMenuViewportOffset ( {
48+ value,
49+ isMobile,
50+ triggerRef : currentTriggerRef ,
51+ containerRef,
52+ } ) ;
53+ const viewportLeft =
54+ ! isMobile && offset !== null
55+ ? `clamp(0px, calc(${ offset - SCREEN_OFFSET } px - var(--radix-navigation-menu-viewport-width, 0px)/2), calc(100% - var(--radix-navigation-menu-viewport-width, 0px)))`
56+ : '0px' ;
5857
5958 return structure . length > 0 ? (
6059 < NavigationMenu . Root
@@ -65,6 +64,12 @@ export function SiteSectionTabs(props: {
6564 className
6665 ) }
6766 ref = { containerRef }
67+ style = {
68+ {
69+ '--site-section-column-width' : COLUMN_WIDTH ,
70+ '--site-section-column-gap' : COLUMN_GAP ,
71+ } as React . CSSProperties
72+ }
6873 value = { value }
6974 onValueChange = { setValue }
7075 skipDelayDuration = { 500 }
@@ -122,11 +127,22 @@ export function SiteSectionTabs(props: {
122127 icon = { icon as IconName }
123128 />
124129 </ NavigationMenu . Trigger >
125- < NavigationMenu . Content >
126- < SectionGroupTileList
127- items = { structureItem . children }
128- currentSection = { currentSection }
129- />
130+ < NavigationMenu . Content
131+ className = { tcls ( [
132+ 'absolute top-0 left-0 w-full md:w-auto' ,
133+ 'data-[motion=from-start]:*:animate-[enterFromLeft_300ms_ease_both] data-[motion=to-end]:*:animate-[exitToRight_300ms_ease_both] data-[motion=to-start]:*:animate-[exitToLeft_300ms_ease_both] motion-safe:data-[motion=from-end]:*:animate-[enterFromRight_300ms_ease_both]' ,
134+ ] ) }
135+ >
136+ < div
137+ className = { tcls (
138+ 'max-h-[calc(100vh-8rem)] w-full overflow-y-auto overflow-x-hidden'
139+ ) }
140+ >
141+ < SectionGroupTileList
142+ items = { structureItem . children }
143+ currentSection = { currentSection }
144+ />
145+ </ div >
130146 </ NavigationMenu . Content >
131147 </ >
132148 ) : (
@@ -153,30 +169,60 @@ export function SiteSectionTabs(props: {
153169
154170 < div
155171 className = "absolute top-full left-0 z-20 flex w-full"
156- style = { {
157- padding : `0 ${ SCREEN_OFFSET } px 0 ${ SCREEN_OFFSET } px` ,
158- } }
172+ style = { { paddingInline : `${ SCREEN_OFFSET } px` } }
159173 >
160174 < NavigationMenu . Viewport
161175 className = { tcls (
162- 'relative origin-top overflow-auto circular-corners:rounded-3xl rounded-corners:rounded-xl border border-tint bg-tint-base shadow-lg ease-in-out ' ,
163- '-mt-0.5 w-full md:w-max ' ,
176+ 'relative origin-[center_top] overflow-hidden circular-corners:rounded-3xl rounded-corners:rounded-xl border border-tint bg-tint-base shadow-lg' ,
177+ '-mt-0.5 h-(--radix-navigation-menu-viewport-height) w-full max-w-full md:w-(--radix-navigation-menu-viewport-width) ' ,
164178 'max-h-[calc(100vh-8rem)] data-[state=closed]:animate-scale-out data-[state=open]:animate-scale-in' ,
165- "[&:not([style*='--radix-navigation-menu-viewport-width'])]:hidden" // The viewport width is only calculated once it's triggered, and can take a while. We hide the viewport until it's ready.
179+ 'ease has-[&[data-motion]]:transition-[left,width,height] has-[&[data-motion]]:duration-300'
166180 ) }
167181 style = { {
168- translate :
169- ! isMobile && offset
170- ? `clamp(0px, calc(${ offset } px - var(--radix-navigation-menu-viewport-width, 0px)/2), calc(100vw - var(--radix-navigation-menu-viewport-width, 0px) - ${ SCREEN_OFFSET * 3 } px)) 0 0`
171- : '0 0 0' , // TranslateZ is needed to force a stacking context, fixing a rendering bug on Safari
172- display : offset === null ? 'none' : undefined ,
182+ left : viewportLeft ,
183+ translate : '0 0 0' , // TranslateZ is needed to force a stacking context, fixing a rendering bug on Safari
173184 } }
174185 />
175186 </ div >
176187 </ NavigationMenu . Root >
177188 ) : null ;
178189}
179190
191+ function useNavigationMenuViewportOffset ( args : {
192+ value : string | undefined ;
193+ isMobile : boolean ;
194+ triggerRef : React . RefObject < HTMLButtonElement | null > ;
195+ containerRef : React . RefObject < HTMLDivElement | null > ;
196+ } ) {
197+ const { value, isMobile, triggerRef, containerRef } = args ;
198+ const [ offset , setOffset ] = React . useState < number | null > ( null ) ;
199+
200+ React . useLayoutEffect ( ( ) => {
201+ if ( isMobile ) {
202+ setOffset ( null ) ;
203+ return ;
204+ }
205+
206+ if ( ! value ) {
207+ return ;
208+ }
209+
210+ const trigger = triggerRef . current ;
211+ const container = containerRef . current ;
212+ if ( ! trigger || ! container ) {
213+ return ;
214+ }
215+
216+ const containerLeft = container . getBoundingClientRect ( ) . left ;
217+ const triggerWidth = trigger . getBoundingClientRect ( ) . width ;
218+ const triggerLeft = trigger . getBoundingClientRect ( ) . left - containerLeft ;
219+
220+ setOffset ( triggerLeft + triggerWidth / 2 ) ;
221+ } , [ containerRef , isMobile , triggerRef , value ] ) ;
222+
223+ return offset ;
224+ }
225+
180226/**
181227 * A tab representing a section or section group
182228 */
@@ -222,14 +268,16 @@ function SectionGroupTileList(props: {
222268
223269 const hasSections = sections . length > 0 ;
224270 const hasGroups = groups . length > 0 ;
271+ const isMasonryLayout = groups . length > GROUP_MASONRY_THRESHOLD ;
272+ const masonryColumnCount = Math . min ( Math . ceil ( groups . length / 2 ) , MAX_MASONRY_COLUMNS ) ;
225273
226274 return (
227- < div className = "flex flex-col md:flex-row" >
275+ < div className = "flex w-full flex-col md:flex-row" >
228276 { /* Non-grouped sections */ }
229277 { hasSections && (
230278 < ul
231279 className = { tcls (
232- 'flex min-w-48 shrink-0 grid-flow-row flex-col gap-x-2 gap-y-1 self-start p-3 md:grid' ,
280+ 'flex w-full shrink-0 grid-flow-row flex-col gap-x-2 gap-y-0.5 self-stretch p-3 md:sticky md:top-0 md: grid md:w-max md:self-start ' ,
233281 hasGroups ? 'bg-tint-base' : ''
234282 ) }
235283 style = { {
@@ -248,25 +296,38 @@ function SectionGroupTileList(props: {
248296
249297 { /* Grouped sections */ }
250298 { hasGroups && (
251- < ul
299+ < div
252300 className = { tcls (
253- 'grow gap-x-8 space-y-8 p-3' ,
254- groups . length > GROUP_MASONRY_THRESHOLD
255- ? 'w-screen md:columns-[15rem]'
256- : 'flex flex-col justify-start md:flex-row md:items-start' ,
301+ 'w-full md:w-max md:min-w-0 md:max-w-full' ,
257302 hasSections
258303 ? 'border-tint-subtle bg-tint-subtle max-md:border-t md:border-l'
259304 : ''
260305 ) }
261306 >
262- { groups . map ( ( group ) => (
263- < SectionGroupTile
264- key = { group . id }
265- child = { group }
266- currentSection = { currentSection }
267- />
268- ) ) }
269- </ ul >
307+ < ul
308+ className = { tcls (
309+ 'p-3' ,
310+ isMasonryLayout
311+ ? 'w-full max-md:space-y-8 md:w-max md:max-w-full md:gap-x-[var(--site-section-column-gap)] md:[column-count:var(--masonry-columns)] md:[&>li]:mb-4'
312+ : 'flex w-full flex-col justify-start space-y-8 md:w-max md:flex-row md:items-start md:gap-[var(--site-section-column-gap)] md:space-y-0'
313+ ) }
314+ style = {
315+ isMasonryLayout
316+ ? ( {
317+ '--masonry-columns' : String ( masonryColumnCount ) ,
318+ } as React . CSSProperties )
319+ : undefined
320+ }
321+ >
322+ { groups . map ( ( group ) => (
323+ < SectionGroupTile
324+ key = { group . id }
325+ child = { group }
326+ currentSection = { currentSection }
327+ />
328+ ) ) }
329+ </ ul >
330+ </ div >
270331 ) }
271332 </ div >
272333 ) ;
@@ -278,38 +339,40 @@ function SectionGroupTileList(props: {
278339function SectionGroupTile ( props : {
279340 child : ClientSiteSection | ClientSiteSectionGroup ;
280341 currentSection : ClientSiteSection ;
342+ invertIcon ?: boolean ;
281343} ) {
282- const { child, currentSection } = props ;
344+ const { child, currentSection, invertIcon } = props ;
283345
284346 if ( child . object === 'site-section' ) {
285347 const { url, icon, title, description } = child ;
286348 const isActive = child . id === currentSection . id ;
287349 return (
288- < li className = "group/section-tile flex shrink-0 grow md:max-w-68 " >
350+ < li className = "group/section-tile flex w-full min-w-0 shrink-0 grow md:max-w-[var(--site-section-column-width)] " >
289351 < Link
290352 href = { url }
291353 className = { tcls (
292- 'grow circular-corners:rounded-2xl rounded-corners:rounded-lg px-3 py-2 transition-colors' ,
354+ 'grow circular-corners:rounded-2xl rounded-corners:rounded-lg px-2.5 py-1.5 transition-colors' ,
293355 isActive
294356 ? 'bg-primary-active text-primary-strong'
295357 : 'text-tint-strong hover:bg-tint-hover'
296358 ) }
297359 >
298- < div className = "mb-auto flex grow items-center gap-2" >
360+ < div className = "mb-auto flex min-w-0 grow items-center gap-2" >
299361 { icon && (
300362 < div
301363 className = { tcls (
302364 '-ml-1 self-start circular-corners:rounded-2xl rounded-corners:rounded-lg p-2 transition-colors' ,
365+ isActive || invertIcon ? 'bg-primary-base' : 'bg-tint' ,
303366 isActive
304- ? 'bg-primary-base text-primary-subtle'
305- : 'bg-tint text-tint-strong group-hover/section-tile:bg-tint-base'
367+ ? 'text-primary-subtle'
368+ : 'text-tint-strong group-hover/section-tile:bg-tint-base'
306369 ) }
307370 >
308371 < SectionIcon isActive = { isActive } icon = { icon as IconName } />
309372 </ div >
310373 ) }
311- < div className = "flex flex-col gap-1 " >
312- { title }
374+ < div className = "flex min-w-0 flex-col gap-0.5 " >
375+ < span className = "block min-w-0 whitespace-normal" > { title } </ span >
313376 { description && (
314377 < p className = { isActive ? 'text-primary' : 'text-tint' } >
315378 { description }
@@ -326,15 +389,15 @@ function SectionGroupTile(props: {
326389 const { title, icon, children } = child ;
327390
328391 return (
329- < li className = "flex shrink-0 break-inside-avoid flex-col gap-1" >
330- < div className = "mt-3 mb-1 flex gap-2.5 px-3 font-semibold text-tint-subtle text-xs uppercase tracking-wider " >
392+ < li className = "flex w-full min-w-0 shrink-0 break-inside-avoid flex-col gap-1 md:w-auto " >
393+ < div className = "mt-2 mb-1 flex min-w-0 gap-2 px-2.5 font-semibold text-tint-subtle text-xs" >
331394 { icon && (
332395 < SectionIcon className = "mt-0.5" isActive = { false } icon = { icon as IconName } />
333396 ) }
334- { title }
397+ < span className = "min-w-0 flex-1 whitespace-normal" > { title } </ span >
335398 </ div >
336399 < ul
337- className = "flex grid-flow-row flex-col gap-x-2 gap-y-1 md:grid"
400+ className = "flex w-full grid-flow-row flex-col gap-x-2 gap-y-0.5 md:grid"
338401 style = { {
339402 gridTemplateColumns : `repeat(${ Math . ceil ( children . length / MAX_ITEMS_PER_COLUMN ) } , minmax(0, auto))` ,
340403 } }
@@ -344,6 +407,7 @@ function SectionGroupTile(props: {
344407 key = { nestedChild . id }
345408 child = { nestedChild }
346409 currentSection = { currentSection }
410+ invertIcon = { true }
347411 />
348412 ) ) }
349413 </ ul >
0 commit comments