Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"mermaid": "^11.12.1",
"remark-directive": "^4.0.0",
"sharp": "^0.34.5",
"starlight-contributor-list": "^0.3.1",
"starlight-contributor-list": "^0.3.2",
"starlight-github-alerts": "^0.1.1",
"starlight-image-zoom": "^0.13.2",
"starlight-kbd": "^0.3.0",
Expand Down
4,804 changes: 1,154 additions & 3,650 deletions src/frontend/pnpm-lock.yaml

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions src/frontend/src/components/IconLinkCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const isImageIcon = !isStringIcon && icon && typeof icon === 'object';
position: absolute;
inset: 0;
}

.sl-link-card .title {
display: flex;
align-items: center;
Expand Down Expand Up @@ -131,6 +132,16 @@ const isImageIcon = !isStringIcon && icon && typeof icon === 'object';
border-color: var(--sl-color-gray-2);
}

.sl-link-card:focus-within {
outline: 0.15rem solid;
outline-offset: 0.15rem;
}

.sl-link-card a:focus,
.sl-link-card a:focus-visible {
outline: none;
}

.sl-link-card:hover .icon {
color: var(--sl-color-white);
}
Expand Down
184 changes: 183 additions & 1 deletion src/frontend/src/components/IntegrationGrid.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
import { Image } from 'astro:assets';
import PauseIcon from '@assets/icons/pause.svg';
import PlayIcon from '@assets/icons/play.svg';

import adminerIcon from '@assets/icons/adminer-icon.png';
import activeMqIcon from '@assets/icons/activemq-icon.png';
Expand Down Expand Up @@ -274,7 +276,7 @@ const infiniteIconsRow2 = [...row2, ...row2];
const infiniteIconsRow3 = [...row3, ...row3];
---

<div class="infinite-scroll-container">
<div class="infinite-scroll-container" data-integration-grid>
<!-- Row 1: Right to Left -->
<div class="scroll-row scroll-rtl">
<div class="scroll-content">
Expand Down Expand Up @@ -355,8 +357,107 @@ const infiniteIconsRow3 = [...row3, ...row3];
}
</div>
</div>
<div class="integration-controls">
<button
type="button"
class="control-btn pause-btn"
aria-label="Pause animation"
aria-pressed="false"
data-pause-btn
>
<span class="icon-pause" aria-hidden="true"><PauseIcon width="24" height="24" /></span>
<span class="icon-play" aria-hidden="true"><PlayIcon width="24" height="24" /></span>
</button>
</div>
</div>

<script is:inline>
(function() {
const container = document.querySelector('[data-integration-grid]');
if (!container) return;

const pauseBtn = container.querySelector('[data-pause-btn]');
if (!pauseBtn) return;

const scrollContents = container.querySelectorAll('.scroll-content');
const scrollRows = container.querySelectorAll('.scroll-row');
const links = container.querySelectorAll('.scroll-row a.img');

let isPaused = false;

function updateState() {
// Update button state (CSS handles icon visibility via aria-pressed)
pauseBtn.setAttribute('aria-pressed', isPaused.toString());
pauseBtn.setAttribute('aria-label', isPaused ? 'Play animation' : 'Pause animation');

// Toggle container class for CSS-based pausing and scrolling
container.classList.toggle('is-paused', isPaused);

// Reset scroll positions when resuming playback
if (!isPaused) {
scrollRows.forEach((row) => {
row.scrollLeft = 0;
});
}

// Toggle animation - only set inline style when paused,
// otherwise remove it to let CSS hover rules work
scrollContents.forEach((content) => {
if (isPaused) {
content.style.animationPlayState = 'paused';
} else {
content.style.removeProperty('animation-play-state');
}
});

// Update tabindex for links - when paused, make all links tabbable
// When playing, only first few are tabbable to reduce tab stops
links.forEach((link, index) => {
if (isPaused) {
link.setAttribute('tabindex', '0');
} else {
// When animating, limit tab stops to reduce confusion
link.setAttribute('tabindex', index < 6 ? '0' : '-1');
}
});
}

// Scroll focused link into view when paused, and auto-pause on focus
links.forEach((link) => {
link.addEventListener('focus', () => {
// Auto-pause when user tabs into integrations (helps screen reader & keyboard users)
if (!isPaused) {
isPaused = true;
updateState();
}
link.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
});
});

pauseBtn.addEventListener('click', () => {
isPaused = !isPaused;
updateState();
});

// Respect prefers-reduced-motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
if (prefersReducedMotion.matches) {
isPaused = true;
}

// Listen for changes to reduced motion preference
prefersReducedMotion.addEventListener('change', (e) => {
if (e.matches) {
isPaused = true;
updateState();
}
});

// Initialize state
updateState();
})();
</script>

<style>
.infinite-scroll-container {
width: 100%;
Expand All @@ -367,6 +468,59 @@ const infiniteIconsRow3 = [...row3, ...row3];
gap: 1rem;
}

.integration-controls {
display: flex;
justify-content: flex-start;
}

.control-btn {
--carousel-primary: var(--aspire-color-primary);
background: var(--carousel-primary);
color: #fff;
border: none;
border-radius: 0.5rem;
width: 42px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition:
background 0.25s ease,
transform 0.25s ease;
}

.control-btn:hover {
transform: scale(1.06);
}

.control-btn:focus-visible {
outline: 0.1875rem solid var(--carousel-primary);
outline-offset: 0.125rem;
}

.icon-pause,
.icon-play {
display: flex;
align-items: center;
justify-content: center;
line-height: 0;
}

/* Hide play icon by default (showing pause) */
.icon-play {
display: none;
}

/* When paused, show play and hide pause */
.control-btn[aria-pressed='true'] .icon-pause {
display: none;
}

.control-btn[aria-pressed='true'] .icon-play {
display: flex;
}

.scroll-row {
display: flex;
overflow: hidden;
Expand Down Expand Up @@ -488,4 +642,32 @@ const infiniteIconsRow3 = [...row3, ...row3];
gap: 0.5rem;
}
}

/* Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
.scroll-content {
animation: none !important;
transform: none !important;
}

.scroll-row {
overflow-x: auto;
scrollbar-width: thin;
mask-image: none;
-webkit-mask-image: none;
}
}

/* When paused via button, allow scrolling */
.infinite-scroll-container.is-paused .scroll-row {
overflow-x: auto;
scrollbar-width: thin;
mask-image: none;
-webkit-mask-image: none;
}

.infinite-scroll-container.is-paused .scroll-content {
animation: none !important;
transform: none !important;
}
</style>
36 changes: 36 additions & 0 deletions src/frontend/src/components/LoopingVideo.astro
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const uid = globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slic
loop
muted
playsinline
tabindex="0"
{controls}
title={title}
data-sources={sourcesJson}
Expand Down Expand Up @@ -160,6 +161,29 @@ const uid = globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slic
video.addEventListener('click', togglePlayback);
}

// Keyboard accessibility: toggle playback on Enter/Space when video is focused
video.addEventListener('keydown', (e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
togglePlayback();
}
});

// Show native controls when video receives keyboard focus (for a11y)
const hasExplicitControls = video.hasAttribute('controls');
video.addEventListener('focus', (e) => {
// Only show controls if focus came from keyboard (not mouse click)
if (e.target === video && !video.matches(':hover')) {
video.setAttribute('controls', '');
}
});
video.addEventListener('blur', () => {
// Restore original controls state when focus leaves
if (!hasExplicitControls) {
video.removeAttribute('controls');
}
});

video.addEventListener('play', syncButton);
video.addEventListener('pause', syncButton);
syncButton();
Expand Down Expand Up @@ -254,6 +278,18 @@ const uid = globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slic
}
}

/* Show controls when video or toggle button is keyboard focused */
.looping-video-wrapper:focus-within .looping-video-toggle {
opacity: 1;
pointer-events: auto;
}

/* Visual focus indicator on the video itself */
video.looping-video:focus-visible {
outline: 2px solid var(--aspire-color-primary, #6d4aff);
outline-offset: 2px;
}

/* Mobile / touch: smaller, only when force-visible (after interaction) */
@media (hover: none) {
.looping-video-toggle {
Expand Down
8 changes: 7 additions & 1 deletion src/frontend/src/components/starlight/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,19 @@ import { isHomepage } from "@utils/helpers";
gap: 1rem;
align-items: center;
}

.social-icons::after {
content: "";
height: 2rem;
border-inline-end: 1px solid var(--sl-color-gray-5);
}

@media (max-width: 1200px) {
:global(.social-icons) {
display: none !important;
}
}

@media (min-width: 50rem) {
:global(:root[data-has-sidebar]) {
--__sidebar-pad: calc(2 * var(--sl-nav-pad-x));
Expand Down
Loading
Loading