22 <div ref =" integrations" class =" integrations" :class =" { 'integrations--visible': isVisible }" >
33 <div class =" integrations__row" >
44 <div
5- v-for =" n in lineLength "
5+ v-for =" n in totalItemsPerLine "
66 :key =" n"
77 class =" integrations__item"
88 />
99 </div >
1010
11- <!-- First line -->
12- <div class =" integrations__row" >
13- <template v-if =" Math .floor ((lineLength - firstLine .length ) / 2 ) > 0 " >
14- <div
15- v-for =" n in Math.floor((lineLength - firstLine.length) / 2)"
16- :key =" `first-line-before-${n}`"
17- class =" integrations__item"
18- />
19- </template >
11+ <!-- Dynamic lines -->
12+ <div
13+ v-for =" (line, lineIndex) in lines"
14+ :key =" `line-${lineIndex}`"
15+ class =" integrations__row"
16+ >
2017 <div
21- v-for =" (integration, index) in firstLine "
22- :key =" integration.name "
18+ v-for =" n in Math.max(1, Math.floor((totalItemsPerLine - line.length) / 2)) "
19+ :key =" `line-${lineIndex}-before-${n}` "
2320 class =" integrations__item"
24- :style =" {
25- '--glow-color': integration.glowColor,
26- '--animation-delay': `${index * 0.1}s`,
27- }"
28- >
29- <img :src =" integration.picture" :alt =" integration.name" >
30- </div >
31- <template v-if =" Math .ceil ((lineLength - firstLine .length ) / 2 ) + 1 > 0 " >
32- <div
33- v-for =" n in Math.ceil((lineLength - firstLine.length) / 2) + 1"
34- :key =" `first-line-after-${n}`"
35- class =" integrations__item"
36- />
37- </template >
38- </div >
39-
40- <!-- Second line -->
41- <div class =" integrations__row" >
42- <template v-if =" Math .ceil ((lineLength - secondLine .length ) / 2 ) > 0 " >
43- <div
44- v-for =" n in Math.ceil((lineLength - secondLine.length) / 2)"
45- :key =" `second-line-before-${n}`"
46- class =" integrations__item"
47- />
48- </template >
21+ />
4922 <div
50- v-for =" (integration, index) in secondLine "
23+ v-for =" (integration, index) in line "
5124 :key =" integration.name"
5225 class =" integrations__item"
5326 :style =" {
5427 '--glow-color': integration.glowColor,
55- '--animation-delay': `${(index + 5 ) * 0.1}s`,
28+ '--animation-delay': `${(lineIndex * maxItemsPerLine + index ) * 0.1}s`,
5629 }"
5730 >
5831 <img :src =" integration.picture" :alt =" integration.name" >
5932 </div >
60- <template v-if =" Math .floor ((lineLength - secondLine .length ) / 2 ) + 1 > 0 " >
61- <div
62- v-for =" n in Math.ceil((lineLength - secondLine.length) / 2) + 1"
63- :key =" `second-line-after-${n}`"
64- class =" integrations__item"
65- />
66- </template >
67- </div >
6833
69- <!-- Third line -->
70- <div class =" integrations__row" >
71- <template v-if =" Math .floor ((lineLength - thirdLine .length ) / 2 ) > 0 " >
72- <div
73- v-for =" n in Math.floor((lineLength - thirdLine.length) / 2)"
74- :key =" `third-line-before-${n}`"
75- class =" integrations__item"
76- />
77- </template >
7834 <div
79- v-for =" (integration, index) in thirdLine "
80- :key =" integration.name "
35+ v-for =" n in Math.max(1, Math.ceil((totalItemsPerLine - line.length) / 2) + 1) "
36+ :key =" `line-${lineIndex}-after-${n}` "
8137 class =" integrations__item"
82- :style =" {
83- '--glow-color': integration.glowColor,
84- '--animation-delay': `${(index + 13) * 0.1}s`,
85- }"
86- >
87- <img :src =" integration.picture" :alt =" integration.name" >
88- </div >
89- <template v-if =" Math .floor ((lineLength - thirdLine .length ) / 2 ) + 1 > 0 " >
90- <div
91- v-for =" n in Math.ceil((lineLength - thirdLine.length) / 2) + 1"
92- :key =" `third-line-after-${n}`"
93- class =" integrations__item"
94- />
95- </template >
38+ />
9639 </div >
9740
9841 <div class =" integrations__row" >
9942 <div
100- v-for =" n in lineLength "
43+ v-for =" n in totalItemsPerLine "
10144 :key =" n"
10245 class =" integrations__item"
10346 />
@@ -128,8 +71,26 @@ import webpackSvg from '~/assets/svg/integrations/webpack.svg';
12871import viteSvg from ' ~/assets/svg/integrations/vite.svg' ;
12972import sentrySvg from ' ~/assets/svg/integrations/sentry.svg' ;
13073
74+ interface Integration {
75+ name: string ;
76+ picture: string ;
77+ link: string ;
78+ glowColor: string ;
79+ }
80+
81+ type Data = {
82+ integrations: Integration [];
83+ windowWidth: number ;
84+ isVisible: boolean ;
85+ observer: IntersectionObserver | null ;
86+ mobileBreakpoint: number ;
87+ itemsWidth: number ;
88+ itemsGap: number ;
89+ containerPadding: number ;
90+ };
91+
13192export default Vue .extend ({
132- data() {
93+ data : function () : Data {
13394 return {
13495 integrations: [
13596 {
@@ -245,27 +206,117 @@ export default Vue.extend({
245206 isVisible: false ,
246207 observer: null as IntersectionObserver | null ,
247208 mobileBreakpoint: 850 ,
209+ itemsWidth: 80 ,
210+ itemsGap: 20 ,
211+ containerPadding: 2 * 20 ,
248212 };
249213 },
250214 computed: {
251- lineLength() {
252- const itemWidth = 80 ;
253- const itemsGap = 20 ;
254- const lineWidth = this .windowWidth - 2 * 20 ;
215+ /**
216+ * Maximum number of items per line
217+ * Used for the first and last empty lines
218+ */
219+ totalItemsPerLine : function (): number {
220+ return Math .ceil (this .windowWidth / (this .itemsWidth + this .itemsGap ));
221+ },
255222
256- return Math .ceil (lineWidth / (itemWidth + itemsGap ));
223+ /**
224+ * How many non-empty items should be in the line
225+ * Based on the screen width
226+ */
227+ maxItemsPerLine : function (): number {
228+ // Use 30% of screen width on desktop, 100% on mobile
229+ const isMobile = this .windowWidth < this .mobileBreakpoint ;
230+ const maxWidth = isMobile ? this .windowWidth - this .containerPadding : this .windowWidth * 0.3 ;
231+
232+ return Math .ceil (maxWidth / (this .itemsWidth + this .itemsGap ));
257233 },
258- isMobile() {
234+
235+ /**
236+ * Whether the screen is mobile
237+ */
238+ isMobile : function (): boolean {
259239 return this .windowWidth < this .mobileBreakpoint ;
260240 },
261- firstLine() {
262- return this .integrations .slice (0 , 5 );
263- },
264- secondLine() {
265- return this .integrations .slice (5 , 13 );
241+
242+ /**
243+ * How many lines should be in the component
244+ * Based on the max items per line
245+ */
246+ totalLines : function (): number {
247+ // Calculate how many lines we need based on total items and items per line
248+ const totalItems = this .integrations .length ;
249+ const maxItemsPerLine = this .maxItemsPerLine ;
250+
251+ return Math .ceil (totalItems / maxItemsPerLine );
266252 },
267- thirdLine() {
268- return this .integrations .slice (13 );
253+
254+ /**
255+ * Separation by lines
256+ */
257+ lines : function (): Integration [][] {
258+ const totalItems = this .integrations .length ;
259+ const targetItemsPerLine = this .maxItemsPerLine ;
260+ const lines = [];
261+
262+ if (this .isMobile ) {
263+ // On mobile, use normal distribution
264+ const totalLines = Math .ceil (totalItems / targetItemsPerLine );
265+ const baseItemsPerLine = Math .floor (totalItems / totalLines );
266+ const remainder = totalItems % totalLines ;
267+
268+ for (let i = 0 ; i < totalLines ; i ++ ) {
269+ const startIndex = i * baseItemsPerLine + Math .min (i , remainder );
270+ const endIndex = startIndex + baseItemsPerLine + (i < remainder ? 1 : 0 );
271+
272+ lines .push (this .integrations .slice (startIndex , endIndex ));
273+ }
274+ } else {
275+ // On desktop, make first and last lines have -1 item, center lines have +2 items
276+ const totalLines = Math .ceil (totalItems / targetItemsPerLine );
277+
278+ if (totalLines <= 2 ) {
279+ // If only 1-2 lines, use normal distribution
280+ const baseItemsPerLine = Math .floor (totalItems / totalLines );
281+ const remainder = totalItems % totalLines ;
282+
283+ for (let i = 0 ; i < totalLines ; i ++ ) {
284+ const startIndex = i * baseItemsPerLine + Math .min (i , remainder );
285+ const endIndex = startIndex + baseItemsPerLine + (i < remainder ? 1 : 0 );
286+
287+ lines .push (this .integrations .slice (startIndex , endIndex ));
288+ }
289+ } else {
290+ // For 3+ lines, redistribute items
291+ const itemsPerLine = Math .floor (totalItems / totalLines );
292+ const remainder = totalItems % totalLines ;
293+ let currentIndex = 0 ;
294+
295+ for (let i = 0 ; i < totalLines ; i ++ ) {
296+ let itemsInThisLine;
297+
298+ if (i === 0 || i === totalLines - 1 ) {
299+ // First and last lines get -1 item
300+ itemsInThisLine = Math .max (1 , itemsPerLine - 1 );
301+ } else {
302+ // Center lines get +2 items (if available)
303+ itemsInThisLine = itemsPerLine + 2 ;
304+ }
305+
306+ // Adjust for remainder
307+ if (i < remainder ) {
308+ itemsInThisLine += 1 ;
309+ }
310+
311+ const endIndex = Math .min (currentIndex + itemsInThisLine , totalItems );
312+
313+ lines .push (this .integrations .slice (currentIndex , endIndex ));
314+ currentIndex = endIndex ;
315+ }
316+ }
317+ }
318+
319+ return lines ;
269320 },
270321 },
271322 mounted() {
@@ -286,11 +337,11 @@ export default Vue.extend({
286337 },
287338
288339 methods: {
289- handleResize() {
340+ handleResize : function () : void {
290341 this .windowWidth = window .innerWidth ;
291342 },
292343
293- setupIntersectionObserver() {
344+ setupIntersectionObserver : function () : void {
294345 this .observer = new IntersectionObserver (
295346 (entries ) => {
296347 entries .forEach ((entry ) => {
@@ -313,6 +364,18 @@ export default Vue.extend({
313364 this .observer .observe (element );
314365 }
315366 },
367+
368+ calculateItemsPerLine : function (): number {
369+ const itemWidth = 80 ;
370+ const itemsGap = 20 ;
371+ const containerPadding = 2 * 20 ;
372+
373+ // Use 60% of screen width on desktop, 100% on mobile
374+ const isMobile = this .windowWidth < this .mobileBreakpoint ;
375+ const maxWidth = isMobile ? this .windowWidth - containerPadding : this .windowWidth * 0.6 ;
376+
377+ return Math .ceil (maxWidth / (itemWidth + itemsGap ));
378+ },
316379 },
317380});
318381 </script >
@@ -329,6 +392,7 @@ export default Vue.extend({
329392 width : 100 % ;
330393 overflow : hidden;
331394 position : relative;
395+ color :red;
332396
333397 mask-image : linear-gradient (90 deg ,transparent 0 % ,#ffffff 300 px ,#ffffff calc (100 vw - 300 px ),transparent 100 % );
334398
@@ -364,6 +428,12 @@ export default Vue.extend({
364428 &:nth-of-type (2 n+ 1 ) {
365429 margin-left : -50 px ;
366430 }
431+
432+ @media (--screen-mobile-extra-small ) {
433+ &:nth-of-type (2 n) {
434+ margin-left : -100 px ;
435+ }
436+ }
367437 }
368438
369439 & __item {
0 commit comments