Merge branch 'eclipse-main' into bug6150

This commit is contained in:
Axel Uhl committed 2026-10-12 21:55:06 +02:00
commit 999be66b10
6 files changed
+467 -32

No files matched your search

+467 -32
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Eclipse Azimuth Sailing Analytics — Developer Hub</title>
<title>Eclipse Azimuth Sailing Analytics - Developer Hub</title>
<meta name="description" content="Open-source platform for live visualization, analysis, and broadcasting of sailing regattas. Proven at the 2020 and 2024 Olympics.">
<link rel="icon" href="source/favicon-light.png" type="image/png" media="(prefers-color-scheme: light)">
<link rel="icon" href="source/favicon-dark.png" type="image/png" media="(prefers-color-scheme: dark)">
@@ -156,10 +156,10 @@
/* ════════════ SOLUTIONS CAROUSEL ════════════ */
.solutions__carousel-wrap {
position: relative; margin-top: 48px;
position: relative; margin-top: 40px;
}
.solutions__track-outer {
overflow: hidden; border-radius: var(--radius-lg);
overflow: hidden; border-radius: var(--radius-lg); padding-top: 8px;
}
.solutions__track {
display: flex; gap: 20px;
@@ -430,7 +430,7 @@
}
.getting-started .tech-item span { color: rgba(255,255,255,.9); }
.getting-started .tech-item small { color: rgba(255,255,255,.5); }
/* Java and Jetty use dark #333 — invert to white on dark bg */
/* Java and Jetty use dark #333 - invert to white on dark bg */
.getting-started .tech-item--invert img { filter: brightness(0) invert(1) opacity(.85); }
/* ════════════ DOCS GRAPH ════════════ */
@@ -503,7 +503,7 @@
}
/* Floating close button for open accordions */
.accordion-close-fab { display: none; }
/* Sticky "Close all" bar — fixed, only visible while an accordion in #whatsnew is open */
/* Sticky "Close all" bar - fixed, only visible while an accordion in #whatsnew is open */
.whatsnew__header {
display: flex; align-items: flex-end; justify-content: space-between;
flex-wrap: wrap; gap: 12px; margin-bottom: 0;
@@ -527,7 +527,7 @@
.whatsnew__close-btn:hover { background: var(--blue-dark); }
/* ════════════ GETTING STARTED ════════════ */
.getting-started { background: var(--dark); color: var(--white); }
.getting-started { background: #192533; color: var(--white); }
.getting-started .eyebrow { color: var(--yellow); }
.getting-started h2 { color: var(--white); }
.getting-started .lead { color: rgba(255,255,255,.7); }
@@ -567,6 +567,193 @@
.ck { color: #4caf50; }
.cv { color: var(--yellow); }
/* ════════════ REVIEWS ════════════ */
.reviews {
background: #e8ecf0;
position: relative; overflow: hidden;
}
.reviews::before {
content: ''; position: absolute; inset: 0; pointer-events: none;
background:
radial-gradient(ellipse 700px 400px at 0% 60%, rgba(0,143,204,.07) 0%, transparent 70%),
radial-gradient(ellipse 500px 400px at 100% 20%, rgba(239,170,0,.05) 0%, transparent 70%);
}
.reviews .eyebrow { color: var(--blue); }
.reviews h2 { color: var(--text); }
.reviews .lead { color: var(--muted); }
.reviews__ticker-wrap {
position: relative; margin-top: 44px;
overflow-x: auto; overflow-y: visible;
padding-top: 8px;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
cursor: grab;
}
.reviews__ticker-wrap:active { cursor: grabbing; }
/* hide scrollbar */
.reviews__ticker-wrap::-webkit-scrollbar { display: none; }
.reviews__ticker-wrap { -ms-overflow-style: none; scrollbar-width: none; }
.reviews__ticker {
display: flex; gap: 24px;
width: max-content;
}
.rev-card {
flex-shrink: 0; width: 340px;
background: var(--white);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 26px 26px 22px;
position: relative;
display: flex; flex-direction: column;
cursor: pointer;
box-shadow: var(--shadow-sm);
transition: border-color .2s, transform .2s, box-shadow .2s;
}
.rev-card:hover {
border-color: rgba(0,143,204,.4);
box-shadow: var(--shadow-md);
transform: translateY(-3px);
}
.rev-card__quote-mark {
font-size: 4rem; line-height: 1; color: var(--blue);
opacity: .12; position: absolute; top: 14px; right: 16px;
font-family: Georgia, serif; user-select: none;
}
.rev-card__body-wrap {
height: 120px; overflow: hidden; position: relative; margin-bottom: 18px;
}
.rev-card__body-wrap.overflows::after {
content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 56px;
background: linear-gradient(to bottom, transparent, var(--white));
pointer-events: none;
}
.rev-card__body-wrap.overflows .rev-card__chevron {
display: flex;
}
.rev-card__chevron {
display: none;
position: absolute; bottom: 4px; left: 0; right: 0;
justify-content: center;
color: var(--blue); font-size: 1.5rem; line-height: 1;
pointer-events: none;
z-index: 4;
opacity: .6;
transition: opacity .2s, bottom .2s;
}
.rev-card:hover .rev-card__chevron { opacity: 1; bottom: 7px; }
.rev-card__body {
font-size: .875rem; color: var(--text);
line-height: 1.7; padding-right: 20px;
}
.rev-card__footer {
display: flex; align-items: center; gap: 12px;
border-top: 1px solid var(--border);
padding-top: 14px; margin-top: auto;
}
/* Ticker nav arrows */
.reviews__nav {
display: flex; align-items: center; justify-content: center;
gap: 28px; margin-top: 40px;
}
.reviews__nav-btn {
background: none; border: none; cursor: pointer;
color: rgba(0,0,0,.3);
font-size: 1.6rem; font-weight: 400; line-height: 1;
padding: 4px 10px;
letter-spacing: -1px;
transition: color .2s;
}
.reviews__nav-btn:hover { color: var(--blue); }
.rev-card__avatar {
width: 48px; height: 48px; border-radius: 50%;
object-fit: cover;
border: 2px solid rgba(0,143,204,.3);
flex-shrink: 0;
}
.rev-card__avatar--top { object-position: center top; }
.rev-card__avatar--face { object-position: center 20%; }
.rev-card__avatar-placeholder {
width: 48px; height: 48px; border-radius: 50%;
background: linear-gradient(135deg, var(--blue-dark), var(--blue));
display: flex; align-items: center; justify-content: center;
font-size: 1rem; color: var(--white); font-weight: 700;
flex-shrink: 0; border: 2px solid rgba(0,143,204,.3); line-height: 1;
}
.rev-card__name { font-size: .85rem; font-weight: 700; color: var(--dark); line-height: 1.3; }
.rev-card__role { font-size: .73rem; color: var(--muted); margin-top: 2px; }
.rev-card__org { font-size: .68rem; color: var(--blue); font-weight: 700; text-transform: uppercase; letter-spacing: .07em; margin-top: 3px; }
/* Review modal */
.rev-modal-overlay {
display: none; position: fixed; inset: 0; z-index: 1000;
background: rgba(0,0,0,.5); backdrop-filter: blur(4px);
align-items: center; justify-content: center; padding: 24px;
}
.rev-modal-overlay.open { display: flex; }
.rev-modal {
background: var(--white); border: 1px solid var(--border);
border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
width: 100%; max-width: 600px;
position: relative; animation: modalIn .2s ease;
display: flex; overflow: hidden; max-height: 90vh;
}
.rev-modal__photo {
width: 180px; flex-shrink: 0;
background: var(--surface);
overflow: hidden;
}
.rev-modal__photo img {
width: 100%; height: 100%; object-fit: cover;
}
.rev-modal__photo-placeholder {
width: 100%; height: 100%;
background: linear-gradient(135deg, var(--blue-dark), var(--blue));
display: flex; align-items: center; justify-content: center;
font-size: 3rem; color: var(--white); font-weight: 700;
}
.rev-modal__content {
flex: 1; padding: 44px 28px 24px; min-width: 0;
overflow-y: auto; position: relative;
}
.rev-modal__quote-mark {
font-size: 5rem; line-height: 1; color: var(--blue); opacity: .08;
position: absolute; top: 8px; right: 16px;
font-family: Georgia, serif; user-select: none;
}
.rev-modal__body {
font-size: .9rem; color: var(--text);
line-height: 1.75; margin-bottom: 20px;
}
.rev-modal__footer {
display: flex; align-items: center; gap: 14px;
border-top: 1px solid var(--border); padding-top: 16px;
}
.rev-modal__close {
position: absolute; top: 10px; right: 10px;
background: rgba(0,0,0,.06); border: 1px solid rgba(0,0,0,.1);
color: var(--muted); width: 28px; height: 28px; border-radius: 50%;
font-size: 1rem; cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: background .15s; z-index: 2; flex-shrink: 0;
}
.rev-modal__close:hover { background: rgba(0,0,0,.12); color: var(--text); }
/* Review maintenance: one adaptive layout for short and long feedback. */
.rev-card { width: min(340px, calc(100vw - 48px)); }
.rev-card:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }
.rev-modal { max-width: min(760px, calc(100vw - 32px)); min-height: 240px; }
.rev-modal__photo { width: clamp(150px, 30%, 230px); align-self: stretch; }
.rev-modal__photo img { object-position: center 20%; }
.rev-modal__content { padding: 36px 30px 26px; }
@media (max-width: 620px) {
.rev-modal { flex-direction: column; overflow-y: auto; max-height: 90dvh; }
.rev-modal__photo { width: 100%; height: 190px; flex: 0 0 190px; }
.rev-modal__photo img { object-fit: cover; }
.rev-modal__content { padding: 24px 22px; overflow: visible; }
}
/* ════════════ FOOTER ════════════ */
.footer { background: var(--dark); color: rgba(255,255,255,.6); padding: 56px 0 28px; }
.footer__grid {
@@ -629,6 +816,7 @@
</a>
<div class="nav__links">
<a href="#solutions">Solutions</a>
<a href="#reviews">Reviews</a>
<a href="#getting-started">Quick Start</a>
<a href="#docs">Documentation</a>
<a href="#tech">Technology</a>
@@ -650,7 +838,7 @@
<h1>Open-source analytics for <span>competitive sailing</span></h1>
<p class="hero__sub">
Eclipse Azimuth Sailing Analytics is a community-driven platform for live GPS tracking,
real-time race analysis, and fan engagement — from club regattas to the Olympic Games.
real-time race analysis, and fan engagement - from club regattas to the Olympic Games.
Free to use, free to contribute.
</p>
<div class="hero__actions">
@@ -702,7 +890,7 @@
<div class="sol-card__body">
<span class="sol-card__tag sol-card__tag--web">Web Platform</span>
<h3>Eclipse Azimuth Sailing Analytics</h3>
<p>Real-time GPS analytics for rankings, speeds, maneuvers, and bearings — from club regattas to the Olympic Games. Processes live telemetry in the cloud and visualizes it in multiple frontends.</p>
<p>Real-time GPS analytics for rankings, speeds, maneuvers, and bearings - from club regattas to the Olympic Games. Processes live telemetry in the cloud and visualizes it in multiple frontends.</p>
<div class="sol-card__links">
<button class="btn btn-primary" onclick="openModal('modal-sa')">Learn More</button>
<a class="sol-card__store" href="https://www.sapsailing.com/gwt/Home.html" target="_blank" rel="noopener"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>Open App</a>
@@ -788,6 +976,30 @@
</div>
</section>
<!-- ═══════════════════════════════════ REVIEWS ══════════════════════════════ -->
<section class="section reviews" id="reviews">
<div class="container">
<p class="eyebrow">User Feedback 2026</p>
<h2>Trusted by the sailing world</h2>
<p class="lead" style="max-width:560px;margin-top:10px;">
From club regattas to international championships - what organizers, coaches, and sailors say.
</p>
</div>
<div class="reviews__ticker-wrap">
<div class="reviews__ticker" id="rev-ticker">
<!-- Cards are generated from REVIEWS below. Add future feedback there only. -->
</div>
</div>
<div class="container">
<div class="reviews__nav">
<button class="reviews__nav-btn" onclick="revTickerStep(-1)">&#8249;</button>
<button class="reviews__nav-btn" onclick="revTickerStep(1)">&#8250;</button>
</div>
</div>
</section>
<!-- ═══════════════════════════════════ GETTING STARTED ═══════════════════════ -->
<section class="section getting-started" id="getting-started">
<div class="container">
@@ -833,7 +1045,7 @@
<div style="display:flex;gap:14px;flex-wrap:wrap;align-items:center;margin-top:28px;">
<a class="btn btn-yellow" href="https://wiki.sapsailing.com/wiki/howto/onboarding.md" target="_blank" rel="noopener">Full Onboarding Guide</a>
<a class="btn btn-outline-white" href="https://github.com/eclipse-sailing-analytics/sailing-analytics/releases" target="_blank" rel="noopener">Latest Release</a>
<a class="btn btn-outline-white" href="https://github.com/eclipse-sailing-analytics/sailing-analytics/pkgs/container/sailing-analytics" target="_blank" rel="noopener">Docker Image</a>
<a class="btn btn-outline-white" href="https://github.com/eclipse-sailing-analytics/sailing-analytics/releases?q=docker&expanded=true" target="_blank" rel="noopener">Docker Image</a>
<span style="margin-left:auto;display:inline-flex;align-items:center;gap:8px;padding:9px 16px;background:rgba(239,170,0,.12);border:1px solid rgba(239,170,0,.35);border-radius:var(--radius);border-left:3px solid var(--yellow);">
<span style="font-size:.9rem;font-weight:900;color:var(--yellow);line-height:1;">!</span>
<span style="font-size:.82rem;color:rgba(255,255,255,.8);">Found a bug or have a feature request? Use the <a href="https://github.com/eclipse-sailing-analytics/sailing-analytics/issues/" target="_blank" rel="noopener" style="color:var(--yellow);font-weight:700;">issue tracker</a> on GitHub.</span>
@@ -871,7 +1083,7 @@
<div class="tech-item">
<img src="https://cdn.simpleicons.org/eclipseide/2C2255" alt="OSGi / Eclipse">
<span>OSGi / Equinox</span>
<small>Modular plugin architecture — 225+ bundles</small>
<small>Modular plugin architecture - 225+ bundles</small>
</div>
<div class="tech-item">
<img src="source/logo-gwt.svg" alt="GWT">
@@ -901,7 +1113,7 @@
<div class="tech-item">
<img src="source/logo-aws.svg" alt="AWS">
<span>AWS</span>
<small>Cloud hosting — EC2, S3, CloudFront</small>
<small>Cloud hosting - EC2, S3, CloudFront</small>
</div>
<div class="tech-item">
<img src="https://cdn.simpleicons.org/android/34A853" alt="Android">
@@ -1024,13 +1236,30 @@
</div>
</section>
<!-- Review dialog is populated automatically from REVIEWS. -->
<div class="rev-modal-overlay" id="review-dialog" role="presentation">
<div class="rev-modal" role="dialog" aria-modal="true" aria-labelledby="review-dialog-name">
<button type="button" class="rev-modal__close" aria-label="Close review">&times;</button>
<div class="rev-modal__photo" id="review-dialog-photo"></div>
<div class="rev-modal__content">
<div class="rev-modal__quote-mark" aria-hidden="true">&ldquo;</div>
<p class="rev-modal__body" id="review-dialog-quote"></p>
<div class="rev-modal__footer"><div>
<div class="rev-card__name" id="review-dialog-name"></div>
<div class="rev-card__role" id="review-dialog-role"></div>
<div class="rev-card__org" id="review-dialog-org"></div>
</div></div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════ FOOTER ════════════════════════════════ -->
<footer class="footer">
<div class="container">
<div class="footer__grid">
<div class="footer__brand">
<img src="source/logo-azimuth-no-bg.png" alt="Azimuth" style="height:80px;width:auto;margin-bottom:4px;">
<p>Open-source regatta analytics platform — live GPS tracking, race analysis, and fan engagement. Proven at the Olympic Games.</p>
<p>Open-source regatta analytics platform - live GPS tracking, race analysis, and fan engagement. Proven at the Olympic Games.</p>
<div class="footer__social">
<a href="https://github.com/eclipse-sailing-analytics/sailing-analytics" target="_blank" rel="noopener" title="GitHub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="white"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
@@ -1047,11 +1276,21 @@
<li><a href="#solutions">Solutions</a></li>
<li><a href="#getting-started">Quick Start</a></li>
<li><a href="#whatsnew">What's New</a></li>
<li><a href="#reviews">Reviews</a></li>
<li><a href="https://www.sapsailing.com/gwt/Home.html" target="_blank" rel="noopener">Application</a></li>
<li><a href="https://github.com/eclipse-sailing-analytics/sailing-analytics/releases" target="_blank" rel="noopener">Releases</a></li>
</ul>
</div>
<div class="footer__col">
<h4>Community</h4>
<ul>
<li><a href="https://github.com/eclipse-sailing-analytics/sailing-analytics" target="_blank" rel="noopener">GitHub</a></li>
<li><a href="https://github.com/eclipse-sailing-analytics/sailing-analytics/blob/main/CONTRIBUTING.md" target="_blank" rel="noopener">Contributing</a></li>
<li><a href="https://wiki.sapsailing.com/wiki/howto/tutorials/home.md" target="_blank" rel="noopener">Support</a></li>
</ul>
</div>
<div class="footer__col">
<h4>Developers</h4>
<ul>
@@ -1062,15 +1301,6 @@
<li><a href="https://github.com/eclipse-sailing-analytics/sailing-analytics/issues/" target="_blank" rel="noopener">Issue Tracker</a></li>
</ul>
</div>
<div class="footer__col">
<h4>Community</h4>
<ul>
<li><a href="https://github.com/eclipse-sailing-analytics/sailing-analytics" target="_blank" rel="noopener">GitHub</a></li>
<li><a href="https://github.com/eclipse-sailing-analytics/sailing-analytics/blob/main/CONTRIBUTING.md" target="_blank" rel="noopener">Contributing</a></li>
<li><a href="https://wiki.sapsailing.com/wiki/howto/tutorials/home.md" target="_blank" rel="noopener">Support</a></li>
</ul>
</div>
</div>
<div class="footer__bottom">
@@ -1107,7 +1337,7 @@
<div class="modal__card-icon modal__card-icon--blue">🚀</div>
<div class="modal__card-text">
<div class="modal__card-title">Open Application</div>
<div class="modal__card-desc">sapsailing.com — real events, real data</div>
<div class="modal__card-desc">sapsailing.com - real events, real data</div>
</div>
<span class="modal__card-arrow">→</span>
</a>
@@ -1451,7 +1681,116 @@
]},
];
// ── Solutions carousel ────────────────────────────────────────────────────
// ══════════ EDIT ONLY THE REVIEWS ARRAY ══════════
// Add another { id, name, role, org, quote, photo?, photoPosition? } entry.
// Photos live in source/. No manual HTML cards, modal sizes or handlers needed.
// photoPosition (optional) is CSS object-position, e.g. 'center 20%'.
const REVIEWS = [
{"id": "frank", "name": "Frank Schärffe", "role": "Managing Director", "org": "Travemünder Woche gGmbH", "photo": "source/frank_scharffe.png", "photoPosition": "center 12%", "quote": "With SAP's return to processing tracked data at Travemünder Woche 2026, 42 races across 9 boat classes could be processed in a graphically appealing way and presented attractively for the interested public. An excellent instrument for our participants to analyse their own performance - a genuine added value! For the sailing-enthusiast public, the data was incorporated into the live broadcast on an LED large screen and provided excellent background information for the commentary - overall, we were able to make the regatta sport of Travemünder Woche more viewer-friendly!"},
{"id": "sven-christensen", "name": "Sven Christensen", "role": "CEO, Point of Sailing - Sailing promotion & staging", "org": "Kieler Woche", "photo": "source/sven_christensen.png", "photoPosition": "center 15%", "quote": "SAP Sailing Analytics is an extremely helpful tool for Kieler Woche in many respects. Not only the analysis itself, but also the presentation and the depth of information behind it help us convey sailing in near real-time in a way that is broadly understandable and exciting. Beyond that, the data provides participants and coaches with valuable opportunities to analyse performance and decisions."},
{"id": "stanislaw", "name": "Stanisław Konarzewski", "role": "Marketing & Communication Manager", "org": "Polish Sailing League", "photo": "source/stanislav_konarzewski.jpg", "photoPosition": "45% 22%", "quote": "As the organizers of the Polish Sailing League, integrating SAP Sailing Analytics has been a complete game-changer for our fast-paced regattas. The system's live tracking, complete with real-time data, effortlessly translates complex tactical decisions into an engaging, easily digestible experience for our fans and commentators. In a format featuring back-to-back flights, the automated, real-time scoring is invaluable; leaderboards update instantly, eliminating manual calculation delays and ensuring absolute accuracy. From an operational standpoint, the platform is intuitive and easy to use. Deploying the GPS units across our one-design fleet adds no friction to our morning routines, while the Race Committee backend makes virtual course setting and safety monitoring seamless. Combined with the in-depth post-race analytics, SAP Sailing is much more than just a tracking tool - it is a robust, indispensable platform that has fundamentally elevated the professionalism, operational efficiency, and broadcast quality of our entire league."},
{"id": "jack", "name": "Jack V.", "role": "Pacific Northwest Sailor", "org": "Sidney North Saanich Yacht Club", "quote": "SAP Sailing Analytics strikes a unique balance of being accessible to any average club racer on a budget, and powerful enough to satisfy the most complex regatta or performance analytics far better than its competitors. We use it for all sorts of events and are always discovering new and interesting features to take advantage of."},
{"id": "sven-jurgensen", "name": "Sven Jürgensen", "role": "Chairman, Wir sind Wir Inclusion in Sailing e.V.", "org": "Helga Cup", "photo": "source/sven_jurgensen.png", "photoPosition": "center 22%", "quote": "More than 20 J/70 boats, well over 60 crews, three racing groups: results, tracking, live broadcasts, commentary and analyses for the crews are almost unimaginable at the Helga Cup without SAP Sailing Analytics. The teams even use data from previous years to prepare specifically for the regatta."}
];
(function initReviews() {
const ticker = document.getElementById('rev-ticker');
const dialog = document.getElementById('review-dialog');
const photoBox = document.getElementById('review-dialog-photo');
const close = dialog.querySelector('.rev-modal__close');
let previousFocus = null;
let selected = null;
function avatar(review, large) {
if (review.photo) {
const img = document.createElement('img');
img.src = review.photo;
img.alt = review.name;
img.loading = large ? 'eager' : 'lazy';
img.style.objectPosition = review.photoPosition || 'center 20%';
if (!large) img.className = 'rev-card__avatar';
return img;
}
const placeholder = document.createElement('div');
placeholder.className = large ? 'rev-modal__photo-placeholder' : 'rev-card__avatar-placeholder';
placeholder.textContent = review.name.charAt(0).toUpperCase();
return placeholder;
}
function openReview(review) {
selected = review;
previousFocus = document.activeElement;
document.getElementById('review-dialog-quote').textContent = review.quote;
document.getElementById('review-dialog-name').textContent = review.name;
document.getElementById('review-dialog-role').textContent = review.role || '';
document.getElementById('review-dialog-org').textContent = review.org || '';
photoBox.replaceChildren(avatar(review, true));
dialog.classList.add('open');
document.body.style.overflow = 'hidden';
close.focus();
}
function closeReview() {
dialog.classList.remove('open');
document.body.style.overflow = '';
if (previousFocus) previousFocus.focus();
selected = null;
}
close.addEventListener('click', closeReview);
dialog.addEventListener('click', e => { if (e.target === dialog) closeReview(); });
document.addEventListener('keydown', e => {
if (!selected) return;
if (e.key === 'Escape') { e.stopPropagation(); closeReview(); }
if (e.key === 'Tab') {
// Only one dialog control: do not let keyboard focus escape the dialog.
e.preventDefault(); close.focus();
}
});
REVIEWS.forEach(review => {
const card = document.createElement('div');
card.className = 'rev-card';
card.tabIndex = 0;
card.setAttribute('role', 'button');
card.setAttribute('aria-label', 'Read full feedback from ' + review.name);
const mark = document.createElement('div');
mark.className = 'rev-card__quote-mark';
mark.setAttribute('aria-hidden','true');
mark.textContent = '“';
const wrap = document.createElement('div');
wrap.className = 'rev-card__body-wrap';
const body = document.createElement('p');
body.className = 'rev-card__body';
body.textContent = review.quote;
const chevron = document.createElement('span');
chevron.className = 'rev-card__chevron';
chevron.setAttribute('aria-hidden', 'true');
chevron.textContent = '⌄';
wrap.append(body, chevron);
const footer = document.createElement('div');
footer.className = 'rev-card__footer';
const details = document.createElement('div');
[['rev-card__name',review.name],['rev-card__role',review.role],['rev-card__org',review.org]].forEach(([cls,val])=>{
const field=document.createElement('div');field.className=cls;field.textContent=val||'';details.appendChild(field);
});
footer.append(avatar(review,false),details);
card.append(mark,wrap,footer);
card.addEventListener('click',()=>openReview(review));
card.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();openReview(review);}});
ticker.appendChild(card);
});
function updateTruncation() {
ticker.querySelectorAll('.rev-card').forEach(card => {
const wrap = card.querySelector('.rev-card__body-wrap');
const body = card.querySelector('.rev-card__body');
wrap.classList.toggle('overflows', body.scrollHeight > wrap.clientHeight + 4);
});
}
requestAnimationFrame(updateTruncation);
window.addEventListener('load',updateTruncation);
window.addEventListener('resize',updateTruncation);
})();
// ── Solutions carousel ┈───────────────────────────────────────────────────
(function() {
const track = document.getElementById('sol-track');
const dotsEl = document.getElementById('sol-dots');
@@ -1591,16 +1930,17 @@
body.appendChild(div);
})
.catch(function() {
var div = document.createElement('div');
div.className = 'accordion-loading';
div.appendChild(document.createTextNode('Could not load changelog — '));
var a = document.createElement('a');
a.href = body.dataset.src;
a.target = '_blank';
a.textContent = 'view source file';
div.appendChild(a);
body.innerHTML = '';
body.appendChild(div);
const loading = document.createElement('div');
loading.className = 'accordion-loading';
loading.appendChild(document.createTextNode('Could not load changelog - '));
const link = document.createElement('a');
link.href = body.dataset.src || '#';
link.target = '_blank';
link.rel = 'noopener';
link.textContent = 'view source file';
loading.appendChild(link);
body.appendChild(loading);
});
}
}
@@ -1643,6 +1983,101 @@
}
}
(function() {
var wrap = document.querySelector('.reviews__ticker-wrap');
if (!wrap) return;
var isDragging = false, startX, scrollLeft;
wrap.addEventListener('mousedown', function(e) {
isDragging = true;
startX = e.pageX - wrap.offsetLeft;
scrollLeft = wrap.scrollLeft;
wrap.style.userSelect = 'none';
});
document.addEventListener('mouseup', function() {
isDragging = false;
wrap.style.userSelect = '';
});
document.addEventListener('mousemove', function(e) {
if (!isDragging) return;
e.preventDefault();
var x = e.pageX - wrap.offsetLeft;
wrap.scrollLeft = scrollLeft - (x - startX);
});
})();
// Infinite review carousel
(function () {
const wrap = document.querySelector('.reviews__ticker-wrap');
const ticker = document.getElementById('rev-ticker');
if (!wrap || !ticker) return;
const originals = Array.from(ticker.querySelectorAll('.rev-card'));
const count = originals.length;
if (!count) return;
// Duplicate cards before and after the originals.
// This allows scrolling in either direction indefinitely.
originals.forEach(card => ticker.appendChild(card.cloneNode(true)));
originals.slice().reverse().forEach(card => {ticker.insertBefore(card.cloneNode(true), ticker.firstChild);});
const allCards = Array.from(ticker.querySelectorAll('.rev-card'));
allCards.forEach((card, index) => {
if (index >= count && index < count * 2) return;
const original = originals[index % count];
card.addEventListener('click', () => original.click());
card.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
original.click();
}
});
});
const cardWidth = () => originals[0].offsetWidth + 24;
const sectionWidth = () =>
ticker.children[count * 2].offsetLeft -
ticker.children[count].offsetLeft;
// Start at the original set of cards.
wrap.style.scrollBehavior = 'auto';
wrap.scrollLeft = sectionWidth();
// Keep the scroll position within the middle copy.
// All three copies have the same card order.
let resetting = false;
wrap.addEventListener('scroll', () => {
if (resetting) return;
const width = sectionWidth();
const position = wrap.scrollLeft;
// Reset only when leaving the middle copy.
if (position < width || position >= width * 2) {
resetting = true;
const previousBehavior = wrap.style.scrollBehavior;
wrap.style.scrollBehavior = 'auto';
wrap.scrollLeft = position < width
? position + width
: position - width;
requestAnimationFrame(() => {
wrap.style.scrollBehavior = previousBehavior;
resetting = false;
});
}
});
window.revTickerStep = function (dir) {
wrap.scrollBy({
left: dir * cardWidth(),
behavior: 'smooth'
});
};
})();
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay.open').forEach(function(m) {
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB