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 be33b3e20ed [Feature](website) Add community roadmap page and improve
homepage loading (#4038)
be33b3e20ed is described below
commit be33b3e20ed114e295a137c946eacb9b3030b132
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Wed Aug 5 20:22:46 2026 +0800
[Feature](website) Add community roadmap page and improve homepage loading
(#4038)
## Summary
- add a community roadmap landing page and route
- give both Ask Me entry points immediate loading, slow-loading, and
failure feedback
- queue early Ask Me clicks and automatically open the Kapa dialog once
the widget is ready
- lazy-load and asynchronously decode below-the-fold homepage images
## Validation
- `yarn start --no-open --port 3000` — English development site compiled
successfully
- `node --check static/js/custom-script.js`
- `git diff --check`
- simulated a delayed Kapa response: loading feedback appeared
immediately, changed after 8 seconds, and the queued click opened the
dialog after readiness
- simulated a failed Kapa response: both buttons showed `AI unavailable`
- verified Deployment, Ecosystem, and Stats lazy-loading attributes in
the browser
## Versions
- Not applicable: website UI changes only
## Languages
- [x] English
- [ ] Chinese
- [ ] Japanese candidate translation needed
## Docs Checklist
- [x] Checked by AI
- [x] Test Cases Built
- [x] Updated required version and language counterparts, or explained
why not
- [x] If only one language changed, confirmed whether source/translation
counterparts need sync
The full repository `yarn typecheck` remains blocked by pre-existing
unrelated type errors; none of the touched files appeared in the error
list.
---------
Co-authored-by: morningman <[email protected]>
---
docusaurus.config.js | 2 +-
.../community-roadmap/CommunityRoadmap.scss | 315 +++++++++++++++++++++
.../community-roadmap/CommunityRoadmap.tsx | 115 ++++++++
src/components/home-next/NavbarNext.tsx | 13 +-
.../home-next/sections/DeploymentSection.tsx | 16 +-
.../home-next/sections/EcosystemSection.tsx | 6 +
src/components/home-next/sections/HeroSection.tsx | 14 +-
src/components/home-next/sections/StatsSection.tsx | 1 +
src/pages/community/roadmap/index.tsx | 6 +
src/scss/custom.scss | 25 ++
static/js/custom-script.js | 150 ++++++++++
11 files changed, 652 insertions(+), 11 deletions(-)
diff --git a/docusaurus.config.js b/docusaurus.config.js
index b20f553884a..11df557dc77 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -594,7 +594,7 @@ const config = {
},
{
label: 'Roadmap',
- href:
'https://github.com/apache/doris/issues/60036',
+ href: '/community/roadmap',
},
{
label: 'Improvement proposal',
diff --git a/src/components/community-roadmap/CommunityRoadmap.scss
b/src/components/community-roadmap/CommunityRoadmap.scss
new file mode 100644
index 00000000000..a60afc4eb3a
--- /dev/null
+++ b/src/components/community-roadmap/CommunityRoadmap.scss
@@ -0,0 +1,315 @@
+@use '../shared/typography' as type;
+
+.community-roadmap {
+ position: relative;
+ isolation: isolate;
+ min-height: calc(100vh - 64px);
+ overflow: hidden;
+ background:
+ radial-gradient(
+ ellipse at 76% 10%,
+ rgb(var(--brand-primary-glow-rgb) / 20%),
+ transparent 52%
+ ),
+ radial-gradient(ellipse at 2% 94%, rgb(var(--brand-accent-rgb) / 8%),
transparent 54%),
+ linear-gradient(
+ 180deg,
+ var(--brand-primary-dark),
+ color-mix(in srgb, var(--brand-primary-dark) 80%,
var(--brand-primary-darker))
+ );
+ color: var(--brand-cream-light);
+ font-family: var(--font-family-base);
+
+ * {
+ box-sizing: border-box;
+ }
+}
+
+.community-roadmap__background {
+ position: absolute;
+ inset: 0;
+ z-index: -1;
+ background-image: radial-gradient(
+ color-mix(in srgb, var(--brand-cream-light) 10%, transparent) 1.2px,
+ transparent 1.2px
+ );
+ background-size: 28px 28px;
+ mask-image: linear-gradient(180deg, black, black 72%, transparent);
+ pointer-events: none;
+}
+
+.community-roadmap__inner {
+ width: min(calc(100% - 112px), 1328px);
+ margin: 0 auto;
+ padding: 62px 0 50px;
+}
+
+.community-roadmap__hero {
+ display: grid;
+ grid-template-columns: minmax(0, 1.48fr) minmax(300px, 0.52fr);
+ align-items: end;
+ gap: 80px;
+ margin-bottom: 58px;
+}
+
+.community-roadmap__hero h1 {
+ max-width: 900px;
+ margin: 0 !important;
+ color: var(--brand-cream-light) !important;
+ font-family: var(--font-mono) !important;
+ font-size: clamp(46px, 5.15vw, 76px) !important;
+ font-weight: 700;
+ letter-spacing: -0.055em;
+ line-height: 0.98;
+ text-wrap: balance;
+
+ span {
+ color: var(--brand-accent);
+ }
+}
+
+.community-roadmap__intro {
+ margin: 0;
+ padding: 0 0 7px 24px;
+ border-left: 2px solid var(--brand-primary-glow);
+ color: color-mix(in srgb, var(--brand-cream-light) 78%, transparent);
+ font-size: 17px;
+ line-height: 1.65;
+ text-wrap: pretty;
+
+ strong {
+ color: var(--brand-cream-light);
+ font-weight: 650;
+ }
+}
+
+.community-roadmap__grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 14px;
+}
+
+.community-roadmap__card {
+ position: relative;
+ display: flex;
+ min-height: 312px;
+ flex-direction: column;
+ overflow: hidden;
+ padding: 28px 30px 26px;
+ border: 1px solid color-mix(in srgb, var(--brand-border-soft) 72%,
transparent);
+ border-radius: 3px;
+ background: var(--brand-paper);
+ box-shadow: 0 26px 54px -38px rgb(var(--brand-shadow-rgb) / 80%);
+ color: var(--brand-ink);
+ text-decoration: none !important;
+ transition:
+ transform 180ms ease,
+ border-color 180ms ease,
+ background-color 180ms ease,
+ box-shadow 180ms ease;
+
+ &:hover {
+ z-index: 1;
+ border-color: var(--brand-primary-glow);
+ background: var(--brand-surface-soft);
+ box-shadow: 0 34px 64px -38px rgb(var(--brand-shadow-rgb) / 95%);
+ color: var(--brand-ink);
+ text-decoration: none !important;
+ transform: translateY(-7px);
+ }
+
+ &:focus-visible {
+ z-index: 2;
+ outline: 3px solid var(--brand-accent);
+ outline-offset: 4px;
+ }
+}
+
+.community-roadmap__card-number {
+ position: absolute;
+ right: 24px;
+ bottom: -20px;
+ color: color-mix(in srgb, var(--brand-primary) 10%, transparent);
+ font: 700 110px/0.8 var(--font-mono);
+ letter-spacing: -0.09em;
+ pointer-events: none;
+}
+
+.community-roadmap__card-top {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 18px;
+
+ p {
+ margin: 0;
+ color: var(--brand-primary-deep);
+ @include type.mono-text(12px, 700, 1.25, 0.095em, uppercase);
+ }
+}
+
+.community-roadmap__card-arrow {
+ display: grid;
+ width: 38px;
+ height: 38px;
+ flex: 0 0 38px;
+ place-items: center;
+ border: 1px solid color-mix(in srgb, var(--brand-ink) 22%, transparent);
+ border-radius: 50%;
+ font: 500 20px/1 var(--font-mono);
+ transition:
+ transform 180ms ease,
+ color 180ms ease,
+ background-color 180ms ease;
+}
+
+.community-roadmap__card:hover .community-roadmap__card-arrow {
+ background: var(--brand-primary-deep);
+ color: var(--brand-paper);
+ transform: translate(2px, -2px);
+}
+
+.community-roadmap__card h3 {
+ position: relative;
+ z-index: 1;
+ margin: 42px 0 15px;
+ color: var(--brand-ink);
+ font-family: var(--font-mono);
+ font-size: clamp(26px, 2.4vw, 36px);
+ font-weight: 700;
+ letter-spacing: -0.045em;
+ line-height: 1.05;
+
+ span {
+ display: block;
+ }
+}
+
+.community-roadmap__card-description {
+ position: relative;
+ z-index: 1;
+ max-width: 34ch;
+ margin: 0;
+ color: color-mix(in srgb, var(--brand-ink-soft) 84%, transparent);
+ font-size: 15px;
+ line-height: 1.58;
+ text-wrap: pretty;
+}
+
+.community-roadmap__card-meta {
+ position: relative;
+ z-index: 1;
+ margin: auto 0 0;
+ padding-top: 26px;
+ color: color-mix(in srgb, var(--brand-ink-soft) 66%, transparent);
+ @include type.mono-text(12px, 650, 1.2);
+}
+
+.community-roadmap__footer-action {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 34px;
+ padding-top: 24px;
+ border-top: 1px solid color-mix(in srgb, var(--brand-cream-light) 14%,
transparent);
+
+ a {
+ color: var(--brand-cream-light);
+ font: 700 13px/1.4 var(--font-mono);
+ text-decoration-color: var(--brand-primary-glow);
+ text-underline-offset: 5px;
+
+ &:hover {
+ color: var(--brand-accent);
+ }
+
+ &:focus-visible {
+ outline: 3px solid var(--brand-accent);
+ outline-offset: 4px;
+ }
+ }
+}
+
+@media (max-width: 960px) {
+ .community-roadmap__inner {
+ width: min(calc(100% - 48px), 820px);
+ }
+
+ .community-roadmap__hero {
+ grid-template-columns: 1fr;
+ gap: 34px;
+ }
+
+ .community-roadmap__intro {
+ max-width: 680px;
+ }
+
+ .community-roadmap__grid {
+ grid-template-columns: 1fr;
+ gap: 12px;
+ }
+
+ .community-roadmap__card {
+ min-height: 230px;
+ padding-right: 25%;
+ }
+
+ .community-roadmap__card h3 {
+ margin-top: 28px;
+ }
+}
+
+@media (max-width: 620px) {
+ .community-roadmap__inner {
+ width: min(calc(100% - 32px), 520px);
+ padding: 42px 0 36px;
+ }
+
+ .community-roadmap__hero {
+ gap: 26px;
+ margin-bottom: 42px;
+ }
+
+ .community-roadmap__hero h1 {
+ font-size: clamp(40px, 13vw, 56px) !important;
+ line-height: 1.01;
+ }
+
+ .community-roadmap__intro {
+ padding-left: 18px;
+ font-size: 16px;
+ }
+
+ .community-roadmap__card {
+ min-height: 292px;
+ padding: 24px 24px 22px;
+ }
+
+ .community-roadmap__card-number {
+ right: 18px;
+ font-size: 96px;
+ }
+
+ .community-roadmap__card h3 {
+ margin-top: 48px;
+ font-size: 30px;
+ }
+
+ .community-roadmap__card-description {
+ max-width: 29ch;
+ }
+
+ .community-roadmap__footer-action {
+ margin-top: 28px;
+ padding-top: 20px;
+ }
+
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .community-roadmap__card,
+ .community-roadmap__card-arrow {
+ transition: none;
+ }
+}
diff --git a/src/components/community-roadmap/CommunityRoadmap.tsx
b/src/components/community-roadmap/CommunityRoadmap.tsx
new file mode 100644
index 00000000000..6ae007503f5
--- /dev/null
+++ b/src/components/community-roadmap/CommunityRoadmap.tsx
@@ -0,0 +1,115 @@
+import React, { JSX } from 'react';
+import Link from '@docusaurus/Link';
+import { LayoutNext } from '../home-next/LayoutNext';
+import './CommunityRoadmap.scss';
+
+const ROADMAP_URL = 'https://github.com/apache/doris/issues/60036';
+const TRACKING_ISSUES_URL =
+
'https://github.com/apache/doris/issues?q=state%3Aopen%20label%3A%22tracking%20issue%22';
+const GOOD_FIRST_ISSUE_URL = 'https://github.com/apache/doris/issues/17176';
+
+interface DevelopmentDestination {
+ phase: string;
+ title: string;
+ titleDetail?: string;
+ description: string;
+ meta: string;
+ href: string;
+ ariaLabel: string;
+}
+
+const DESTINATIONS: DevelopmentDestination[] = [
+ {
+ phase: 'Understand the direction',
+ title: 'Roadmap',
+ titleDetail: '2026',
+ description: 'Start with the high-level direction and priorities
shaping Apache Doris.',
+ meta: 'Issue #60036',
+ href: ROADMAP_URL,
+ ariaLabel: 'Read the Apache Doris Roadmap 2026 on GitHub (opens in a
new tab)',
+ },
+ {
+ phase: 'Follow active work',
+ title: 'Tracking Issues',
+ description: 'Explore topic-level coordination across features,
improvements, and engineering work.',
+ meta: 'Live label search',
+ href: TRACKING_ISSUES_URL,
+ ariaLabel: 'Browse open Apache Doris tracking issues on GitHub (opens
in a new tab)',
+ },
+ {
+ phase: 'Find a place to contribute',
+ title: 'Good First Issue',
+ description: 'Begin with a curated path toward approachable work for
new contributors.',
+ meta: 'Issue #17176',
+ href: GOOD_FIRST_ISSUE_URL,
+ ariaLabel: 'View Apache Doris Good First Issue opportunities on GitHub
(opens in a new tab)',
+ },
+];
+
+function ExternalArrow(): JSX.Element {
+ return (
+ <span className="community-roadmap__card-arrow" aria-hidden="true">
+ ↗
+ </span>
+ );
+}
+
+export default function CommunityRoadmap(): JSX.Element {
+ return (
+ <LayoutNext
+ title="Community Development"
+ description="Explore the Apache Doris roadmap, active tracking
issues, and approachable contribution opportunities."
+ >
+ <div className="community-roadmap">
+ <div className="community-roadmap__background"
aria-hidden="true" />
+
+ <div className="community-roadmap__inner">
+ <header className="community-roadmap__hero">
+ <div className="community-roadmap__hero-title">
+ <h1>
+ See what we’re <span>building next.</span>
+ </h1>
+ </div>
+ <p className="community-roadmap__intro">
+ <strong>Apache Doris plans and builds in the
open.</strong> Use this guide to understand the
+ direction, follow active engineering work, and
find a place to contribute.
+ </p>
+ </header>
+
+ <section className="community-roadmap__destinations"
aria-label="Community development links">
+ <div className="community-roadmap__grid">
+ {DESTINATIONS.map((destination, index) => (
+ <a
+ className="community-roadmap__card"
+ href={destination.href}
+ target="_blank"
+ rel="noopener noreferrer"
+ aria-label={destination.ariaLabel}
+ key={destination.title}
+ >
+ <span
className="community-roadmap__card-number" aria-hidden="true">
+ {String(index + 1).padStart(2, '0')}
+ </span>
+ <div
className="community-roadmap__card-top">
+ <p>{destination.phase}</p>
+ <ExternalArrow />
+ </div>
+ <h3>
+ <span>{destination.title}</span>
+ {destination.titleDetail &&
<span>{destination.titleDetail}</span>}
+ </h3>
+ <p
className="community-roadmap__card-description">{destination.description}</p>
+ <p
className="community-roadmap__card-meta">Opens on GitHub ·
{destination.meta}</p>
+ </a>
+ ))}
+ </div>
+ </section>
+
+ <div className="community-roadmap__footer-action">
+ <Link
to="/community/how-to-contribute/contribute-to-doris">Read the contribution
guide →</Link>
+ </div>
+ </div>
+ </div>
+ </LayoutNext>
+ );
+}
diff --git a/src/components/home-next/NavbarNext.tsx
b/src/components/home-next/NavbarNext.tsx
index 62656f2466c..f54707bccdc 100644
--- a/src/components/home-next/NavbarNext.tsx
+++ b/src/components/home-next/NavbarNext.tsx
@@ -28,6 +28,7 @@ function buildNavItems(
v3xDocsHref: string,
v21DocsHref: string,
releasesHref: string,
+ roadmapHref: string,
joinCommunityHref: string,
communityReportHref: string,
): NavItem[] {
@@ -72,7 +73,7 @@ function buildNavItems(
label: 'Community',
items: [
{ label: 'Community Report', href: communityReportHref },
- { label: 'Roadmap', href:
'https://github.com/apache/doris/issues/60036', external: true },
+ { label: 'Roadmap', href: roadmapHref },
{ label: 'Build with Us', href: joinCommunityHref },
{ label: 'Join GitHub Discussions', href:
'https://github.com/apache/doris/discussions', external: true },
],
@@ -128,6 +129,7 @@ export function NavbarNext(): JSX.Element {
const v3xDocsHref =
`${localePrefix}/docs/3.x/gettingStarted/what-is-apache-doris`;
const v21DocsHref =
`${localePrefix}/docs/2.1/gettingStarted/what-is-apache-doris`;
const releasesHref = `${localePrefix}/releases/all-release`;
+ const roadmapHref = `${localePrefix}/community/roadmap`;
const joinCommunityHref = `${localePrefix}/community/join-community`;
const communityReportHref = `${localePrefix}/community-report`;
const navItems = buildNavItems(
@@ -136,6 +138,7 @@ export function NavbarNext(): JSX.Element {
v3xDocsHref,
v21DocsHref,
releasesHref,
+ roadmapHref,
joinCommunityHref,
communityReportHref,
);
@@ -202,9 +205,15 @@ export function NavbarNext(): JSX.Element {
id="navbar-ask-ai-btn"
className="navbar-next__ask-ai"
aria-label="Ask Me"
+ aria-busy="false"
+ aria-disabled="false"
+ data-kapa-trigger
+ suppressHydrationWarning
>
<StarGreenIcon />
- <span>Ask Me</span>
+ <span data-kapa-label aria-live="polite"
suppressHydrationWarning>
+ Ask Me
+ </span>
</button>
<a
href={`https://github.com/${GITHUB_REPO}`}
diff --git a/src/components/home-next/sections/DeploymentSection.tsx
b/src/components/home-next/sections/DeploymentSection.tsx
index 934d92d37ce..1c35c8ba5ed 100644
--- a/src/components/home-next/sections/DeploymentSection.tsx
+++ b/src/components/home-next/sections/DeploymentSection.tsx
@@ -14,6 +14,8 @@ interface DeploymentCardItem {
image: {
src: string;
alt: string;
+ width: number;
+ height: number;
};
cta: {
label: string;
@@ -33,6 +35,8 @@ const DEPLOYMENT_CARDS: DeploymentCardItem[] = [
image: {
src: '/images/next/home-page/cs-coupled.jpg',
alt: 'Apache Doris compute-storage coupled mode architecture
diagram',
+ width: 1774,
+ height: 887,
},
cta: {
label: 'Deploy Now!',
@@ -50,6 +54,8 @@ const DEPLOYMENT_CARDS: DeploymentCardItem[] = [
image: {
src: '/images/next/home-page/cs-decoupled.jpg',
alt: 'Apache Doris compute-storage decoupled mode architecture
diagram',
+ width: 1672,
+ height: 941,
},
cta: {
label: 'Deploy Now!',
@@ -75,7 +81,15 @@ function DeploymentCard({ card }: { card: DeploymentCardItem
}): JSX.Element {
</a>
</div>
<div className="deployment-next__visual">
- <img className="deployment-next__visual-image"
src={card.image.src} alt={card.image.alt} />
+ <img
+ className="deployment-next__visual-image"
+ src={card.image.src}
+ alt={card.image.alt}
+ width={card.image.width}
+ height={card.image.height}
+ loading="lazy"
+ decoding="async"
+ />
</div>
</article>
);
diff --git a/src/components/home-next/sections/EcosystemSection.tsx
b/src/components/home-next/sections/EcosystemSection.tsx
index 9e06ab2805b..a5a09a976e8 100644
--- a/src/components/home-next/sections/EcosystemSection.tsx
+++ b/src/components/home-next/sections/EcosystemSection.tsx
@@ -208,6 +208,8 @@ function EcosystemBlock({ data, side, refMap }:
EcosystemBlockProps): JSX.Elemen
src={logo.logoSrc}
alt=""
aria-hidden="true"
+ loading="lazy"
+ decoding="async"
draggable={false}
/>
</span>
@@ -526,6 +528,8 @@ function CompactEcosystem(): JSX.Element {
className="ecosystem-next__doris-logo"
src="/images/logo-doris.svg"
alt="Apache Doris"
+ loading="lazy"
+ decoding="async"
draggable={false}
/>
</div>
@@ -588,6 +592,8 @@ export function EcosystemSection(): JSX.Element {
className="ecosystem-next__doris-logo"
src="/images/logo-doris.svg"
alt="Apache Doris"
+ loading="lazy"
+ decoding="async"
draggable={false}
/>
</div>
diff --git a/src/components/home-next/sections/HeroSection.tsx
b/src/components/home-next/sections/HeroSection.tsx
index 23b63e8ebe9..4d80729128b 100644
--- a/src/components/home-next/sections/HeroSection.tsx
+++ b/src/components/home-next/sections/HeroSection.tsx
@@ -896,16 +896,16 @@ export function HeroSection(): JSX.Element {
<button
type="button"
className="hero-next__btn
hero-next__btn--primary"
- onClick={() => {
- // Reuse the Kapa modal trigger bound to
the
- // navbar Ask Me button via
docusaurus.config.js
- // (data-modal-override-open-selector).
-
document.getElementById('navbar-ask-ai-btn')?.click();
- }}
aria-label="Ask Me"
+ aria-busy="false"
+ aria-disabled="false"
+ data-kapa-trigger
+ suppressHydrationWarning
>
<StarGreenIcon />
- Ask Me
+ <span data-kapa-label aria-live="polite"
suppressHydrationWarning>
+ Ask Me
+ </span>
</button>
<a
className="hero-next__btn
hero-next__btn--ghost"
diff --git a/src/components/home-next/sections/StatsSection.tsx
b/src/components/home-next/sections/StatsSection.tsx
index f8fbf68b97b..8f4bcde3287 100644
--- a/src/components/home-next/sections/StatsSection.tsx
+++ b/src/components/home-next/sections/StatsSection.tsx
@@ -356,6 +356,7 @@ function LogoTile({ logo, duplicate = false, expanded =
false, onEnter, onLeave
alt={duplicate ? '' : logo.name}
title={logo.name}
loading="lazy"
+ decoding="async"
draggable={false}
/>
</div>
diff --git a/src/pages/community/roadmap/index.tsx
b/src/pages/community/roadmap/index.tsx
new file mode 100644
index 00000000000..98598b095a6
--- /dev/null
+++ b/src/pages/community/roadmap/index.tsx
@@ -0,0 +1,6 @@
+import React, { JSX } from 'react';
+import CommunityRoadmap from
'@site/src/components/community-roadmap/CommunityRoadmap';
+
+export default function CommunityRoadmapPage(): JSX.Element {
+ return <CommunityRoadmap />;
+}
diff --git a/src/scss/custom.scss b/src/scss/custom.scss
index fa0ff88e363..c4996156343 100644
--- a/src/scss/custom.scss
+++ b/src/scss/custom.scss
@@ -165,6 +165,31 @@ body {
z-index: 9999 !important;
}
+[data-kapa-trigger][aria-busy='true'] {
+ cursor: progress;
+}
+
+[data-kapa-trigger][aria-busy='true'] svg {
+ animation: doris-kapa-loading 1s linear infinite;
+}
+
+[data-kapa-trigger][aria-disabled='true'] {
+ cursor: not-allowed;
+ opacity: 0.72;
+}
+
+@keyframes doris-kapa-loading {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ [data-kapa-trigger][aria-busy='true'] svg {
+ animation: none;
+ }
+}
+
// Centering the Kapa modal itself is done by injecting CSS into its shadow
// root from static/js/custom-script.js, since light-DOM rules can't pierce it.
diff --git a/static/js/custom-script.js b/static/js/custom-script.js
index 410c23b1829..9de27bc3f4b 100644
--- a/static/js/custom-script.js
+++ b/static/js/custom-script.js
@@ -11,6 +11,156 @@
y.parentNode.insertBefore(t, y);
})(window, document, 'clarity', 'script', 'kfyqejiz0g');
+// Kapa's script is loaded asynchronously and can take a long time on a cold
+// cache. Keep Ask Me clicks made before the widget is ready, expose visible
+// loading/error feedback, and replay the latest intent once Kapa has mounted.
+// This listener runs before React hydrates, so clicks made immediately after
+// the server-rendered buttons appear are covered as well.
+(function manageKapaTrigger() {
+ var KAPA_SCRIPT_SELECTOR =
'script[src="https://widget.kapa.ai/kapa-widget.bundle.js"]';
+ var TRIGGER_SELECTOR = '[data-kapa-trigger]';
+ var NAVBAR_TRIGGER_ID = 'navbar-ask-ai-btn';
+ var state = 'loading';
+ var pendingOpen = false;
+ var slow = false;
+ var replaying = false;
+ var slowTimer;
+ var pollTimer;
+
+ function getReadyHost() {
+ var host = document.getElementById('kapa-widget-container');
+ return host && host.shadowRoot ? host : null;
+ }
+
+ function syncTriggers() {
+ document.querySelectorAll(TRIGGER_SELECTOR).forEach(function (trigger)
{
+ var label = trigger.querySelector('[data-kapa-label]');
+ if (label && !label.getAttribute('data-kapa-default-label')) {
+ label.setAttribute('data-kapa-default-label',
label.textContent || 'Ask Me');
+ }
+
+ var waiting = pendingOpen && state === 'loading';
+ trigger.setAttribute('aria-busy', waiting ? 'true' : 'false');
+ trigger.setAttribute('aria-disabled', state === 'error' ? 'true' :
'false');
+
+ if (!label) return;
+ var nextLabel;
+ if (waiting) {
+ nextLabel = slow ? 'Still loading…' : 'Loading AI…';
+ } else if (state === 'error') {
+ nextLabel = 'AI unavailable';
+ } else {
+ nextLabel = label.getAttribute('data-kapa-default-label') ||
'Ask Me';
+ }
+ if (label.textContent !== nextLabel) label.textContent = nextLabel;
+ });
+ }
+
+ function replayOpen() {
+ if (!pendingOpen || state !== 'ready') return;
+ var navbarTrigger = document.getElementById(NAVBAR_TRIGGER_ID);
+ if (!navbarTrigger) return;
+
+ pendingOpen = false;
+ slow = false;
+ window.clearTimeout(slowTimer);
+ syncTriggers();
+
+ replaying = true;
+ navbarTrigger.click();
+ replaying = false;
+ }
+
+ function markReady() {
+ if (state === 'ready') return;
+ state = 'ready';
+ window.clearInterval(pollTimer);
+ syncTriggers();
+ window.setTimeout(replayOpen, 0);
+ }
+
+ function markError() {
+ if (state === 'ready') return;
+ state = 'error';
+ pendingOpen = false;
+ slow = false;
+ window.clearTimeout(slowTimer);
+ window.clearInterval(pollTimer);
+ syncTriggers();
+ }
+
+ function checkReady() {
+ if (getReadyHost()) {
+ markReady();
+ return true;
+ }
+ return false;
+ }
+
+ function attachScriptListeners(script) {
+ if (!script || script.__dorisKapaListenersAttached) return;
+ script.__dorisKapaListenersAttached = true;
+ script.addEventListener('load', checkReady);
+ script.addEventListener('error', markError);
+ }
+
+ function requestOpen() {
+ if (state === 'error') {
+ syncTriggers();
+ return;
+ }
+ pendingOpen = true;
+ slow = false;
+ window.clearTimeout(slowTimer);
+ slowTimer = window.setTimeout(function () {
+ if (!pendingOpen || state !== 'loading') return;
+ slow = true;
+ syncTriggers();
+ }, 8000);
+ syncTriggers();
+ if (checkReady()) replayOpen();
+ }
+
+ document.addEventListener(
+ 'click',
+ function (event) {
+ if (replaying) return;
+ var target = event.target;
+ var trigger = target && target.closest ?
target.closest(TRIGGER_SELECTOR) : null;
+ if (!trigger) return;
+
+ // Once ready, Kapa's own listener handles the navbar button. The
+ // hero button still needs to proxy its click to that bound button.
+ if (state === 'ready' && trigger.id === NAVBAR_TRIGGER_ID) return;
+
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ requestOpen();
+ },
+ true,
+ );
+
+ var observer = new MutationObserver(function (mutations) {
+ mutations.forEach(function (mutation) {
+ Array.prototype.forEach.call(mutation.addedNodes, function (node) {
+ if (node.nodeType !== 1) return;
+ if (node.matches && node.matches(KAPA_SCRIPT_SELECTOR)) {
+ attachScriptListeners(node);
+ } else if (node.querySelector) {
+
attachScriptListeners(node.querySelector(KAPA_SCRIPT_SELECTOR));
+ }
+ });
+ });
+ syncTriggers();
+ checkReady();
+ });
+ observer.observe(document.documentElement, { childList: true, subtree:
true });
+
+ attachScriptListeners(document.querySelector(KAPA_SCRIPT_SELECTOR));
+ pollTimer = window.setInterval(checkReady, 250);
+ syncTriggers();
+})();
+
// Position the Kapa Ask Me modal below the sticky NavbarNext.
// Kapa renders inside a Shadow DOM on `#kapa-widget-container`, so light-DOM
// CSS can't reach it. We inject a <style> into the shadow root and re-inject
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]