Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions gatsby-ssr.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
import { wrapPageElement } from './gatsby-shared'
import {
ALERT_DISMISSED_ATTRIBUTE,
ALERT_DISMISSED_STORAGE_KEY
} from './src/components/LayoutHeader/Alert/state'

const alertInitScript = `
try {
if (localStorage.getItem(${JSON.stringify(
ALERT_DISMISSED_STORAGE_KEY
)}) === 'true') {
document.body.setAttribute(${JSON.stringify(
ALERT_DISMISSED_ATTRIBUTE
)}, '');
}
} catch(e) {}
`

const themeInitScript = `
void function() {
Expand Down Expand Up @@ -67,6 +83,12 @@ export const onRenderBody = ({ setHeadComponents, setPreBodyComponents }) => {
))
)
setPreBodyComponents([
<script
key="layout-alert-init"
id="layout-alert-init"
data-uc-allowed="true"
dangerouslySetInnerHTML={{ __html: alertInitScript }}
/>,
<script
key="theme-mode-init"
id="theme-mode-init"
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@
"prismjs": "1.30.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-intersection-observer": "10.0.3",
"rehype-parse": "9.0.1",
"rehype-stringify": "10.0.1",
"remark-html": "16.0.1",
Expand Down
5 changes: 4 additions & 1 deletion src/components/Documentation/Layout/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,10 @@
}

.innerSidebar {
@apply flex flex-col sticky top-0 max-h-screen z-20 pt-14;
@apply flex flex-col sticky z-20 pt-14;

top: var(--layout-alert-height);
max-height: calc(100vh - var(--layout-alert-height));
}

.backdrop {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const ARROW_SIZE = 10
const VIEWPORT_PADDING = 16

const getNavbarBottom = (): number => {
const header = document.querySelector('header[data-collapsed], #header')
const header = document.getElementById('header')

return header?.getBoundingClientRect().bottom ?? 0
}
Expand Down
28 changes: 12 additions & 16 deletions src/components/Documentation/RightPanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,11 @@ const RightPanel: React.FC<IRightPanelProps> = ({ headings, githubLink }) => {
animatingRef.current = false

// A heading is "active" once it passes the visible top boundary.
// scroll-padding-top accounts for the fixed header; the extra
// buffer covers scroll-margin on headings and minor spacing.
let baseOffset =
(parseInt(
getComputedStyle(document.documentElement).scrollPaddingTop,
10
) || 0) + 30
// The extra buffer covers the anchor margin and minor spacing.
const getBaseOffset = (): number =>
(document.getElementById('header')?.getBoundingClientRect().bottom ??
56) + 30
let baseOffset = getBaseOffset()

const update = (): void => {
if (animatingRef.current) return
Expand Down Expand Up @@ -63,23 +61,21 @@ const RightPanel: React.FC<IRightPanelProps> = ({ headings, githubLink }) => {
})
}

const onResize = (): void => {
baseOffset =
(parseInt(
getComputedStyle(document.documentElement).scrollPaddingTop,
10
) || 0) + 30
const onLayoutResize = (): void => {
baseOffset = getBaseOffset()
scheduleUpdate()
}

// Detect layout shifts from <details>, images, etc. that don't
// fire scroll or resize events but still move headings.
const markdownRoot = document.getElementById('markdown-root')
const resizeObserver = new ResizeObserver(scheduleUpdate)
const header = document.getElementById('header')
const resizeObserver = new ResizeObserver(onLayoutResize)
if (markdownRoot) resizeObserver.observe(markdownRoot)
if (header) resizeObserver.observe(header)

document.addEventListener('scroll', scheduleUpdate, { passive: true })
window.addEventListener('resize', onResize, { passive: true })
window.addEventListener('resize', onLayoutResize, { passive: true })
scheduleUpdateRef.current = scheduleUpdate
rafId = requestAnimationFrame(() => {
rafId = 0
Expand All @@ -88,7 +84,7 @@ const RightPanel: React.FC<IRightPanelProps> = ({ headings, githubLink }) => {

return (): void => {
document.removeEventListener('scroll', scheduleUpdate)
window.removeEventListener('resize', onResize)
window.removeEventListener('resize', onLayoutResize)
resizeObserver.disconnect()
cancelAnimationFrame(rafId)
guardCleanupRef.current?.()
Expand Down
4 changes: 3 additions & 1 deletion src/components/Documentation/RightPanel/styles.module.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
@reference "../../../styles/global.css";

.container {
@apply mt-7 pt-14 sticky top-0 max-h-screen;
@apply mt-7 pt-14 sticky;

align-self: flex-start;
top: var(--layout-alert-height);
max-height: calc(100vh - var(--layout-alert-height));
flex-shrink: 0;
width: 170px;
font-size: 16px;
Expand Down
23 changes: 13 additions & 10 deletions src/components/LayoutHeader/Alert/content.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import Link from '../../Link'
import { ReactComponent as GithubSVG } from '../../SocialIcon/github.svg'

import * as styles from './styles.module.css'

const WEBINAR_URL =
'https://dvc.org/webinars/end-to-end-lineage-aws-dvc-mlflow/'

export const AlertContent = () => (
<>
<span role="img" aria-label="rocket">
🚀
</span>{' '}
<Link href="https://github.com/treeverse/dvc">
Star us on <GithubSVG className="h-5 w-5 inline-block align-middle" />
</Link>
!
</>
<Link className={styles.alertLink} href={WEBINAR_URL}>
<span className={styles.message}>
See how AWS built end-to-end ML lineage with DVC, SageMaker &amp; MLflow
</span>
<span className={styles.cta}>
Join us <time dateTime="2026-09-22">Sept. 22</time>
</span>
</Link>
)
33 changes: 33 additions & 0 deletions src/components/LayoutHeader/Alert/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'

import LayoutAlert from './index'

describe('LayoutAlert', () => {
it('links to the webinar registration page with the event details', () => {
const { container } = render(<LayoutAlert onDismiss={vi.fn()} />)

const link = screen.getByRole('link', {
name: /See how AWS built end-to-end ML lineage with DVC, SageMaker & MLflow/i
})
const time = container.querySelector('time')

expect(link.getAttribute('href')).toBe(
'https://dvc.org/webinars/end-to-end-lineage-aws-dvc-mlflow/'
)
expect(time?.getAttribute('datetime')).toBe('2026-09-22')
expect(link.textContent).toContain('Join us Sept. 22')
})

it('can be dismissed', () => {
const onDismiss = vi.fn()

render(<LayoutAlert onDismiss={onDismiss} />)

fireEvent.click(
screen.getByRole('button', { name: 'Dismiss webinar announcement' })
)

expect(onDismiss).toHaveBeenCalledOnce()
})
})
48 changes: 25 additions & 23 deletions src/components/LayoutHeader/Alert/index.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
import cn from 'clsx/lite'

import { AlertContent } from './content'
import * as styles from './styles.module.css'

const LayoutAlert: React.FC<{ collapsed?: boolean }> = ({
collapsed = false
}) => (
<div
className={cn(
styles.alert,
'w-full',
'transition-all',
'ease-in-out',
'delay-150',
'whitespace-nowrap',
'text-center',
'truncate',
'overflow-hidden',
'px-2',
collapsed ? 'h-0' : 'h-7'
)}
>
<span className="align-middle">
const LayoutAlert: React.FC<{ onDismiss: () => void }> = ({ onDismiss }) => (
<aside aria-label="Upcoming event" className={styles.alert}>
<div className={styles.inner}>
<AlertContent />
</span>
</div>
<button
type="button"
className={styles.dismissButton}
onClick={onDismiss}
aria-label="Dismiss webinar announcement"
>
<svg
aria-hidden="true"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M3 3l10 10M13 3L3 13"
stroke="currentColor"
strokeWidth="2"
/>
</svg>
</button>
</div>
</aside>
)

export default LayoutAlert
42 changes: 42 additions & 0 deletions src/components/LayoutHeader/Alert/state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
afterAll,
afterEach,
beforeAll,
describe,
expect,
it,
vi
} from 'vitest'

import {
ALERT_DISMISSED_ATTRIBUTE,
ALERT_DISMISSED_STORAGE_KEY,
dismissAlert
} from './state'

describe('layout alert state', () => {
const setItem = vi.fn()

beforeAll(() => {
Object.defineProperty(window, 'localStorage', {
configurable: true,
value: { setItem }
})
})

afterEach(() => {
document.body.removeAttribute(ALERT_DISMISSED_ATTRIBUTE)
setItem.mockClear()
})

afterAll(() => {
delete (window as { localStorage?: Storage }).localStorage
})

it('persists a dismissal and updates the document immediately', () => {
dismissAlert()

expect(document.body.hasAttribute(ALERT_DISMISSED_ATTRIBUTE)).toBe(true)
expect(setItem).toHaveBeenCalledWith(ALERT_DISMISSED_STORAGE_KEY, 'true')
})
})
12 changes: 12 additions & 0 deletions src/components/LayoutHeader/Alert/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const ALERT_DISMISSED_ATTRIBUTE = 'data-layout-alert-dismissed'
export const ALERT_DISMISSED_STORAGE_KEY = 'webinar-2026-09-22-alert-dismissed'

export const dismissAlert = (): void => {
document.body.setAttribute(ALERT_DISMISSED_ATTRIBUTE, '')

try {
window.localStorage.setItem(ALERT_DISMISSED_STORAGE_KEY, 'true')
} catch {
// The banner still stays dismissed for this page when storage is blocked.
}
}
Loading
Loading