3
3
- id : news
4
4
contents :
5
5
- " news/posts/*/index.qmd"
6
- max-items : 10
6
+ # max-items: 10
7
7
sort : date desc
8
8
type : grid
9
9
grid-columns : 3
@@ -17,12 +17,10 @@ listing:
17
17
### News
18
18
19
19
::: {#news}
20
-
21
20
:::
22
21
23
22
[ See all news &rarr ; ] ( news/ )
24
23
25
-
26
24
``` {=html}
27
25
<style>
28
26
/* hide default Quarto grid once JS enhancement is active */
@@ -42,48 +40,98 @@ listing:
42
40
43
41
#carousel-track {
44
42
display: flex;
45
- align-items: flex-start;
43
+ align-items: stretch;
46
44
transition: transform 0.7s cubic-bezier(0.25, 1, 0.5, 1);
47
45
will-change: transform;
48
46
}
49
47
50
- /* each slide sizing & height animation */
51
48
#carousel-track > .g-col-1 {
52
- flex: 0 0 33.3333%; /* Default for desktop (3 columns) */
49
+ flex: 0 0 33.3333%;
53
50
padding: 1rem;
54
51
box-sizing: border-box;
55
52
display: block !important;
56
- transition: height 0.3s ease ;
53
+ min-width: 0 ;
57
54
}
58
55
59
56
/* Tablet/iPad size: 2 columns */
60
57
@media (max-width: 1024px) and (min-width: 769px) {
61
58
#carousel-track > .g-col-1 {
62
- flex: 0 0 50%; /* 2 columns */
59
+ flex: 0 0 50%;
63
60
}
64
61
}
65
62
66
- /* Single-column on smaller mobile */
67
63
@media (max-width: 768px) {
68
64
#carousel-track > .g-col-1 {
69
- flex: 0 0 100%; /* 1 column */
65
+ flex: 0 0 100%;
70
66
}
71
67
}
72
68
73
- /* remove default card styling */
74
69
#carousel-track > .g-col-1 .card {
75
70
background: none;
76
71
box-shadow: none;
77
72
border: none;
78
73
}
79
74
80
- /* trim default listing padding */
81
75
.quarto-listing {
82
76
padding-bottom: 0 !important;
83
77
}
78
+
79
+ #carousel-track > .g-col-1 .card.news-item {
80
+ overflow: hidden;
81
+ display: flex;
82
+ flex-direction: column;
83
+ }
84
+
85
+ #carousel-track > .g-col-1 .card-body {
86
+ flex-grow: 1;
87
+ overflow: hidden;
88
+ display: flex;
89
+ flex-direction: column;
90
+ }
91
+
92
+ #carousel-track > .g-col-1 .card-img-top {
93
+ max-height: 150px;
94
+ object-fit: cover;
95
+ }
96
+
97
+ #carousel-track .listing-title {
98
+ white-space: nowrap;
99
+ overflow: hidden;
100
+ text-overflow: ellipsis;
101
+ }
102
+
103
+ #carousel-track .listing-description {
104
+ display: -webkit-box;
105
+ -webkit-box-orient: vertical;
106
+ -webkit-line-clamp: 2;
107
+ overflow: hidden;
108
+ text-overflow: ellipsis;
109
+ }
110
+
111
+ #carousel-track .card-attribution {
112
+ margin-top: auto;
113
+ padding-top: 1rem;
114
+ display: flex;
115
+ align-items: flex-end;
116
+ gap: 1em;
117
+ }
118
+
119
+ #carousel-track .listing-author {
120
+ white-space: nowrap;
121
+ overflow: hidden;
122
+ text-overflow: ellipsis;
123
+ min-width: 0;
124
+ }
125
+
126
+ #carousel-track .listing-date {
127
+ white-space: nowrap;
128
+ flex-shrink: 0;
129
+ }
130
+
84
131
</style>
85
132
86
133
<script>
134
+ // The script block remains the same. No changes are needed here.
87
135
document.addEventListener('DOMContentLoaded', function () {
88
136
const listing = document.getElementById('listing-news');
89
137
if (!listing) return;
@@ -93,39 +141,34 @@ listing:
93
141
);
94
142
const N_original = originalItems.length;
95
143
96
- originalItems.forEach(item => {
97
- const card = item.querySelector('.card');
98
- if (card) {
99
- card.classList.add('news-item');
100
- }
101
- });
144
+ originalItems.forEach(item => {
145
+ const card = item.querySelector('.card');
146
+ if (card) {
147
+ card.classList.add('news-item');
148
+ }
149
+ });
102
150
103
- // Helper to get items per view (cached on first call, recalculated on resize)
104
151
function getItemsPerView() {
105
152
const width = window.innerWidth;
106
- if (width <= 768) { // Mobile
153
+ if (width <= 768) {
107
154
return 1;
108
- } else if (width > 768 && width <= 1024) { // Tablet/iPad
155
+ } else if (width > 768 && width <= 1024) {
109
156
return 2;
110
- } else { // Desktop
157
+ } else {
111
158
return 3;
112
159
}
113
160
}
114
161
115
- // If there are too few items to scroll, just display them statically.
116
- // This check now uses the initial itemsPerView.
117
162
if (N_original <= getItemsPerView()) {
118
163
listing.classList.remove('enhanced-carousel');
119
164
return;
120
165
}
121
166
122
- // Add enhanced-carousel class only if the carousel is actually being initialized
123
167
listing.classList.add('enhanced-carousel');
124
168
125
169
let carouselContainer = document.getElementById('carousel-container');
126
170
let carouselTrack = document.getElementById('carousel-track');
127
171
128
- // Initialize carousel elements if they don't exist (first load or after a full re-init on resize)
129
172
if (!carouselContainer) {
130
173
carouselContainer = document.createElement('div');
131
174
carouselContainer.id = 'carousel-container';
@@ -138,13 +181,12 @@ listing:
138
181
carouselContainer.appendChild(carouselTrack);
139
182
listing.parentNode.insertBefore(carouselContainer, listing.nextSibling);
140
183
} else {
141
- // Clear existing children from track if re-initializing on resize
142
184
while(carouselTrack.firstChild) {
143
185
carouselTrack.removeChild(carouselTrack.firstChild);
144
186
}
145
187
}
146
188
147
- let itemsPerView = getItemsPerView(); // Initial calculation
189
+ let itemsPerView = getItemsPerView();
148
190
const numClones = Math.max(itemsPerView, 1);
149
191
150
192
const clonedItems = [];
@@ -162,22 +204,18 @@ listing:
162
204
163
205
const allItems = [...originalItems, ...clonedItems];
164
206
207
+ allItems.forEach(item => {
208
+ const titleElement = item.querySelector('.listing-title');
209
+ if (titleElement) {
210
+ titleElement.setAttribute('title', titleElement.textContent.trim());
211
+ }
212
+ });
213
+
165
214
let currentIndex = 0;
166
215
let shiftPercent = 100 / itemsPerView;
167
216
const displayDuration = 2000;
168
217
const transitionDuration = 700;
169
218
170
- function recalcHeight() {
171
- for (let i = currentIndex; i < Math.min(currentIndex + itemsPerView, allItems.length); i++) {
172
- allItems[i].style.height = 'auto';
173
- }
174
-
175
- const vis = allItems.slice(currentIndex, currentIndex + itemsPerView);
176
- const h = vis.length > 0 ? Math.max(...vis.map(i => i.offsetHeight)) : 0;
177
- vis.forEach(i => i.style.height = h + 'px');
178
- carouselContainer.style.height = h + 'px';
179
- }
180
-
181
219
function updateSlide(idx, instant = false) {
182
220
if (instant) {
183
221
carouselTrack.style.transition = 'none';
@@ -186,7 +224,6 @@ listing:
186
224
}
187
225
188
226
carouselTrack.style.transform = `translateX(-${idx * shiftPercent}%)`;
189
- recalcHeight();
190
227
191
228
allItems.forEach((item, i) => {
192
229
if (i >= currentIndex && i < currentIndex + itemsPerView) {
@@ -209,10 +246,8 @@ listing:
209
246
updateSlide(currentIndex);
210
247
}
211
248
212
- recalcHeight();
213
249
updateSlide(0);
214
250
215
- // auto-play with pause on hover/focus/visibility
216
251
let intervalId = setInterval(nextSlide, displayDuration);
217
252
['mouseenter','focusin'].forEach(e =>
218
253
carouselContainer.addEventListener(e, () => clearInterval(intervalId))
@@ -247,7 +282,6 @@ listing:
247
282
itemsPerView = newItemsPerView;
248
283
shiftPercent = 100 / itemsPerView;
249
284
currentIndex = Math.min(currentIndex, N_original - 1);
250
- recalcHeight();
251
285
updateSlide(currentIndex, true);
252
286
}, 150);
253
287
});
0 commit comments