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
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import clsx from 'clsx';
import React, { type ReactNode, useEffect, useMemo, useState } from 'react';
import { type ReactNode, useEffect, useMemo, useState } from 'react';

import HandIcon from '../HandIcon';
import styles from './styles.module.css';

declare global {
interface Window {
dataLayer?: unknown[];
}
}

type Promo = {
key: string;
href: string;
Expand Down Expand Up @@ -45,11 +51,44 @@ const PROMOS: readonly Promo[] = [
},
];

export default function TopPromoRotator() {
// bump when adding promos so users who dismissed banner see the new one
export const PROMO_VERSION = 1;

type Props = {
onClose?: () => void;
};

export default function TopPromoRotator({ onClose }: Props) {
const promos = useMemo(() => PROMOS, []);

const [index, setIndex] = useState(0);

useEffect(() => {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return;
}

const existingScript = document.querySelector<HTMLScriptElement>(
'script[src*="www.googletagmanager.com/gtm.js?id=GTM-WV2G3SQL"]'
);

if (existingScript) return;

(function (w: Window, d: Document, s: string, l: string, i: string) {
w.dataLayer = w.dataLayer || [];
w.dataLayer.push({
'gtm.start': new Date().getTime(),
event: 'gtm.js',
});
const f = d.getElementsByTagName(s)[0] as HTMLScriptElement;
const j = d.createElement(s) as HTMLScriptElement;
const dl = l !== 'dataLayer' ? `&l=${l}` : '';
j.async = true;
j.src = `https://www.googletagmanager.com/gtm.js?id=${i}${dl}`;
f.parentNode?.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', 'GTM-WV2G3SQL');
}, []);
Comment on lines +66 to +90
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component injects a second GTM container (GTM-WV2G3SQL) at runtime, but the site already loads GTM via @docusaurus/plugin-google-tag-manager (GTM-PHF2NKVT in docusaurus.config.js). Loading two containers on the same dataLayer can lead to duplicated tracking/perf overhead and makes analytics configuration harder to reason about. Consider moving this to the Docusaurus config (or a custom plugin) and/or using a distinct dataLayer name for the dedicated container if it must coexist.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that both work fine, right @p-malecki?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it won't be any problem.


useEffect(() => {
const id = window.setInterval(() => {
setIndex(i => (i + 1) % promos.length);
Expand Down Expand Up @@ -89,6 +128,15 @@ export default function TopPromoRotator() {
</a>
))}
</div>
{onClose && (
<button
type="button"
className={styles.closeButton}
aria-label="Close promotion banner"
onClick={onClose}>
×
</button>
)}
Comment on lines +131 to +139
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The close button is absolutely positioned over the full-width <a> banner without reserving space, so at narrower widths the button can overlap banner text/CTA. Consider reserving right-side padding/margin when onClose is present (e.g., a wrapper modifier class) so content layout doesn’t collide with the close affordance.

Copilot uses AI. Check for mistakes.
<span className="sr-only">
{typeof active.label === 'string' ? active.label : ''}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,17 @@
text-decoration: underline;
text-underline-offset: 2px;
}

.closeButton {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
border: none;
background: transparent;
color: #001a72;
cursor: pointer;
padding: 4px;
line-height: 1;
font-size: 16px;
}
Comment on lines +88 to +91
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.closeButton’s hit area is quite small (roughly 24×24px with current padding/font-size), which is below common touch target guidelines and can be hard to tap on mobile. Consider increasing padding/size (and optionally adding :focus-visible styling) to improve usability and accessibility.

Suggested change
padding: 4px;
line-height: 1;
font-size: 16px;
}
padding: 8px;
min-width: 32px;
min-height: 32px;
line-height: 1;
font-size: 16px;
}
.closeButton:focus-visible {
outline: 2px solid #001a72;
outline-offset: 2px;
}

Copilot uses AI. Check for mistakes.
48 changes: 46 additions & 2 deletions packages/docs-gesture-handler/src/theme/Navbar/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,48 @@
import React from 'react';
import useBaseUrl from '@docusaurus/useBaseUrl';
import { useLocation } from '@docusaurus/router';
import { Navbar } from '@swmansion/t-rex-ui';
import TopPromoRotator from '@site/src/components/TopPromoRotator';
import TopPromoRotator, {
PROMO_VERSION,
} from '@site/src/components/TopPromoRotator';

export default function NavbarWrapper(props) {
const location = useLocation();
const baseUrl = useBaseUrl('/');
const isLanding = location.pathname === baseUrl;

const [showPromo, setShowPromo] = React.useState(true);

React.useEffect(() => {
if (isLanding || typeof globalThis === 'undefined') {
return;
}

try {
const raw = globalThis.localStorage?.getItem('topPromoState');
const state = raw ? JSON.parse(raw) : null;
if (state?.v === PROMO_VERSION && state?.hidden) {
setShowPromo(false);
}
} catch (_) {
// ignore
}
}, [isLanding]);

Comment on lines +14 to +31
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

showPromo is initialized to true and only updated after mount in an effect. If the user previously dismissed the promo, this will still render the banner briefly (UI flash) before setShowPromo(false) runs. Consider initializing state lazily from localStorage (when not landing and in the browser) so the first render matches the persisted hidden state.

Suggested change
const [showPromo, setShowPromo] = React.useState(true);
React.useEffect(() => {
if (isLanding || typeof globalThis === 'undefined') {
return;
}
try {
const raw = globalThis.localStorage?.getItem('topPromoState');
const state = raw ? JSON.parse(raw) : null;
if (state?.v === PROMO_VERSION && state?.hidden) {
setShowPromo(false);
}
} catch (_) {
// ignore
}
}, [isLanding]);
const [showPromo, setShowPromo] = React.useState(() => {
// On the landing page, `showPromo` is not used, so default to true.
if (isLanding) {
return true;
}
// Only attempt to read from localStorage in the browser.
if (typeof window === 'undefined' || !window.localStorage) {
return true;
}
try {
const raw = window.localStorage.getItem('topPromoState');
const state = raw ? JSON.parse(raw) : null;
if (state?.v === PROMO_VERSION && state?.hidden) {
return false;
}
} catch {
// ignore and fall through to default
}
return true;
});

Copilot uses AI. Check for mistakes.
const handleClosePromo = React.useCallback(() => {
setShowPromo(false);
if (typeof globalThis !== 'undefined') {
try {
globalThis.localStorage?.setItem(
'topPromoState',
JSON.stringify({ v: PROMO_VERSION, hidden: true })
);
} catch {
// ignore
}
}
}, []);

const titleImages = {
light: useBaseUrl('/img/title.svg'),
dark: useBaseUrl('/img/title-dark.svg'),
Expand All @@ -12,9 +51,14 @@ export default function NavbarWrapper(props) {
const heroImages = {
logo: useBaseUrl('/img/logo-hero.svg'),
};

return (
<div style={{ display: 'flex', flexDirection: 'column', flexShrink: 0 }}>
<TopPromoRotator />
{isLanding ? (
<TopPromoRotator />
) : (
showPromo && <TopPromoRotator onClose={handleClosePromo} />
)}
<Navbar heroImages={heroImages} titleImages={titleImages} {...props} />
</div>
);
Expand Down
Loading