This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-website.git


The following commit(s) were added to refs/heads/master by this push:
     new f04c0a1b8e2 feat: add Slack community nudge to homepage (#3967)
f04c0a1b8e2 is described below

commit f04c0a1b8e2daf3db5302229f4c9f24f7715f284
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Mon Jul 6 13:54:46 2026 +0800

    feat: add Slack community nudge to homepage (#3967)
---
 .../home-next/SlackCommunityNudge.logic.d.ts       |  27 ++
 .../home-next/SlackCommunityNudge.logic.js         |  48 ++++
 .../home-next/SlackCommunityNudge.logic.test.js    | 119 ++++++++
 src/components/home-next/SlackCommunityNudge.scss  | 312 +++++++++++++++++++++
 src/components/home-next/SlackCommunityNudge.tsx   | 280 ++++++++++++++++++
 .../home-next/sections/EcosystemSection.tsx        |   3 +-
 src/theme/Layout/Provider/index.tsx                |   8 +-
 7 files changed, 795 insertions(+), 2 deletions(-)

diff --git a/src/components/home-next/SlackCommunityNudge.logic.d.ts 
b/src/components/home-next/SlackCommunityNudge.logic.d.ts
new file mode 100644
index 00000000000..c92e311e634
--- /dev/null
+++ b/src/components/home-next/SlackCommunityNudge.logic.d.ts
@@ -0,0 +1,27 @@
+export interface SlackNudgeTriggerState {
+    dismissed: boolean;
+    opened: boolean;
+    elapsedMs: number;
+    ecosystemVisible: boolean;
+}
+
+export interface MascotPupilOffsetInput {
+    eyeX: number;
+    eyeY: number;
+    mouseX: number;
+    mouseY: number;
+    eyeRadiusPx: number;
+    pupilRadiusRatio: number;
+    softDistancePx: number;
+}
+
+export interface MascotPupilOffset {
+    x: number;
+    y: number;
+}
+
+export function computeMascotPupilOffset(input: MascotPupilOffsetInput): 
MascotPupilOffset;
+
+export function getSlackNudgeBenefits(): string[];
+
+export function shouldOpenSlackNudge(state: SlackNudgeTriggerState): boolean;
diff --git a/src/components/home-next/SlackCommunityNudge.logic.js 
b/src/components/home-next/SlackCommunityNudge.logic.js
new file mode 100644
index 00000000000..3fd25063406
--- /dev/null
+++ b/src/components/home-next/SlackCommunityNudge.logic.js
@@ -0,0 +1,48 @@
+function shouldOpenSlackNudge({ dismissed, opened, elapsedMs, ecosystemVisible 
}) {
+    if (dismissed || opened) {
+        return false;
+    }
+
+    return elapsedMs >= 5000 || ecosystemVisible;
+}
+
+function computeMascotPupilOffset({
+    eyeX,
+    eyeY,
+    mouseX,
+    mouseY,
+    eyeRadiusPx,
+    pupilRadiusRatio,
+    softDistancePx,
+}) {
+    const dx = mouseX - eyeX;
+    const dy = mouseY - eyeY;
+    const dist = Math.hypot(dx, dy);
+
+    if (dist === 0) {
+        return { x: 0, y: 0 };
+    }
+
+    const pupilRadiusPx = eyeRadiusPx * pupilRadiusRatio;
+    const maxOffset = Math.max(eyeRadiusPx - pupilRadiusPx, 0);
+    const scale = Math.min(dist / softDistancePx, 1);
+
+    return {
+        x: Number(((dx / dist) * maxOffset * scale).toFixed(2)),
+        y: Number(((dy / dist) * maxOffset * scale).toFixed(2)),
+    };
+}
+
+function getSlackNudgeBenefits() {
+    return [
+        'Talk with Doris users and developers.',
+        'Get the latest Doris community updates, learning resources, and event 
information.',
+        'Join community building, feature discussions, and PR reviews.',
+    ];
+}
+
+module.exports = {
+    computeMascotPupilOffset,
+    getSlackNudgeBenefits,
+    shouldOpenSlackNudge,
+};
diff --git a/src/components/home-next/SlackCommunityNudge.logic.test.js 
b/src/components/home-next/SlackCommunityNudge.logic.test.js
new file mode 100644
index 00000000000..8514215f2b5
--- /dev/null
+++ b/src/components/home-next/SlackCommunityNudge.logic.test.js
@@ -0,0 +1,119 @@
+const assert = require('node:assert/strict');
+const test = require('node:test');
+
+const {
+    computeMascotPupilOffset,
+    getSlackNudgeBenefits,
+    shouldOpenSlackNudge,
+} = require('./SlackCommunityNudge.logic');
+
+test('opens after the visitor has stayed on the page for at least five 
seconds', () => {
+    assert.equal(
+        shouldOpenSlackNudge({
+            dismissed: false,
+            opened: false,
+            elapsedMs: 5000,
+            ecosystemVisible: false,
+        }),
+        true,
+    );
+});
+
+test('opens when the ecosystem section becomes visible before five seconds', 
() => {
+    assert.equal(
+        shouldOpenSlackNudge({
+            dismissed: false,
+            opened: false,
+            elapsedMs: 1200,
+            ecosystemVisible: true,
+        }),
+        true,
+    );
+});
+
+test('stays closed before five seconds on pages without the ecosystem 
section', () => {
+    assert.equal(
+        shouldOpenSlackNudge({
+            dismissed: false,
+            opened: false,
+            elapsedMs: 1200,
+            ecosystemVisible: false,
+        }),
+        false,
+    );
+});
+
+test('stays closed after the visitor dismisses or has already seen it', () => {
+    assert.equal(
+        shouldOpenSlackNudge({
+            dismissed: true,
+            opened: false,
+            elapsedMs: 7000,
+            ecosystemVisible: true,
+        }),
+        false,
+    );
+
+    assert.equal(
+        shouldOpenSlackNudge({
+            dismissed: false,
+            opened: true,
+            elapsedMs: 7000,
+            ecosystemVisible: true,
+        }),
+        false,
+    );
+});
+
+test('keeps mascot pupils centered when the pointer is at the eye center', () 
=> {
+    assert.deepEqual(
+        computeMascotPupilOffset({
+            eyeX: 100,
+            eyeY: 100,
+            mouseX: 100,
+            mouseY: 100,
+            eyeRadiusPx: 10,
+            pupilRadiusRatio: 0.5,
+            softDistancePx: 80,
+        }),
+        { x: 0, y: 0 },
+    );
+});
+
+test('limits mascot pupil movement to the remaining eye radius', () => {
+    assert.deepEqual(
+        computeMascotPupilOffset({
+            eyeX: 100,
+            eyeY: 100,
+            mouseX: 260,
+            mouseY: 100,
+            eyeRadiusPx: 10,
+            pupilRadiusRatio: 0.5,
+            softDistancePx: 80,
+        }),
+        { x: 5, y: 0 },
+    );
+});
+
+test('softens mascot pupil movement for nearby pointers', () => {
+    assert.deepEqual(
+        computeMascotPupilOffset({
+            eyeX: 100,
+            eyeY: 100,
+            mouseX: 140,
+            mouseY: 100,
+            eyeRadiusPx: 10,
+            pupilRadiusRatio: 0.5,
+            softDistancePx: 80,
+        }),
+        { x: 2.5, y: 0 },
+    );
+});
+
+test('returns the Slack community benefits shown in the nudge', () => {
+    assert.deepEqual(getSlackNudgeBenefits(), [
+        'Talk with Doris users and developers.',
+        'Get the latest Doris community updates, learning resources, and event 
information.',
+        'Join community building, feature discussions, and PR reviews.',
+    ]);
+});
diff --git a/src/components/home-next/SlackCommunityNudge.scss 
b/src/components/home-next/SlackCommunityNudge.scss
new file mode 100644
index 00000000000..22e0300cfe8
--- /dev/null
+++ b/src/components/home-next/SlackCommunityNudge.scss
@@ -0,0 +1,312 @@
+@use '../shared/typography' as type;
+
+.slack-community-nudge {
+    --sn-green: #06805F;
+    --sn-green-deep: #054C39;
+    --sn-green-glow: #2DDFA8;
+    --sn-cream: #FAF6EE;
+    --sn-cream-warm: #F5EFE4;
+    --sn-yellow: #FFD23F;
+    --sn-yellow-bright: #FFE066;
+    --sn-ink: #0F1A14;
+
+    position: fixed;
+    right: 24px;
+    bottom: 0;
+    z-index: 260;
+    width: 386px;
+    height: 306px;
+    pointer-events: none;
+    font-family: var(--font-family-base);
+
+    &__bubble {
+        position: absolute;
+        right: 22px;
+        bottom: 100px;
+        width: min(348px, calc(100vw - 32px));
+        padding: 18px 18px 16px;
+        border: 1px solid rgba(17, 166, 121, 0.22);
+        border-radius: 8px;
+        background:
+            radial-gradient(circle at 16% 0%, rgba(45, 223, 168, 0.16), 
transparent 34%),
+            linear-gradient(180deg, #FFFCF5 0%, var(--sn-cream) 100%);
+        color: var(--sn-ink);
+        box-shadow:
+            0 28px 70px -34px rgba(5, 76, 57, 0.52),
+            0 14px 34px -24px rgba(15, 26, 20, 0.38),
+            inset 0 1px 0 rgba(255, 255, 255, 0.8);
+        opacity: 0;
+        pointer-events: none;
+        transform: translate3d(18px, 14px, 0) scale(0.94);
+        transform-origin: 88% 100%;
+        transition:
+            opacity 0.18s ease,
+            transform 0.22s cubic-bezier(0.18, 0.9, 0.25, 1.1);
+
+        &::after {
+            content: '';
+            position: absolute;
+            right: 54px;
+            bottom: -9px;
+            width: 18px;
+            height: 18px;
+            border-right: 1px solid rgba(17, 166, 121, 0.22);
+            border-bottom: 1px solid rgba(17, 166, 121, 0.22);
+            background: var(--sn-cream);
+            transform: rotate(45deg);
+            box-shadow: 10px 10px 24px -18px rgba(15, 26, 20, 0.38);
+        }
+    }
+
+    &--open &__bubble {
+        opacity: 1;
+        pointer-events: auto;
+        transform: translate3d(0, 0, 0) scale(1);
+    }
+
+    &__close {
+        position: absolute;
+        top: 10px;
+        right: 10px;
+        width: 26px;
+        height: 26px;
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        border: 1px solid rgba(15, 26, 20, 0.1);
+        border-radius: 6px;
+        background: rgba(255, 255, 255, 0.52);
+        color: rgba(15, 26, 20, 0.58);
+        cursor: pointer;
+        line-height: 1;
+        transition:
+            background 0.16s ease,
+            border-color 0.16s ease,
+            color 0.16s ease,
+            transform 0.16s ease;
+
+        span {
+            display: block;
+            transform: translateY(-1px);
+            font-size: 18px;
+        }
+
+        &:hover {
+            transform: translateY(-1px);
+            border-color: rgba(6, 128, 95, 0.22);
+            background: rgba(255, 255, 255, 0.86);
+            color: var(--sn-green-deep);
+        }
+
+        &:focus-visible {
+            outline: 2px solid var(--sn-yellow);
+            outline-offset: 3px;
+        }
+    }
+
+    &__eyebrow {
+        margin: 0 34px 8px 0;
+        color: var(--sn-green-deep);
+        @include type.mono-text(10.5px, 700, 1.2, 0, uppercase);
+    }
+
+    &__title {
+        margin: 0 34px 7px 0;
+        color: var(--sn-ink);
+        @include type.mono-text(18px, 800, 1.12, 0);
+    }
+
+    &__copy {
+        margin: 0 0 10px;
+        max-width: 298px;
+        color: rgba(15, 26, 20, 0.68);
+        @include type.sans-text(14px, 400, 1.45);
+    }
+
+    &__benefits {
+        display: flex;
+        flex-direction: column;
+        gap: 7px;
+        margin: 0 0 15px;
+        padding: 0;
+        list-style: none;
+    }
+
+    &__benefit {
+        position: relative;
+        padding-left: 15px;
+        color: rgba(15, 26, 20, 0.74);
+        @include type.sans-text(13px, 450, 1.34);
+
+        &::before {
+            content: '';
+            position: absolute;
+            top: 0.6em;
+            left: 0;
+            width: 5px;
+            height: 5px;
+            border-radius: 50%;
+            background: var(--sn-green);
+            box-shadow: 0 0 0 3px rgba(17, 166, 121, 0.11);
+            transform: translateY(-50%);
+        }
+    }
+
+    &__cta {
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        gap: 8px;
+        min-height: 38px;
+        padding: 0 14px;
+        border: 1.5px solid var(--sn-yellow);
+        border-radius: 6px;
+        background: var(--sn-yellow);
+        color: var(--sn-ink) !important;
+        text-decoration: none !important;
+        @include type.mono-text(12px, 800, 1.2, 0, uppercase);
+        box-shadow: 0 14px 28px -20px rgba(15, 26, 20, 0.54);
+        transition:
+            background 0.16s ease,
+            border-color 0.16s ease,
+            box-shadow 0.16s ease,
+            transform 0.16s ease;
+
+        &:hover {
+            transform: translateY(-1px);
+            background: var(--sn-yellow-bright);
+            border-color: var(--sn-yellow-bright);
+            box-shadow: 0 18px 34px -22px rgba(15, 26, 20, 0.62);
+            text-decoration: none !important;
+        }
+
+        &:focus-visible {
+            outline: 2px solid var(--sn-green-deep);
+            outline-offset: 3px;
+        }
+    }
+
+    &__mascot {
+        position: absolute;
+        right: 0;
+        bottom: -22px;
+        width: 178px;
+        aspect-ratio: 1344 / 768;
+        padding: 0;
+        border: 0;
+        background: transparent;
+        color: inherit;
+        cursor: pointer;
+        pointer-events: auto;
+        filter: drop-shadow(0 18px 24px rgba(15, 26, 20, 0.22));
+        transform: translate3d(0, 0, 0);
+        transition:
+            filter 0.18s ease,
+            transform 0.18s ease;
+
+        &:hover {
+            filter: drop-shadow(0 20px 28px rgba(15, 26, 20, 0.28));
+            transform: translate3d(0, -3px, 0);
+        }
+
+        &:focus-visible {
+            outline: 2px solid var(--sn-yellow);
+            outline-offset: 2px;
+            border-radius: 8px;
+        }
+    }
+
+    &--open &__mascot {
+        transform: translate3d(0, -2px, 0);
+    }
+
+    &__mascot-image {
+        display: block;
+        width: 100%;
+        height: auto;
+        user-select: none;
+        pointer-events: none;
+    }
+
+    &__mascot-pupil {
+        position: absolute;
+        width: 1.7%;
+        aspect-ratio: 1;
+        border-radius: 50%;
+        background: var(--sn-ink);
+        pointer-events: none;
+        transform: translate(-50%, -50%);
+        transition: transform 0.08s ease-out;
+        will-change: transform;
+    }
+}
+
+@media (prefers-reduced-motion: reduce) {
+    .slack-community-nudge {
+        &__bubble,
+        &__close,
+        &__cta,
+        &__mascot,
+        &__mascot-pupil {
+            transition: none;
+        }
+    }
+}
+
+@media (max-width: 640px) {
+    .slack-community-nudge {
+        left: 12px;
+        right: 12px;
+        bottom: max(12px, env(safe-area-inset-bottom));
+        width: auto;
+        height: auto;
+
+        &__bubble {
+            position: relative;
+            right: 0;
+            bottom: auto;
+            width: 100%;
+            max-height: calc(100vh - 96px);
+            overflow-y: auto;
+            padding: 16px;
+            transform: translate3d(0, 12px, 0) scale(0.98);
+            transform-origin: 50% 100%;
+        }
+
+        &__bubble::after {
+            display: none;
+        }
+
+        &__title {
+            font-size: 16px;
+        }
+
+        &__copy {
+            max-width: none;
+            font-size: 13px;
+        }
+
+        &__benefits {
+            gap: 6px;
+            margin-bottom: 13px;
+        }
+
+        &__benefit {
+            font-size: 12.5px;
+        }
+
+        &__mascot {
+            display: none;
+        }
+
+        &__mascot-pupil {
+            display: none;
+        }
+    }
+}
+
+@media print {
+    .slack-community-nudge {
+        display: none;
+    }
+}
diff --git a/src/components/home-next/SlackCommunityNudge.tsx 
b/src/components/home-next/SlackCommunityNudge.tsx
new file mode 100644
index 00000000000..74a52b6dd62
--- /dev/null
+++ b/src/components/home-next/SlackCommunityNudge.tsx
@@ -0,0 +1,280 @@
+import React, { JSX, useCallback, useEffect, useRef, useState } from 'react';
+import {
+    computeMascotPupilOffset,
+    getSlackNudgeBenefits,
+    shouldOpenSlackNudge,
+} from './SlackCommunityNudge.logic';
+import './SlackCommunityNudge.scss';
+
+const SLACK_URL = 'https://doris.apache.org/slack';
+const STORAGE_KEY = 'doris-home-slack-nudge-dismissed';
+const ECOSYSTEM_TARGET_ID = 'home-next-ecosystem';
+const OPEN_DELAY_MS = 5000;
+const SMALL_SCREEN_QUERY = '(max-width: 640px)';
+const MASCOT_EYE_RADIUS_PCT = 1.7;
+const MASCOT_PUPIL_RADIUS_RATIO = 0.5;
+const MASCOT_SOFT_DISTANCE_PX = 80;
+const MASCOT_EYES: Array<{ cx: number; cy: number }> = [
+    { cx: 22.96, cy: 54.01 },
+    { cx: 28.59, cy: 54.01 },
+    { cx: 51.01, cy: 51.27 },
+    { cx: 56.39, cy: 51.95 },
+    { cx: 74.99, cy: 58.71 },
+    { cx: 80.30, cy: 59.91 },
+];
+
+function SlackGlyph(): JSX.Element {
+    return (
+        <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" 
aria-hidden="true">
+            <path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 
0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 
0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 
24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 
1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 
2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 
1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528  [...]
+        </svg>
+    );
+}
+
+function hasStoredDismissal(): boolean {
+    try {
+        return window.sessionStorage.getItem(STORAGE_KEY) === 'true';
+    } catch {
+        return false;
+    }
+}
+
+function storeDismissal(): void {
+    try {
+        window.sessionStorage.setItem(STORAGE_KEY, 'true');
+    } catch {
+        // Storage may be unavailable in private or restricted browsing modes.
+    }
+}
+
+export function SlackCommunityNudge(): JSX.Element {
+    const [isOpen, setIsOpen] = useState(false);
+    const [eyeTrackingEnabled, setEyeTrackingEnabled] = useState(false);
+    const autoDismissedRef = useRef(false);
+    const autoOpenedRef = useRef(false);
+    const mountedAtRef = useRef<number | null>(null);
+    const mascotRef = useRef<HTMLButtonElement>(null);
+    const pupilRefs = useRef<Array<HTMLSpanElement | null>>([]);
+    const lastPointerRef = useRef<{ x: number; y: number } | null>(null);
+
+    const openAutomatically = useCallback((ecosystemVisible: boolean) => {
+        const mountedAt = mountedAtRef.current ?? Date.now();
+        const elapsedMs = Date.now() - mountedAt;
+
+        if (
+            shouldOpenSlackNudge({
+                dismissed: autoDismissedRef.current,
+                opened: autoOpenedRef.current,
+                elapsedMs,
+                ecosystemVisible,
+            })
+        ) {
+            autoOpenedRef.current = true;
+            setIsOpen(true);
+        }
+    }, []);
+
+    const dismissAutoPrompt = useCallback(() => {
+        autoDismissedRef.current = true;
+        storeDismissal();
+        setIsOpen(false);
+    }, []);
+
+    useEffect(() => {
+        if (typeof window === 'undefined') return undefined;
+
+        mountedAtRef.current = Date.now();
+        autoDismissedRef.current = hasStoredDismissal();
+
+        const timer = window.setTimeout(() => {
+            openAutomatically(false);
+        }, OPEN_DELAY_MS);
+
+        return () => window.clearTimeout(timer);
+    }, [openAutomatically]);
+
+    useEffect(() => {
+        if (typeof window === 'undefined' || typeof document === 'undefined') 
return undefined;
+
+        const target = document.getElementById(ECOSYSTEM_TARGET_ID);
+        if (!target || typeof window.IntersectionObserver === 'undefined') {
+            return undefined;
+        }
+
+        const observer = new window.IntersectionObserver(
+            entries => {
+                entries.forEach(entry => {
+                    if (entry.isIntersecting && entry.intersectionRatio >= 
0.24) {
+                        openAutomatically(true);
+                    }
+                });
+            },
+            {
+                threshold: [0, 0.24, 0.5],
+                rootMargin: '0px 0px -12% 0px',
+            },
+        );
+
+        observer.observe(target);
+        return () => observer.disconnect();
+    }, [openAutomatically]);
+
+    useEffect(() => {
+        if (typeof window === 'undefined') return undefined;
+
+        const mediaQuery = window.matchMedia(SMALL_SCREEN_QUERY);
+        const updateEyeTracking = () => 
setEyeTrackingEnabled(!mediaQuery.matches);
+
+        updateEyeTracking();
+        if (typeof mediaQuery.addEventListener === 'function') {
+            mediaQuery.addEventListener('change', updateEyeTracking);
+        } else {
+            mediaQuery.addListener(updateEyeTracking);
+        }
+
+        return () => {
+            if (typeof mediaQuery.removeEventListener === 'function') {
+                mediaQuery.removeEventListener('change', updateEyeTracking);
+            } else {
+                mediaQuery.removeListener(updateEyeTracking);
+            }
+        };
+    }, []);
+
+    useEffect(() => {
+        if (typeof window === 'undefined' || !eyeTrackingEnabled) return 
undefined;
+
+        const updateEyes = (mouseX: number, mouseY: number) => {
+            const mascot = mascotRef.current;
+            if (!mascot) return;
+
+            const rect = mascot.getBoundingClientRect();
+            if (rect.width === 0 || rect.height === 0) return;
+
+            const eyeRadiusPx = (MASCOT_EYE_RADIUS_PCT / 100) * rect.width;
+
+            MASCOT_EYES.forEach((eye, index) => {
+                const pupil = pupilRefs.current[index];
+                if (!pupil) return;
+
+                const eyeX = rect.left + (eye.cx / 100) * rect.width;
+                const eyeY = rect.top + (eye.cy / 100) * rect.height;
+                const offset = computeMascotPupilOffset({
+                    eyeX,
+                    eyeY,
+                    mouseX,
+                    mouseY,
+                    eyeRadiusPx,
+                    pupilRadiusRatio: MASCOT_PUPIL_RADIUS_RATIO,
+                    softDistancePx: MASCOT_SOFT_DISTANCE_PX,
+                });
+
+                pupil.style.transform = `translate(calc(-50% + ${offset.x}px), 
calc(-50% + ${offset.y}px))`;
+            });
+        };
+
+        const onMouseMove = (event: MouseEvent) => {
+            lastPointerRef.current = { x: event.clientX, y: event.clientY };
+            updateEyes(event.clientX, event.clientY);
+        };
+
+        const onTouchMove = (event: TouchEvent) => {
+            const touch = event.touches[0];
+            if (!touch) return;
+            lastPointerRef.current = { x: touch.clientX, y: touch.clientY };
+            updateEyes(touch.clientX, touch.clientY);
+        };
+
+        const onScroll = () => {
+            const pointer = lastPointerRef.current;
+            if (pointer) updateEyes(pointer.x, pointer.y);
+        };
+
+        window.addEventListener('mousemove', onMouseMove, { passive: true });
+        window.addEventListener('touchmove', onTouchMove, { passive: true });
+        window.addEventListener('scroll', onScroll, { passive: true });
+
+        return () => {
+            window.removeEventListener('mousemove', onMouseMove);
+            window.removeEventListener('touchmove', onTouchMove);
+            window.removeEventListener('scroll', onScroll);
+        };
+    }, [eyeTrackingEnabled]);
+
+    const handleJoin = () => {
+        dismissAutoPrompt();
+    };
+
+    return (
+        <aside
+            className={`slack-community-nudge${isOpen ? ' 
slack-community-nudge--open' : ''}`}
+            aria-label="Apache Doris Slack community invitation"
+        >
+            <div className="slack-community-nudge__bubble" 
aria-hidden={!isOpen}>
+                <button
+                    type="button"
+                    className="slack-community-nudge__close"
+                    aria-label="Dismiss Slack invitation"
+                    onClick={dismissAutoPrompt}
+                    tabIndex={isOpen ? undefined : -1}
+                >
+                    <span aria-hidden="true">×</span>
+                </button>
+                <div className="slack-community-nudge__eyebrow">
+                    Community help
+                </div>
+                <p className="slack-community-nudge__title">
+                    Building with Doris?
+                </p>
+                <p className="slack-community-nudge__copy">
+                    Join Slack for setup help, performance tips, and answers 
from Apache Doris users and maintainers.
+                </p>
+                <ul className="slack-community-nudge__benefits">
+                    {getSlackNudgeBenefits().map(benefit => (
+                        <li className="slack-community-nudge__benefit" 
key={benefit}>
+                            {benefit}
+                        </li>
+                    ))}
+                </ul>
+                <a
+                    className="slack-community-nudge__cta"
+                    href={SLACK_URL}
+                    target="_blank"
+                    rel="noopener noreferrer"
+                    onClick={handleJoin}
+                    tabIndex={isOpen ? undefined : -1}
+                >
+                    <SlackGlyph />
+                    Join Slack
+                </a>
+            </div>
+
+            <button
+                type="button"
+                className="slack-community-nudge__mascot"
+                ref={mascotRef}
+                aria-expanded={isOpen}
+                aria-label={isOpen ? 'Hide Slack invitation' : 'Open Slack 
invitation'}
+                onClick={() => setIsOpen(open => !open)}
+            >
+                <img
+                    className="slack-community-nudge__mascot-image"
+                    src="/images/next/home-page/doris-mascot.png"
+                    width={1344}
+                    height={768}
+                    alt=""
+                    draggable={false}
+                />
+                {MASCOT_EYES.map((eye, index) => (
+                    <span
+                        key={index}
+                        ref={(el) => { pupilRefs.current[index] = el; }}
+                        className="slack-community-nudge__mascot-pupil"
+                        style={{ left: `${eye.cx}%`, top: `${eye.cy}%` }}
+                        aria-hidden="true"
+                    />
+                ))}
+            </button>
+        </aside>
+    );
+}
diff --git a/src/components/home-next/sections/EcosystemSection.tsx 
b/src/components/home-next/sections/EcosystemSection.tsx
index 243eaa4d142..780f62c8e71 100644
--- a/src/components/home-next/sections/EcosystemSection.tsx
+++ b/src/components/home-next/sections/EcosystemSection.tsx
@@ -504,6 +504,7 @@ function CompactConnector(): JSX.Element {
 function CompactEcosystem(): JSX.Element {
     return (
         <section
+            id="home-next-ecosystem"
             className="ecosystem-next ecosystem-next--compact"
             aria-labelledby="ecosystem-next-title"
         >
@@ -544,7 +545,7 @@ export function EcosystemSection(): JSX.Element {
     }
 
     return (
-        <section className="ecosystem-next" 
aria-labelledby="ecosystem-next-title">
+        <section id="home-next-ecosystem" className="ecosystem-next" 
aria-labelledby="ecosystem-next-title">
             <div className="home-next-container">
                 <EcosystemHeader showCoverage />
 
diff --git a/src/theme/Layout/Provider/index.tsx 
b/src/theme/Layout/Provider/index.tsx
index 4b765fba2c5..e681238e13b 100644
--- a/src/theme/Layout/Provider/index.tsx
+++ b/src/theme/Layout/Provider/index.tsx
@@ -9,6 +9,7 @@ import {
 } from '@docusaurus/theme-common/internal';
 import {DocsPreferredVersionContextProvider} from 
'@docusaurus/plugin-content-docs/client';
 import type {Props} from '@theme/Layout/Provider';
+import {SlackCommunityNudge} from 
'@site/src/components/home-next/SlackCommunityNudge';
 
 const Provider = composeProviders([
   ColorModeProvider,
@@ -20,5 +21,10 @@ const Provider = composeProviders([
 ]);
 
 export default function LayoutProvider({children}: Props): JSX.Element {
-  return <Provider>{children}</Provider>;
+  return (
+    <Provider>
+      {children}
+      <SlackCommunityNudge />
+    </Provider>
+  );
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to