Skip to content

Commit 244a200

Browse files
committed
Fix gap in theme picker to header positioning
1 parent 0378252 commit 244a200

12 files changed

Lines changed: 79 additions & 23 deletions

File tree

src/components/Header/client/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { handleScriptError } from '@components/scripts/errors/handler'
77
import { isType1Element } from '@components/scripts/assertions/elements'
88
import { updateLayoutOffsets } from '@components/scripts/store'
9+
import { getScrollViewportElement } from '@components/scripts/store/selectors'
910
import { animateCollapse, animateExpand, isHeaderAnimating } from './headerAnimation'
1011
import { getHeaderElement, getHeaderShellElement } from './selectors'
1112

@@ -61,14 +62,17 @@ const hasScrollTop = (element: unknown): element is HTMLElement => {
6162
}
6263

6364
// Use the event target when scrolling happens inside a container.
65+
// Also checks #scroll-viewport for cases where no event is available (e.g. resize).
6466
const getScrollTop = (event?: Event): number => {
6567
const target = event?.target
6668
const targetScrollTop =
6769
hasScrollTop(target) ? target.scrollTop : 0
6870
const documentScrollTop = getDocumentScrollTop()
6971
const windowScrollTop = typeof window.scrollY === 'number' ? window.scrollY : 0
72+
const viewport = getScrollViewportElement()
73+
const viewportScrollTop = viewport ? viewport.scrollTop : 0
7074

71-
return Math.max(targetScrollTop, documentScrollTop, windowScrollTop)
75+
return Math.max(targetScrollTop, documentScrollTop, windowScrollTop, viewportScrollTop)
7276
}
7377

7478
const shouldForceExpanded = (header: HTMLElement): boolean => {

src/components/Header/index.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
--header-brand-scale-current: var(--header-brand-scale);
5454
--header-nav-font-size-current: var(--header-nav-font-size);
5555

56+
flex-shrink: 0;
5657
position: relative;
5758
}
5859

src/components/Layout/Container/index.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const { id } = Astro.props
1010
---
1111

1212
<div
13-
class="flex flex-col min-h-screen relative transition-transform duration-300 ease-[cubic-bezier(0.25,0.46,0.45,0.94)]
13+
class="flex flex-col flex-1 min-h-0 overflow-hidden relative transition-transform duration-300 ease-[cubic-bezier(0.25,0.46,0.45,0.94)]
1414
[&_#header]:mx-auto [&_#header]:max-w-300 [&_#header]:w-[90%]
1515
[&_#main]:mx-auto [&_#main]:max-w-300 [&_#main]:w-[90%]
1616
[&_#breadcrumbs]:mx-auto [&_#breadcrumbs]:max-w-300 [&_#breadcrumbs]:w-[90%]

src/components/Layout/Content/index.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<main
88
id="main"
9-
class="flex flex-col flex-[0_1_auto]"
9+
class="flex flex-col flex-1"
1010
role="main"
1111
tabindex="-1"
1212
>

src/components/Pages/Contact/index.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ const { content, prefilledProjectType = '' } = Astro.props
406406
{/** Contact Information */}
407407
<div
408408
class="bg-content-inverse p-6 md:p-8 rounded-2xl shadow-lg h-fit sticky"
409-
style="top: calc(var(--layout-top-offset, 0px) + 1rem);"
409+
style="top: 1rem;"
410410
>
411411
<div class="space-y-8">
412412
<div>

src/components/ThemePicker/client/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,14 @@ export class ThemePickerElement extends LitElement {
724724
requestAnimationFrame(() => {
725725
this.pickerModal.classList.add(CLASSES.isOpen)
726726
updateLayoutOffsets()
727+
728+
// Re-measure after the max-height transition completes so the
729+
// offset is perfectly accurate once the panel settles.
730+
this.pickerModal.addEventListener(
731+
'transitionend',
732+
() => updateLayoutOffsets(),
733+
{ once: true },
734+
)
727735
})
728736
} else {
729737
this.pickerModal.classList.add(CLASSES.isOpen)

src/components/ThemePicker/themePicker.module.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* Reserve layout space for the theme picker when open. */
22
:global(theme-picker) {
33
display: block;
4+
flex-shrink: 0;
45
height: var(--theme-picker-offset, 0);
56
position: relative;
67
z-index: var(--z-theme-picker);

src/components/Toc/index.astro

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ const tocTree = buildTocTree(items)
2222
data-toc-toggle
2323
aria-controls="toc-drawer"
2424
aria-label="Open table of contents"
25-
class="fixed right-0 top-1/2 -translate-y-1/2 z-(--z-content-floating) flex h-16 w-9 items-center justify-center rounded-l-full border border-r-0 border-trim bg-page-base shadow-elevated transition-opacity hover:shadow-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary lg:hidden"
25+
class="fixed right-0 top-1/2 -translate-y-1/2 z-(--z-content-floating) flex h-16 w-9 items-center justify-center rounded-l-full border border-r-0 border-trim bg-page-base transition-opacity hover:shadow-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary md:hidden"
2626
>
27-
<Icon icon="arrow-left" size={18} />
27+
<Icon icon="arrow-left" size={6} />
2828
</button>
2929

3030
{/* Full-screen scrim overlay */}
@@ -33,7 +33,7 @@ const tocTree = buildTocTree(items)
3333
data-toc-overlay
3434
data-visible="false"
3535
aria-label="Close table of contents"
36-
class="fixed inset-0 z-(--z-content-scrim) bg-surface/80 opacity-0 backdrop-blur-sm pointer-events-none transition-opacity duration-300 data-[visible=true]:opacity-100 data-[visible=true]:pointer-events-auto lg:hidden"
36+
class="fixed inset-0 z-(--z-content-scrim) bg-surface/80 opacity-0 backdrop-blur-sm pointer-events-none transition-opacity duration-300 data-[visible=true]:opacity-100 data-[visible=true]:pointer-events-auto md:hidden"
3737
></button>
3838

3939
{/* Right-side drawer panel */}
@@ -58,7 +58,7 @@ const tocTree = buildTocTree(items)
5858

5959
<aside
6060
class="px-5 py-6 lg:sticky lg:rounded-lg lg:border lg:border-trim lg:bg-page-base lg:shadow-none"
61-
style="top: calc(var(--layout-top-offset, 0px) + 1rem);"
61+
style="top: 1rem;"
6262
aria-labelledby="toc-heading"
6363
>
6464
{/* Desktop heading — hidden on mobile where the drawer header shows instead */}

src/components/scripts/store/layoutPosition.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,25 @@ function measureThemePickerHeight(): number {
8989
const modal = getThemePickerModalElement()
9090
if (!modal || !modal.classList.contains(IS_OPEN_CLASS)) return 0
9191

92+
// Prefer the live rendered height (accurate once the CSS transition ends).
9293
const rect = modal.getBoundingClientRect()
9394
if (rect.height > 0) return rect.height
9495

95-
// Fallback: compute the CSS 14em target when mid-transition
96-
const computed = window.getComputedStyle(modal)
97-
const maxH = Number.parseFloat(computed.maxHeight)
98-
if (Number.isFinite(maxH) && maxH > 0) return maxH
96+
// During the opening frame the max-height transition starts at 0, so
97+
// getBoundingClientRect().height is 0. scrollHeight gives the intrinsic
98+
// content height regardless of max-height / overflow constraints. We cap
99+
// it at the CSS target max-height (14em) so we never overshoot.
100+
const scrollH = modal.scrollHeight
101+
if (scrollH > 0) {
102+
const modalFontSize =
103+
Number.parseFloat(getComputedStyle(modal).fontSize) ||
104+
Number.parseFloat(getComputedStyle(document.documentElement).fontSize) ||
105+
16
106+
const maxTargetHeight = 14 * modalFontSize
107+
return Math.min(scrollH, maxTargetHeight)
108+
}
99109

100-
// Last resort
110+
// Last resort: the CSS 14em target.
101111
const fontSize = Number.parseFloat(
102112
getComputedStyle(document.documentElement).fontSize,
103113
) || 16

src/components/scripts/store/selectors.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
* Type-safe HTML element selectors for the layout position store.
33
*
44
* These selectors are used by the layout measurement functions to find
5-
* the ThemePicker modal, the fixed header, and the progress bar.
5+
* the ThemePicker modal, the fixed header, the progress bar, and the
6+
* scroll viewport container.
67
*/
78

89
export const LAYOUT_SELECTORS = {
910
themePickerModal: '[data-theme-modal]',
1011
headerFixed: '.header-fixed',
1112
progressBar: '[data-progress-bar]',
13+
scrollViewport: '#scroll-viewport',
1214
} as const
1315

1416
/**
@@ -31,3 +33,10 @@ export function getHeaderFixedElement(): HTMLElement | null {
3133
export function getProgressBarElement(): HTMLElement | null {
3234
return document.querySelector<HTMLElement>(LAYOUT_SELECTORS.progressBar)
3335
}
36+
37+
/**
38+
* Query the scroll viewport container.
39+
*/
40+
export function getScrollViewportElement(): HTMLElement | null {
41+
return document.querySelector<HTMLElement>(LAYOUT_SELECTORS.scrollViewport)
42+
}

0 commit comments

Comments
 (0)