Skip to content

Commit 544aa34

Browse files
feat(landing): calmer first screen, full-bleed header, credits-led community entries, hesitant-only download prompt (#7178)
* feat(landing): simplify header community entry, move language switcher to footer, video-first hero - Header: community entry becomes a plain word (飞书群 / 飛書群 / Discord); zh / zh-tw hover reveals the Feishu group QR; benefit list and framed pill removed; Discord / Feishu rows dropped from the Community dropdown - Language switcher moves from the fixed header into the footer bottom bar (homepage + shared site-footer, opens upward); Header keeps the prop for call-site compatibility but no longer renders it - Hero: drop the Claude-Design eyebrow (SEO layer keeps it), move the value-promise paragraph and the product shot into the About section, replace the first-screen shot with a click-to-play YouTube walkthrough * feat(landing): merge about statement with hero value promise, product shot replaces desktop-native panel art * feat(landing): move community entry from header to hero, beside the download CTA * feat(landing): full-bleed flush-top header with icon-only community + X links * feat(landing): drop X from the Community dropdown (lives in the header icon cluster) * feat(landing): restore hero community button; keep Download in the bar at every width * feat(landing): Feishu mark for the community entry, credits nudge on header + hero entries, core-trait chips in hero * feat(landing): reword the coding-agent chip * feat(landing): reframe the local-run chip around data safety * feat(landing): reword the brand chip to brand consistency * feat(landing): shorten the data-safety chip * feat(landing): merge BYOK and coding-agent chips * feat(landing): right-align the footer language switcher next to the social icons * feat(landing): tighten the about statement across all locales * feat(landing): lead the hero chips with brand consistency * feat(landing): icon-only language switcher back in the header action cluster * feat(landing): hero headline from the redesign, scenario line, no selection frame; hesitant-only download prompt; drop Launch Week float - Hero: "The Vibe Design Workspace for your brand." as the single-line headline (all 17 locales), scenario line reordered to prototype / slides / marketing image / video / dashboard, blueprint selection frame removed - Download prompt: any download click suppresses it for 30 days; triggers are now exit intent or scroll-past-hero + 20s dwell (the old 35s / 3-page rules fired on cold traffic with an 84% close rate); tests updated - Launch Week float removed from the homepage (754 closes vs 61 opens) * chore(landing): drop a stray local screenshot script * fix(landing): keep the brand intact on phones; thread canonical-only into the footer switcher - <=640px hides the secondary icon cluster from the bar and pins the brand so it no longer collapses to 0px at 320/375px (ja/zh/en); Download stays. Layout test now asserts 320/375/420 alongside 1081/1367. - The three canonical-only community pages and the canonical PPT tutorial now pass localeCanonicalOnly to SiteFooter, matching their header; a source-level test fails if a canonical-only page forgets the flag. * fix(landing): make the hero video play button the single focus target The facade wrapper was a focusable generic div while the labelled button was pulled out of the tab order. The button is now the only focusable control and native Enter / Space activation bubbles to the frame's click handler; the custom keydown handler on the div is removed. Adds a source-level regression test for the facade's accessibility contract. * fix(landing): visible focus ring on the hero video play button --------- Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
1 parent 5459ffb commit 544aa34

17 files changed

Lines changed: 936 additions & 614 deletions

File tree

apps/landing-page/app/_components/download-engagement-prompt.astro

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,14 @@ const directAssets = {
2424
windows: latestRelease?.matrix.winSetup?.url ?? '',
2525
};
2626
27-
const ACTIVE_SECONDS_THRESHOLD = 35;
28-
const PAGE_COUNT_THRESHOLD = 3;
27+
// Trigger rework (2026-08): the old "35 active seconds / 3 page views" rules
28+
// fired on cold traffic that closed it 84% of the time, and 66% of the people
29+
// who did click had already clicked a hero/nav download button. Now the modal
30+
// only targets hesitant visitors: (a) anyone who clicked ANY download CTA is
31+
// suppressed for DOWNLOAD_COOLDOWN_MS; (b) it fires on exit intent, or after
32+
// the visitor scrolled past the first screen and lingered for
33+
// SCROLL_DWELL_SECONDS of active time without downloading.
34+
const SCROLL_DWELL_SECONDS = 20;
2935
const DISMISS_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000;
3036
const DOWNLOAD_COOLDOWN_MS = 30 * 24 * 60 * 60 * 1000;
3137
---
@@ -92,8 +98,7 @@ const DOWNLOAD_COOLDOWN_MS = 30 * 24 * 60 * 60 * 1000;
9298
define:vars={{
9399
pageName,
94100
locale,
95-
ACTIVE_SECONDS_THRESHOLD,
96-
PAGE_COUNT_THRESHOLD,
101+
SCROLL_DWELL_SECONDS,
97102
DISMISS_COOLDOWN_MS,
98103
DOWNLOAD_COOLDOWN_MS,
99104
directAssets,
@@ -245,16 +250,52 @@ const DOWNLOAD_COOLDOWN_MS = 30 * 24 * 60 * 60 * 1000;
245250
write(sessionStorage, KEYS.pageViews, String(pageViews));
246251

247252
let activeSeconds = Math.max(0, Number(read(sessionStorage, KEYS.activeSeconds) || 0) || 0);
248-
if (pageViews >= PAGE_COUNT_THRESHOLD) {
249-
window.setTimeout(() => show('page_count'), 1200);
253+
254+
// (a) Any download click anywhere on the site suppresses the modal.
255+
for (const link of document.querySelectorAll('[data-download-cta]')) {
256+
if (typeof link.addEventListener !== 'function') continue;
257+
if (typeof link.closest === 'function' && link.closest('[data-download-engagement-prompt]')) continue;
258+
link.addEventListener('click', () => {
259+
write(localStorage, KEYS.downloadedAt, String(Date.now()));
260+
});
261+
}
262+
263+
// (b1) Scroll past the first screen, then linger: dwell time only counts
264+
// once the visitor has left the hero (they saw the primary CTA and kept
265+
// reading), so the modal reaches the hesitant, not the just-arrived.
266+
let pastFirstScreen = false;
267+
const checkScroll = () => {
268+
if (pastFirstScreen) return;
269+
const y = Number(window.scrollY) || 0;
270+
const h = Number(window.innerHeight) || 0;
271+
if (h > 0 && y > h * 0.9) {
272+
pastFirstScreen = true;
273+
if (typeof window.removeEventListener === 'function') {
274+
window.removeEventListener('scroll', checkScroll);
275+
}
276+
}
277+
};
278+
if (typeof window.addEventListener === 'function') {
279+
window.addEventListener('scroll', checkScroll, { passive: true });
250280
}
281+
checkScroll();
251282
timer = window.setInterval(() => {
252-
if (document.visibilityState !== 'visible' || dialog.open) return;
283+
if (!pastFirstScreen || document.visibilityState !== 'visible' || dialog.open) return;
253284
activeSeconds += 1;
254285
write(sessionStorage, KEYS.activeSeconds, String(activeSeconds));
255-
if (activeSeconds >= ACTIVE_SECONDS_THRESHOLD) show('active_time');
286+
if (activeSeconds >= SCROLL_DWELL_SECONDS) show('scroll_dwell');
256287
}, 1000);
257288

289+
// (b2) Exit intent: pointer leaves through the top edge (toward the tab
290+
// bar / address bar) — the classic "about to leave" signal on desktop.
291+
const root = document.documentElement;
292+
if (root && typeof root.addEventListener === 'function') {
293+
root.addEventListener('mouseleave', (event) => {
294+
if (event.clientY > 0) return;
295+
show('exit_intent');
296+
});
297+
}
298+
258299
dialog.addEventListener('click', (event) => {
259300
const target = event.target instanceof Element ? event.target : null;
260301
const dismissButton = target?.closest('[data-download-prompt-dismiss]');

0 commit comments

Comments
 (0)