@@ -13,3 +13,216 @@ listing:
1313:::
1414
1515[ See all news &rarr ; ] ( news/ )
16+
17+ ``` {=html}
18+ <style>
19+ /* Scoped styles for the news carousel to avoid conflicts with global themes */
20+ #news-carousel-container {
21+ overflow: hidden;
22+ position: relative;
23+ width: 100%;
24+ }
25+
26+ #news-carousel-container #news-carousel-track {
27+ display: flex;
28+ align-items: stretch;
29+ }
30+
31+ #news-carousel-container .news-carousel-slide {
32+ flex-shrink: 0;
33+ width: 33.3333%;
34+ padding: 0.5rem;
35+ box-sizing: border-box;
36+ }
37+
38+ @media (max-width: 1024px) {
39+ #news-carousel-container .news-carousel-slide {
40+ width: 50%;
41+ }
42+ }
43+
44+ @media (max-width: 768px) {
45+ #news-carousel-container .news-carousel-slide {
46+ width: 100%;
47+ }
48+ }
49+
50+ #news-carousel-container .news-carousel-card {
51+ overflow: hidden;
52+ display: flex;
53+ flex-direction: column;
54+ height: 100%;
55+ text-decoration: none;
56+ border-radius: 0.5rem;
57+ transition: background-color 0.3s ease, border-color 0.3s ease;
58+ background-color: #f8f9fa;
59+ border: 1px solid #e9ecef;
60+ color: #212529;
61+ }
62+
63+ #news-carousel-container .news-carousel-card:hover {
64+ background-color: #e9ecef;
65+ border-color: #dee2e6;
66+ }
67+
68+ #news-carousel-container .news-carousel-card-body {
69+ flex-grow: 1;
70+ display: flex;
71+ flex-direction: column;
72+ padding: 1rem;
73+ }
74+
75+ #news-carousel-container .news-carousel-title {
76+ white-space: nowrap;
77+ overflow: hidden;
78+ text-overflow: ellipsis;
79+ margin-bottom: 0.25rem;
80+ color: #212529;
81+ font-weight: 700;
82+ }
83+
84+ #news-carousel-container .news-carousel-reading-time,
85+ #news-carousel-container .news-carousel-description,
86+ #news-carousel-container .news-carousel-attribution {
87+ color: #6c757d;
88+ }
89+
90+ #news-carousel-container .news-carousel-reading-time {
91+ font-size: 0.9em;
92+ margin-bottom: 0.75rem;
93+ }
94+
95+ #news-carousel-container .news-carousel-description {
96+ flex-grow: 1;
97+ display: -webkit-box;
98+ -webkit-box-orient: vertical;
99+ -webkit-line-clamp: 2;
100+ line-clamp: 2;
101+ overflow: hidden;
102+ text-overflow: ellipsis;
103+ margin-bottom: 1rem;
104+ }
105+
106+ #news-carousel-container .news-carousel-attribution {
107+ display: flex;
108+ justify-content: space-between;
109+ align-items: flex-end;
110+ gap: 1em;
111+ font-size: 0.85em;
112+ margin-top: auto;
113+ }
114+
115+ #news-carousel-container .news-carousel-author {
116+ white-space: nowrap;
117+ overflow: hidden;
118+ text-overflow: ellipsis;
119+ min-width: 0;
120+ }
121+
122+ #news-carousel-container .news-carousel-date {
123+ white-space: nowrap;
124+ flex-shrink: 0;
125+ }
126+ </style>
127+
128+ <script>
129+ document.addEventListener('DOMContentLoaded', function () {
130+ const carouselContainer = document.getElementById('news-carousel-container');
131+ const carouselTrack = document.getElementById('news-carousel-track');
132+
133+ if (!carouselContainer || !carouselTrack || !carouselTrack.children.length) {
134+ return;
135+ }
136+
137+ const slides = Array.from(carouselTrack.children);
138+ const displayDuration = 2500;
139+ let currentTranslate = 0;
140+ let currentIndex = 0;
141+ let intervalId;
142+ let wheelTimeout;
143+ let isWheeling = false;
144+
145+ const getItemsPerView = () => {
146+ const width = window.innerWidth;
147+ if (width <= 768) return 1;
148+ if (width > 768 && width <= 1024) return 2;
149+ return 3;
150+ }
151+
152+ const startAutoplay = () => {
153+ stopAutoplay();
154+ intervalId = setInterval(autoplayNext, displayDuration);
155+ }
156+
157+ const stopAutoplay = () => {
158+ clearInterval(intervalId);
159+ }
160+
161+ const setSliderPosition = () => {
162+ carouselTrack.style.transform = `translateX(${currentTranslate}px)`;
163+ }
164+
165+ const setPositionByIndex = () => {
166+ if (slides.length === 0) return;
167+
168+ const itemsPerView = getItemsPerView();
169+ const maxIndex = slides.length > itemsPerView ? slides.length - itemsPerView : 0;
170+
171+ if (currentIndex > maxIndex) currentIndex = maxIndex;
172+ if (currentIndex < 0) currentIndex = 0;
173+
174+ const slideWidth = slides[0].getBoundingClientRect().width;
175+ currentTranslate = currentIndex * -slideWidth;
176+
177+ carouselTrack.style.transition = 'transform 0.4s ease-out';
178+ setSliderPosition();
179+ }
180+
181+ const autoplayNext = () => {
182+ if (document.hidden) return;
183+ const itemsPerView = getItemsPerView();
184+ const maxIndex = slides.length > itemsPerView ? slides.length - itemsPerView : 0;
185+
186+ currentIndex++;
187+ if (currentIndex > maxIndex) {
188+ currentIndex = 0;
189+ }
190+ setPositionByIndex();
191+ }
192+
193+ const handleWheel = (event) => {
194+ event.preventDefault();
195+
196+ if (isWheeling) return;
197+ isWheeling = true;
198+
199+ stopAutoplay();
200+
201+ const itemsPerView = getItemsPerView();
202+ const maxIndex = slides.length > itemsPerView ? slides.length - itemsPerView : 0;
203+ const delta = event.deltaY;
204+
205+ if (delta > 0) {
206+ if (currentIndex < maxIndex) currentIndex++;
207+ } else if (delta < 0) {
208+ if (currentIndex > 0) currentIndex--;
209+ }
210+ setPositionByIndex();
211+
212+ clearTimeout(wheelTimeout);
213+ wheelTimeout = setTimeout(startAutoplay, 300);
214+
215+ setTimeout(() => { isWheeling = false; }, 100);
216+ }
217+
218+ carouselContainer.addEventListener('wheel', handleWheel, { passive: false });
219+ carouselContainer.addEventListener('mouseenter', stopAutoplay);
220+ carouselContainer.addEventListener('mouseleave', startAutoplay);
221+ document.addEventListener('visibilitychange', () => document.hidden ? stopAutoplay() : startAutoplay());
222+ window.addEventListener('resize', setPositionByIndex);
223+
224+ setPositionByIndex();
225+ startAutoplay();
226+ });
227+ </script>
228+ ```
0 commit comments