From f51010bca3c69be58ecb5c9d9b749a05bd5765ea Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Tue, 28 Jul 2026 21:38:52 +0200 Subject: [PATCH 01/12] Add PlatformOverviewSection component Introduce a new PlatformOverviewSection React component (src/components/PlatformOverviewSection) with styles.module.css that shows tabbed, keyboard-accessible demo videos. Features: in-view autoplay via IntersectionObserver, prefers-reduced-motion support, media status handling and a placeholder UI while videos load. Wire the section into the homepage (import added in src/pages/index.js). Add static/video/platform-overview/README.md describing required demo MP4 files and recommended export settings. --- .../PlatformOverviewSection/index.js | 197 ++++++++++++ .../PlatformOverviewSection/styles.module.css | 288 ++++++++++++++++++ src/pages/index.js | 2 + static/video/platform-overview/README.md | 9 + 4 files changed, 496 insertions(+) create mode 100644 src/components/PlatformOverviewSection/index.js create mode 100644 src/components/PlatformOverviewSection/styles.module.css create mode 100644 static/video/platform-overview/README.md diff --git a/src/components/PlatformOverviewSection/index.js b/src/components/PlatformOverviewSection/index.js new file mode 100644 index 0000000..4832143 --- /dev/null +++ b/src/components/PlatformOverviewSection/index.js @@ -0,0 +1,197 @@ +import React, { useEffect, useRef, useState } from 'react'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import styles from './styles.module.css'; + +const demos = [ + { + id: 'clusters', + label: 'Deployment', + file: 'clusters.mp4', + description: 'Provision a production-ready PostgreSQL cluster.', + }, + { + id: 'parameters', + label: 'Parameters', + file: 'parameters.mp4', + description: 'Change cluster and system parameters from the platform interface.', + }, + { + id: 'sql-editor', + label: 'SQL Editor', + file: 'sql-editor.mp4', + description: 'Run SQL queries directly from the platform interface.', + }, +]; + +export default function PlatformOverviewSection() { + const [activeIndex, setActiveIndex] = useState(0); + const [mediaStatus, setMediaStatus] = useState('loading'); + const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); + const [isInView, setIsInView] = useState(false); + const sectionRef = useRef(null); + const tabRefs = useRef([]); + const videoRef = useRef(null); + const mediaRoot = useBaseUrl('/video/platform-overview/'); + const activeDemo = demos[activeIndex]; + + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + const updatePreference = () => setPrefersReducedMotion(mediaQuery.matches); + + updatePreference(); + mediaQuery.addEventListener?.('change', updatePreference); + return () => mediaQuery.removeEventListener?.('change', updatePreference); + }, []); + + useEffect(() => { + setMediaStatus('loading'); + }, [activeIndex]); + + useEffect(() => { + if (!sectionRef.current) return undefined; + + if (!('IntersectionObserver' in window)) { + setIsInView(true); + return undefined; + } + + const observer = new IntersectionObserver( + ([entry]) => setIsInView(entry.isIntersecting && entry.intersectionRatio >= 0.35), + { threshold: 0.35 } + ); + + observer.observe(sectionRef.current); + return () => observer.disconnect(); + }, []); + + useEffect(() => { + if (!videoRef.current) return; + + if (prefersReducedMotion || !isInView) { + videoRef.current.pause(); + return; + } + + videoRef.current.play().catch(() => {}); + }, [activeIndex, mediaStatus, prefersReducedMotion, isInView]); + + function selectTab(index) { + setActiveIndex(index); + } + + function handleVideoEnded() { + if (prefersReducedMotion || !isInView) return; + setActiveIndex((currentIndex) => (currentIndex + 1) % demos.length); + } + + function handleTabKeyDown(event, index) { + if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; + + event.preventDefault(); + let nextIndex = index; + + if (event.key === 'ArrowLeft') nextIndex = (index - 1 + demos.length) % demos.length; + if (event.key === 'ArrowRight') nextIndex = (index + 1) % demos.length; + if (event.key === 'Home') nextIndex = 0; + if (event.key === 'End') nextIndex = demos.length - 1; + + selectTab(nextIndex); + tabRefs.current[nextIndex]?.focus(); + } + + return ( +
+
+

+ + Platform overview +

+ +
+ {demos.map((demo, index) => ( + + ))} +
+ +
+ + +
+
+
+
+
+ ); +} diff --git a/src/components/PlatformOverviewSection/styles.module.css b/src/components/PlatformOverviewSection/styles.module.css new file mode 100644 index 0000000..d63fefd --- /dev/null +++ b/src/components/PlatformOverviewSection/styles.module.css @@ -0,0 +1,288 @@ +.section { + position: relative; + width: 100vw; + left: 50%; + margin-left: -50vw; + background: var(--color-bg); +} + +.inner { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 48px 80px 0; +} + +.sectionLabel { + display: flex; + align-items: center; + gap: 8px; + margin: 0 0 24px; + color: var(--color-text-muted); + font-size: 13px; + font-weight: var(--fw-regular); + letter-spacing: 0.08em; + line-height: 1; + text-transform: uppercase; +} + +.sectionLabel span, +.placeholderPrompt { + color: var(--color-primary); +} + +.tabPrompt { + color: var(--color-text-muted); +} + +.tabs { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; + overflow-x: auto; + scrollbar-width: none; +} + +.tabs::-webkit-scrollbar { + display: none; +} + +.tab { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 38px; + padding: 0 16px; + border: 1px solid var(--color-border); + border-radius: 0; + background: transparent; + color: var(--color-text-muted); + font-family: var(--font-base); + font-size: 14px; + font-weight: var(--fw-regular); + line-height: 1; + white-space: nowrap; + cursor: pointer; +} + +.tab:hover { + border-color: var(--color-text-muted); + color: var(--color-text); +} + +.tabActive, +.tabActive:hover { + border-color: var(--color-border); + color: var(--color-text); + box-shadow: inset 0 -2px 0 var(--color-primary); +} + +.tabActive .tabPrompt { + color: var(--color-primary); +} + +.tab:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 3px; +} + +.browser { + overflow: hidden; + border: 1px solid var(--color-border); + border-radius: 10px 10px 0 0; + background: var(--color-bg); + box-shadow: 8px 8px 0 var(--shadow-pixel); +} + +.browserBar { + display: flex; + align-items: center; + min-height: 38px; + padding: 0 16px; + border-bottom: 1px solid var(--color-border); + background: var(--color-surface); +} + +.browserControls { + display: flex; + gap: 7px; +} + +.browserControls span { + width: 8px; + height: 8px; + border: 1px solid var(--color-text-muted); + opacity: 0.5; +} + +.browserControls span:first-child { + border-color: var(--color-primary); + background: var(--color-primary); + opacity: 1; +} + +.viewport { + position: relative; + aspect-ratio: 16 / 9; + overflow: hidden; + background: color-mix(in srgb, var(--color-text) 2%, var(--color-bg)); +} + +.video { + position: absolute; + inset: 0; + z-index: 2; + display: block; + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0; +} + +.videoReady { + opacity: 1; +} + +.placeholder { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + padding: 24px; + color: var(--color-text-muted); +} + +.placeholderHeader { + display: flex; + align-items: center; + gap: 8px; + padding-bottom: 16px; + border-bottom: 1px dashed var(--color-border); + font-size: 11px; + letter-spacing: 0.08em; +} + +.placeholderState { + margin-left: auto; + color: var(--color-primary); +} + +.placeholderBody { + display: grid; + grid-template-columns: 96px 1fr; + flex: 1; + min-height: 0; +} + +.mockSidebar { + display: flex; + flex-direction: column; + align-items: center; + gap: 22px; + padding: 28px 18px; + border-right: 1px solid var(--color-border); +} + +.mockSidebar span { + width: 28px; + height: 7px; + background: var(--color-border); +} + +.mockSidebar .mockActive { + background: var(--color-primary); +} + +.mockContent { + position: relative; + padding: 30px 34px; +} + +.mockTitle { + max-width: 620px; + color: var(--color-text); + font-size: clamp(16px, 2vw, 24px); + line-height: 1.35; +} + +.mockToolbar { + display: flex; + gap: 10px; + margin: 26px 0 20px; +} + +.mockToolbar span { + width: 92px; + height: 26px; + border: 1px solid var(--color-border); +} + +.mockToolbar span:first-child { + border-color: var(--color-primary); +} + +.mockGrid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.mockGrid span { + aspect-ratio: 1.65; + border: 1px solid var(--color-border); + background: var(--color-bg); +} + +.mediaHint { + position: absolute; + right: 34px; + bottom: 24px; + color: var(--color-text-muted); + font-size: 10px; + letter-spacing: 0.05em; + opacity: 0.7; +} + +@media (max-width: 1100px) { + .inner { padding: 44px 48px 0; } +} + +@media (max-width: 768px) { + .inner { padding: 36px 24px 0; } + .tabs { gap: 8px; } + .tab { min-height: 36px; padding: 0 12px; font-size: 13px; } + .placeholder { padding: 16px; } + .placeholderBody { grid-template-columns: 58px 1fr; } + .mockSidebar { gap: 16px; padding: 20px 12px; } + .mockSidebar span { width: 22px; } + .mockContent { padding: 22px; } + .mockGrid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } + .mockGrid span:nth-child(n+7) { display: none; } + .mediaHint { right: 22px; bottom: 16px; } +} + +@media (max-width: 480px) { + .inner { padding: 32px 16px 0; } + .sectionLabel { margin-bottom: 18px; } + .browser { border-radius: 7px 7px 0 0; box-shadow: 5px 5px 0 var(--shadow-pixel); } + .browserBar { min-height: 34px; padding: 0 10px; } + .browserControls { gap: 4px; } + .browserControls span { width: 6px; height: 6px; } + .placeholderHeader { padding-bottom: 10px; font-size: 9px; } + .placeholderBody { grid-template-columns: 42px 1fr; } + .mockSidebar { gap: 10px; padding: 14px 8px; } + .mockSidebar span { width: 18px; height: 5px; } + .mockContent { padding: 14px; } + .mockToolbar { margin: 14px 0 12px; } + .mockToolbar span { width: 54px; height: 18px; } + .mockGrid { gap: 6px; } + .mediaHint { display: none; } +} + +@media (prefers-reduced-motion: reduce) { + .video { + animation: none; + } +} diff --git a/src/pages/index.js b/src/pages/index.js index 7686c92..6a8ac7b 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -2,6 +2,7 @@ import React from 'react'; import Layout from '@theme/Layout'; import HeroSection from '@site/src/components/HeroSection'; import ArchDiagramSection from '@site/src/components/ArchDiagramSection'; +import PlatformOverviewSection from '@site/src/components/PlatformOverviewSection'; import ValuePropsSection from '@site/src/components/ValuePropsSection'; import ProductHighlightsSection from '@site/src/components/ProductHighlightsSection'; import CLISection from '@site/src/components/CLISection'; @@ -17,6 +18,7 @@ export default function Home() {
+ {/* */} diff --git a/static/video/platform-overview/README.md b/static/video/platform-overview/README.md new file mode 100644 index 0000000..237fde2 --- /dev/null +++ b/static/video/platform-overview/README.md @@ -0,0 +1,9 @@ +# Platform overview videos + +Place the locally hosted platform demo videos in this directory: + +- `clusters.mp4` +- `parameters.mp4` +- `sql-editor.mp4` + +Recommended export: MP4 (H.264), 16:9, muted, 1080p or 1440p, optimized for web playback. From a9e68fe37dfcc069516b02c56d7d32624912ecf7 Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Tue, 28 Jul 2026 22:24:33 +0200 Subject: [PATCH 02/12] Support dark-mode platform demo videos Use Docusaurus useColorMode to load a dark-themed video file when in dark mode (appends .dark.mp4). Update video key/src to include color mode so the player reloads on theme change, and add colorMode to effect dependencies. Update media hint and README to document the .dark.mp4 variants for each demo. --- src/components/PlatformOverviewSection/index.js | 15 ++++++++++----- static/video/platform-overview/README.md | 3 +++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/PlatformOverviewSection/index.js b/src/components/PlatformOverviewSection/index.js index 4832143..91b2c9a 100644 --- a/src/components/PlatformOverviewSection/index.js +++ b/src/components/PlatformOverviewSection/index.js @@ -1,5 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import useBaseUrl from '@docusaurus/useBaseUrl'; +import { useColorMode } from '@docusaurus/theme-common'; import styles from './styles.module.css'; const demos = [ @@ -31,8 +32,12 @@ export default function PlatformOverviewSection() { const sectionRef = useRef(null); const tabRefs = useRef([]); const videoRef = useRef(null); + const { colorMode } = useColorMode(); const mediaRoot = useBaseUrl('/video/platform-overview/'); const activeDemo = demos[activeIndex]; + const themedFile = colorMode === 'dark' + ? activeDemo.file.replace(/\.mp4$/, '.dark.mp4') + : activeDemo.file; useEffect(() => { const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); @@ -45,7 +50,7 @@ export default function PlatformOverviewSection() { useEffect(() => { setMediaStatus('loading'); - }, [activeIndex]); + }, [activeIndex, colorMode]); useEffect(() => { if (!sectionRef.current) return undefined; @@ -73,7 +78,7 @@ export default function PlatformOverviewSection() { } videoRef.current.play().catch(() => {}); - }, [activeIndex, mediaStatus, prefersReducedMotion, isInView]); + }, [activeIndex, themedFile, mediaStatus, prefersReducedMotion, isInView]); function selectTab(index) { setActiveIndex(index); @@ -144,10 +149,10 @@ export default function PlatformOverviewSection() { className={styles.viewport} >