PageSourceSearch

https://semianalysis.com/wp-content/themes/semianalysis/assets/js/…/landing-claude-header.js?ver=1790715254

js semianalysis.com collected 2026-10-01 08:58:34 UTC 11,202 bytes, 407 lines download raw bytes

1/* eslint-env browser */
2/**
3 * Landing page header (Claude): the glass bar after resend.com.
4 *
5 * Scroll: past a few pixels the bar takes its tint and lit hairline. Menus:
6 * two triggers share one panel under the bar; hovering (with a short intent
7 * delay) or clicking a trigger opens it, centered under that trigger and
8 * sized to the menu it shows, and moving to the other trigger slides the
9 * content sideways while the panel resizes. Leaving the bar and panel, a
10 * click elsewhere, or Escape closes it. Phones get a full-screen menu with
11 * drill-down views for the two menus. Reduced motion shortens the
12 * transitions through CSS; nothing here depends on their duration.
13 */
14(() => {
15	'use strict';
16
17	const header = document.querySelector('[data-sahc-nav]');
18
19	if (!header) {
20		return;
21	}
22
23	const inner = header.querySelector('.sahc-nav__inner');
24	const popup = header.querySelector('[data-sahc-nav-popup]');
25	const triggers = Array.from(
26		header.querySelectorAll('[data-sahc-nav-trigger]')
27	);
28	const panels = Array.from(header.querySelectorAll('[data-sahc-nav-panel]'));
29	const plain = Array.from(header.querySelectorAll('[data-sahc-nav-plain]'));
30	const burger = header.querySelector('[data-sahc-nav-burger]');
31	const mobile = header.querySelector('[data-sahc-nav-mobile]');
32	const desktop = window.matchMedia('(min-width: 1024px)');
33	const order = triggers.map((t) => t.dataset.sahcNavTrigger);
34	const SHIFT = 40;
35
36	let openKey = null;
37	let openTimer = 0;
38	let closeTimer = 0;
39	let hideTimer = 0;
40	let scrollFrame = 0;
41
42	// Scroll: the bar slides away as the page scrolls down and comes back on
43	// the first scroll up, or near the top. Never while the phone menu is
44	// open. An open popup goes with it.
45	let lastY = window.scrollY;
46	let hidden = false;
47	function setHidden(on) {
48		if (hidden === on) {
49			return;
50		}
51		hidden = on;
52		header.classList.toggle('is-hidden', on);
53		// Pages with their own sticky rails read this to take the bar's place.
54		document.documentElement.classList.toggle('sahc-nav-hidden', on);
55		if (on && openKey !== null) {
56			hide();
57		}
58	}
59	// The careers filter bar and the article's contents bar take turns with
60	// the bar too, and need to know when they have stuck (the model pages'
61	// tabs work that out themselves).
62	const turnBars = Array.from(
63		document.querySelectorAll(
64			'.samx-cbar, .single-articles .sa-article-body > .sa-article-body__toc.is-position-sticky'
65		)
66	);
67	function markStuck() {
68		turnBars.forEach((bar) => {
69			const top = parseFloat(window.getComputedStyle(bar).top) || 0;
70			bar.classList.toggle(
71				'is-stuck',
72				bar.getBoundingClientRect().top <= top + 1
73			);
74		});
75	}
76	function onScroll() {
77		if (scrollFrame) {
78			return;
79		}
80		scrollFrame = window.requestAnimationFrame(() => {
81			scrollFrame = 0;
82			const y = window.scrollY;
83			header.classList.toggle('is-scrolled', y > 6);
84			markStuck();
85			if (header.classList.contains('is-menu-open')) {
86				lastY = y;
87				return;
88			}
89			const delta = y - lastY;
90			// A page can ask the bar to stay away for a stretch (the landing
91			// page's folder does while it holds the screen).
92			if (
93				y > 96 &&
94				document.documentElement.classList.contains('sahc-nav-quiet')
95			) {
96				setHidden(true);
97				lastY = y;
98				return;
99			}
100			if (y <= 8) {
101				setHidden(false);
102			} else if (delta > 6 && y > 96) {
103				setHidden(true);
104			} else if (delta < -6) {
105				setHidden(false);
106			}
107			lastY = y;
108		});
109	}
110	window.addEventListener('scroll', onScroll, { passive: true });
111	markStuck();
112
113	// On the landing page the bar arrives held above the screen (the pattern
114	// sends it so) and comes in once the hero says its entrance has settled,
115	// or after a ceiling if that word never comes.
116	if (header.classList.contains('is-held')) {
117		let released = false;
118		const release = () => {
119			if (released) {
120				return;
121			}
122			released = true;
123			header.classList.add('is-arriving');
124			header.classList.remove('is-held');
125			window.setTimeout(
126				() => header.classList.remove('is-arriving'),
127				1000
128			);
129		};
130		document.addEventListener('sahc:hero-settled', release, { once: true });
131		window.setTimeout(release, 5200);
132		if (!document.querySelector('[data-sahc-v2]')) {
133			release();
134		}
135	}
136	onScroll();
137
138	const panelFor = (key) =>
139		panels.find((p) => p.dataset.sahcNavPanel === key) || null;
140	const triggerFor = (key) =>
141		triggers.find((t) => t.dataset.sahcNavTrigger === key) || null;
142
143	// Centers the panel under its trigger, kept inside the bar's content
144	// width, and sizes it to the menu it shows.
145	function place(key) {
146		const trigger = triggerFor(key);
147		const panel = panelFor(key);
148		if (!trigger || !panel || !popup || !inner) {
149			return;
150		}
151		const w = panel.offsetWidth;
152		const h = panel.offsetHeight;
153		const hr = header.getBoundingClientRect();
154		const ir = inner.getBoundingClientRect();
155		const tr = trigger.getBoundingClientRect();
156		let x = tr.left + tr.width / 2 - hr.left;
157		const min = ir.left - hr.left + 8 + w / 2;
158		const max = ir.right - hr.left - 8 - w / 2;
159		x = Math.max(min, Math.min(max, x));
160		popup.style.setProperty('--sahc-pop-x', x.toFixed(1) + 'px');
161		// The pointer sits under the trigger's centre even when the tray
162		// itself had to shift to stay inside the bar.
163		const caret = tr.left + tr.width / 2 - hr.left - (x - w / 2);
164		popup.style.setProperty('--sahc-caret-x', caret.toFixed(1) + 'px');
165		popup.style.width = w + 'px';
166		popup.style.height = h + 'px';
167	}
168
169	// Parks the incoming panel to one side without a transition so its move
170	// to rest reads as travel from the trigger's direction.
171	function prime(panel, from) {
172		panel.style.transition = 'none';
173		panel.style.transform = 'translate3d(' + from + 'px,0,0)';
174		panel.getBoundingClientRect();
175		panel.style.transition = '';
176		panel.style.transform = '';
177	}
178
179	function show(key) {
180		window.clearTimeout(closeTimer);
181		window.clearTimeout(hideTimer);
182		if (!popup || openKey === key) {
183			return;
184		}
185		const previous = openKey;
186		openKey = key;
187		const forward =
188			previous === null || order.indexOf(key) > order.indexOf(previous);
189
190		popup.hidden = false;
191		popup.inert = false;
192		panels.forEach((panel) => {
193			const on = panel.dataset.sahcNavPanel === key;
194			if (on && previous !== null) {
195				prime(panel, forward ? SHIFT : -SHIFT);
196			} else if (panel.dataset.sahcNavPanel === previous) {
197				panel.style.setProperty(
198					'--sahc-out',
199					(forward ? -SHIFT : SHIFT) + 'px'
200				);
201			}
202			panel.classList.toggle('is-active', on);
203		});
204		triggers.forEach((t) => {
205			t.setAttribute(
206				'aria-expanded',
207				t.dataset.sahcNavTrigger === key ? 'true' : 'false'
208			);
209		});
210		place(key);
211		if (previous === null) {
212			// Start from the closed style, then open on the next frame.
213			popup.getBoundingClientRect();
214			window.requestAnimationFrame(() => {
215				if (openKey) {
216					popup.classList.add('is-open');
217					header.classList.add('has-popup');
218				}
219			});
220		}
221	}
222
223	function hide() {
224		window.clearTimeout(openTimer);
225		if (!popup || openKey === null) {
226			return;
227		}
228		openKey = null;
229		triggers.forEach((t) => t.setAttribute('aria-expanded', 'false'));
230		popup.classList.remove('is-open');
231		header.classList.remove('has-popup');
232		popup.inert = true;
233		hideTimer = window.setTimeout(() => {
234			if (openKey === null) {
235				popup.hidden = true;
236				panels.forEach((panel) => panel.classList.remove('is-active'));
237			}
238		}, 400);
239	}
240
241	function scheduleClose(delay) {
242		window.clearTimeout(closeTimer);
243		closeTimer = window.setTimeout(hide, delay);
244	}
245
246	triggers.forEach((trigger) => {
247		const key = trigger.dataset.sahcNavTrigger;
248		trigger.addEventListener('pointerenter', (event) => {
249			if (event.pointerType === 'touch' || !desktop.matches) {
250				return;
251			}
252			window.clearTimeout(closeTimer);
253			window.clearTimeout(openTimer);
254			openTimer = window.setTimeout(() => show(key), 60);
255		});
256		trigger.addEventListener('pointerleave', () => {
257			window.clearTimeout(openTimer);
258		});
259		trigger.addEventListener('click', () => {
260			window.clearTimeout(openTimer);
261			if (openKey === key) {
262				hide();
263			} else {
264				show(key);
265			}
266		});
267		trigger.addEventListener('keydown', (event) => {
268			if (event.key === 'ArrowDown') {
269				event.preventDefault();
270				show(key);
271				const panel = panelFor(key);
272				const first = panel && panel.querySelector('a');
273				if (first) {
274					first.focus();
275				}
276			}
277		});
278	});
279
280	// Hovering the plain links or the actions closes the menu; leaving the
281	// bar and its panel closes it after a short grace.
282	plain.forEach((el) => {
283		el.addEventListener('pointerenter', () => {
284			if (openKey !== null) {
285				scheduleClose(120);
286			}
287		});
288	});
289	header.addEventListener('pointerleave', () => {
290		if (openKey !== null) {
291			scheduleClose(160);
292		}
293	});
294	header.addEventListener('pointerenter', () => {
295		window.clearTimeout(closeTimer);
296	});
297	document.addEventListener('pointerdown', (event) => {
298		if (openKey !== null && !header.contains(event.target)) {
299			hide();
300		}
301	});
302	header.addEventListener('focusout', (event) => {
303		if (
304			openKey !== null &&
305			event.relatedTarget &&
306			!header.contains(event.relatedTarget)
307		) {
308			hide();
309		}
310	});
311
312	// Phones: the burger opens a full-screen menu with drill-down views.
313	function setMobile(open) {
314		if (!mobile || !burger) {
315			return;
316		}
317		if (open) {
318			setHidden(false);
319		}
320		header.classList.toggle('is-menu-open', open);
321		mobile.hidden = !open;
322		burger.setAttribute('aria-expanded', open ? 'true' : 'false');
323		burger.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
324		document.documentElement.classList.toggle('sahc-nav-lock', open);
325		if (!open) {
326			setView('root');
327		}
328	}
329
330	function setView(key) {
331		if (!mobile) {
332			return;
333		}
334		mobile.dataset.view = key;
335		mobile.querySelectorAll('[data-sahc-nav-view]').forEach((view) => {
336			view.classList.toggle(
337				'is-active',
338				view.dataset.sahcNavView === key
339			);
340		});
341		mobile.scrollTop = 0;
342	}
343
344	if (burger && mobile) {
345		burger.addEventListener('click', () =>
346			setMobile(!header.classList.contains('is-menu-open'))
347		);
348		mobile
349			.querySelectorAll('[data-sahc-nav-mobile-trigger]')
350			.forEach((b) =>
351				b.addEventListener('click', () =>
352					setView(b.dataset.sahcNavMobileTrigger)
353				)
354			);
355		mobile
356			.querySelectorAll('[data-sahc-nav-back]')
357			.forEach((b) => b.addEventListener('click', () => setView('root')));
358		setView('root');
359	}
360
361	document.addEventListener('keydown', (event) => {
362		if (event.key !== 'Escape') {
363			return;
364		}
365		if (openKey !== null) {
366			const trigger = triggerFor(openKey);
367			hide();
368			if (trigger) {
369				trigger.focus();
370			}
371		}
372		if (header.classList.contains('is-menu-open')) {
373			setMobile(false);
374			burger.focus();
375		}
376	});
377
378	let resizeFrame = 0;
379	window.addEventListener(
380		'resize',
381		() => {
382			if (resizeFrame) {
383				return;
384			}
385			resizeFrame = window.requestAnimationFrame(() => {
386				resizeFrame = 0;
387				if (openKey !== null) {
388					place(openKey);
389				}
390				if (
391					desktop.matches &&
392					header.classList.contains('is-menu-open')
393				) {
394					setMobile(false);
395				}
396			});
397		},
398		{ passive: true }
399	);
400	desktop.addEventListener('change', () => {
401		hide();
402	});
403
404	if (popup) {
405		popup.inert = true;
406	}
407})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.