@portosaur/theme 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/package.json +10 -3
  2. package/src/plugins/theme.mjs +2 -0
  3. package/theme/DocCategoryGeneratedIndexPage/index.jsx +4 -10
  4. package/theme/MDXComponents.jsx +1 -1
  5. package/theme/Root.jsx +1 -1
  6. package/theme/components/AboutSection/index.jsx +89 -249
  7. package/theme/components/ContactSection/index.jsx +72 -153
  8. package/theme/components/ExperienceSection/index.jsx +35 -106
  9. package/theme/components/HeroSection/index.jsx +64 -186
  10. package/theme/components/NavArrow/index.jsx +38 -55
  11. package/theme/components/NoteIndex/index.jsx +50 -116
  12. package/theme/components/Preview/components/FeedbackStates.jsx +45 -190
  13. package/theme/components/Preview/components/FileTabs.jsx +17 -24
  14. package/theme/components/Preview/components/PreviewContent.jsx +37 -62
  15. package/theme/components/Preview/components/PreviewHeader.jsx +146 -380
  16. package/theme/components/Preview/components/Triggers/Pv.jsx +50 -78
  17. package/theme/components/Preview/components/Triggers/SrcPv.jsx +16 -47
  18. package/theme/components/Preview/components/Triggers/index.jsx +2 -2
  19. package/theme/components/Preview/components/ViewerWindow.jsx +160 -268
  20. package/theme/components/Preview/index.jsx +3 -3
  21. package/theme/components/Preview/renderers/CodeRenderer.jsx +81 -109
  22. package/theme/components/Preview/renderers/ImageRenderer.jsx +30 -67
  23. package/theme/components/Preview/renderers/PdfRenderer.jsx +31 -52
  24. package/theme/components/Preview/renderers/WebRenderer.jsx +18 -32
  25. package/theme/components/Preview/state/index.jsx +46 -30
  26. package/theme/components/ProjectsSection/index.jsx +278 -573
  27. package/theme/components/SocialLinks/index.jsx +43 -55
  28. package/theme/components/Tooltip/index.jsx +28 -39
  29. package/theme/pages/index.jsx +23 -87
  30. package/theme/pages/notes.jsx +26 -104
  31. package/theme/pages/tasks.jsx +220 -903
@@ -10,22 +10,30 @@ import {
10
10
  } from "react-icons/fa";
11
11
  import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
12
12
  import useScrollReveal from "../../hooks/useScrollReveal";
13
- import Tooltip from "../Tooltip/index.js";
13
+ import Tooltip from "../Tooltip/index.jsx";
14
14
  import useBrokenLinks from "@docusaurus/useBrokenLinks";
15
15
  import styles from "./styles.module.css";
16
16
  import "slick-carousel/slick/slick.css";
17
17
  import "slick-carousel/slick/slick-theme.css";
18
+
18
19
  export default function ProjectsSection({ id, className }) {
19
20
  const { siteConfig } = useDocusaurusContext();
20
21
  const brokenLinks = useBrokenLinks();
22
+
21
23
  if (id) {
22
24
  brokenLinks.collectAnchor(id);
23
25
  }
24
- const projectShelf = siteConfig.customFields?.projects || {};
25
- if (projectShelf.enable === false) return null;
26
+
27
+ const projectShelf = siteConfig.customFields?.projectShelf || {};
28
+
29
+ if (projectShelf.enable === false) {
30
+ return null;
31
+ }
32
+
26
33
  const isAutoplayEnabled = projectShelf.autoplay ?? true;
27
34
  const displayHeading = projectShelf.heading;
28
35
  const displaySubheading = projectShelf.subheading;
36
+
29
37
  const [projects, setProjects] = useState([]);
30
38
  const sliderRef = useRef(null);
31
39
  const [atBeginning, setAtBeginning] = useState(true);
@@ -36,6 +44,7 @@ export default function ProjectsSection({ id, className }) {
36
44
  const activeDotRef = useRef(null);
37
45
  const dotsContainerRef = useRef(null);
38
46
  const [sectionRef, isVisible] = useScrollReveal();
47
+
39
48
  const getVisibleSlidesPerView = useCallback(() => {
40
49
  if (typeof window === "undefined") return 3;
41
50
  const width = window.innerWidth;
@@ -43,6 +52,7 @@ export default function ProjectsSection({ id, className }) {
43
52
  if (width <= 1024) return 2;
44
53
  return 3;
45
54
  }, []);
55
+
46
56
  const prepareProjects = useCallback((projectList, slides) => {
47
57
  if (!projectList?.length) return { projects: [], totalPages: 0 };
48
58
  const processedProjects = projectList.map((project, index) => {
@@ -64,18 +74,18 @@ export default function ProjectsSection({ id, className }) {
64
74
  }
65
75
  return processed;
66
76
  });
67
- const totalPages = Math.ceil(processedProjects.length / slides);
68
77
  processedProjects.sort((a, b) => {
69
78
  if (a.featured && !b.featured) return -1;
70
79
  if (!a.featured && b.featured) return 1;
71
80
  return 0;
72
81
  });
82
+ const totalPages = Math.ceil(processedProjects.length / slides);
73
83
  return { projects: processedProjects, totalPages };
74
84
  }, []);
85
+
75
86
  useEffect(() => {
76
- const projectShelf = siteConfig.customFields?.projects;
77
- const configuredProjects = projectShelf?.enable
78
- ? projectShelf?.projects || []
87
+ const configuredProjects = siteConfig.customFields?.projectShelf?.enable
88
+ ? siteConfig.customFields?.projectShelf?.projects || []
79
89
  : [];
80
90
  const handleLayout = () => {
81
91
  const newSlidesToShow = getVisibleSlidesPerView();
@@ -98,6 +108,7 @@ export default function ProjectsSection({ id, className }) {
98
108
  slidesToShow,
99
109
  projects.length,
100
110
  ]);
111
+
101
112
  const goToSlide = useCallback(
102
113
  (index) => {
103
114
  if (sliderRef.current) {
@@ -107,6 +118,7 @@ export default function ProjectsSection({ id, className }) {
107
118
  },
108
119
  [slidesToShow],
109
120
  );
121
+
110
122
  useEffect(() => {
111
123
  const scrollTimeout = setTimeout(() => {
112
124
  if (activeDotRef.current && dotsContainerRef.current) {
@@ -149,6 +161,7 @@ export default function ProjectsSection({ id, className }) {
149
161
  }, 50);
150
162
  return () => clearTimeout(scrollTimeout);
151
163
  }, [currentSlide, totalPages]);
164
+
152
165
  const settings = useMemo(
153
166
  () => ({
154
167
  dots: false,
@@ -183,51 +196,37 @@ export default function ProjectsSection({ id, className }) {
183
196
  className: styles.projectsCarousel,
184
197
  beforeChange: (_, next) => {
185
198
  setAtBeginning(next === 0);
186
- const nextSlideIndex = Math.floor(next / slidesToShow);
187
- setCurrentSlide(nextSlideIndex);
199
+ setCurrentSlide(Math.floor(next / slidesToShow));
188
200
  setAtEnd(next + slidesToShow >= projects.length);
189
201
  },
190
202
  }),
191
203
  [projects, slidesToShow],
192
204
  );
205
+
193
206
  const goToNext = useCallback(() => {
194
- if (!atEnd && sliderRef.current) {
195
- sliderRef.current.slickNext();
196
- }
207
+ if (!atEnd && sliderRef.current) sliderRef.current.slickNext();
197
208
  }, [atEnd]);
209
+
198
210
  const goToPrev = useCallback(() => {
199
- if (!atBeginning && sliderRef.current) {
200
- sliderRef.current.slickPrev();
201
- }
211
+ if (!atBeginning && sliderRef.current) sliderRef.current.slickPrev();
202
212
  }, [atBeginning]);
213
+
203
214
  const renderProjectLink = useCallback((url, Icon, label, ariaLabel) => {
204
215
  if (!url || url === "#" || url === "") return null;
205
- return jsxDEV_7x81h0kn(
206
- "a",
207
- {
208
- href: url,
209
- target: "_blank",
210
- rel: "noopener noreferrer",
211
- className: styles.projectLink,
212
- "aria-label": ariaLabel,
213
- children: [
214
- jsxDEV_7x81h0kn(Icon, {}, undefined, false, undefined, this),
215
- jsxDEV_7x81h0kn(
216
- "span",
217
- { children: label },
218
- undefined,
219
- false,
220
- undefined,
221
- this,
222
- ),
223
- ],
224
- },
225
- undefined,
226
- true,
227
- undefined,
228
- this,
216
+ return (
217
+ <a
218
+ href={url}
219
+ target="_blank"
220
+ rel="noopener noreferrer"
221
+ className={styles.projectLink}
222
+ aria-label={ariaLabel}
223
+ >
224
+ <Icon />
225
+ <span>{label}</span>
226
+ </a>
229
227
  );
230
228
  }, []);
229
+
231
230
  const getProjectStateInfo = useCallback((state) => {
232
231
  switch (state?.toLowerCase()) {
233
232
  case "active":
@@ -247,544 +246,250 @@ export default function ProjectsSection({ id, className }) {
247
246
  return { label: "N/A", className: styles.stateNA };
248
247
  }
249
248
  }, []);
249
+
250
250
  const renderNavigationDots = useCallback(() => {
251
251
  if (totalPages <= 1) return null;
252
252
  const fewDots = totalPages <= 5;
253
- return jsxDEV_7x81h0kn(
254
- "div",
255
- {
256
- className: `${styles.navDotsContainer} ${fewDots ? styles.centerDots : styles.scrollDots}`,
257
- role: "tablist",
258
- "aria-label": "Project carousel navigation",
259
- children: Array.from({ length: totalPages }, (_, i) =>
260
- jsxDEV_7x81h0kn(
261
- "button",
262
- {
263
- className: `${styles.navDot} ${currentSlide === i ? styles.activeDot : ""}`,
264
- onClick: () => goToSlide(i),
265
- "aria-label": `Go to slide ${i + 1} of ${totalPages}`,
266
- "aria-selected": currentSlide === i,
267
- role: "tab",
268
- type: "button",
269
- ref: currentSlide === i ? activeDotRef : null,
270
- },
271
- i,
272
- false,
273
- undefined,
274
- this,
275
- ),
276
- ),
277
- },
278
- undefined,
279
- false,
280
- undefined,
281
- this,
253
+ return (
254
+ <div
255
+ className={`${styles.navDotsContainer} ${fewDots ? styles.centerDots : styles.scrollDots}`}
256
+ role="tablist"
257
+ aria-label="Project carousel navigation"
258
+ >
259
+ {Array.from({ length: totalPages }, (_, i) => (
260
+ <button
261
+ key={i}
262
+ className={`${styles.navDot} ${currentSlide === i ? styles.activeDot : ""}`}
263
+ onClick={() => goToSlide(i)}
264
+ aria-label={`Go to slide ${i + 1} of ${totalPages}`}
265
+ aria-selected={currentSlide === i}
266
+ role="tab"
267
+ type="button"
268
+ ref={currentSlide === i ? activeDotRef : null}
269
+ />
270
+ ))}
271
+ </div>
282
272
  );
283
273
  }, [currentSlide, totalPages, goToSlide]);
284
- return jsxDEV_7x81h0kn(
285
- "div",
286
- {
287
- id,
288
- ref: sectionRef,
289
- className: `${styles.projectsSection} ${isVisible ? "is-visible" : ""} ${className || ""}`,
290
- role: "region",
291
- "aria-label": "Projects section",
292
- children: jsxDEV_7x81h0kn(
293
- "div",
294
- {
295
- className: styles.projectsContainer,
296
- children: [
297
- jsxDEV_7x81h0kn(
298
- "div",
299
- {
300
- className: styles.projectsHeader,
301
- children: [
302
- jsxDEV_7x81h0kn(
303
- "h2",
304
- {
305
- className: styles.projectsTitle,
306
- children: displayHeading,
307
- },
308
- undefined,
309
- false,
310
- undefined,
311
- this,
312
- ),
313
- jsxDEV_7x81h0kn(
314
- "p",
315
- {
316
- className: styles.projectsSubtitle,
317
- children: displaySubheading,
318
- },
319
- undefined,
320
- false,
321
- undefined,
322
- this,
323
- ),
324
- ],
325
- },
326
- undefined,
327
- true,
328
- undefined,
329
- this,
330
- ),
331
- projects.length === 0
332
- ? jsxDEV_7x81h0kn(
333
- "div",
334
- {
335
- className: styles.noProjects,
336
- children: jsxDEV_7x81h0kn(
337
- "p",
338
- { children: "No projects to display." },
339
- undefined,
340
- false,
341
- undefined,
342
- this,
343
- ),
344
- },
345
- undefined,
346
- false,
347
- undefined,
348
- this,
349
- )
350
- : jsxDEV_7x81h0kn(
351
- "div",
352
- {
353
- className: styles.carouselContainer,
354
- children: [
355
- projects.length > slidesToShow &&
356
- jsxDEV_7x81h0kn(
357
- "button",
358
- {
359
- className: `${styles.carouselControl} ${styles.prevButton} ${styles.desktopOnly} ${atBeginning ? styles.disabledButton : ""}`,
360
- onClick: goToPrev,
361
- "aria-label": "View previous projects",
362
- "aria-disabled": atBeginning,
363
- type: "button",
364
- disabled: atBeginning,
365
- children: jsxDEV_7x81h0kn(
366
- FaChevronLeft,
367
- { "aria-hidden": "true" },
368
- undefined,
369
- false,
370
- undefined,
371
- this,
372
- ),
373
- },
374
- undefined,
375
- false,
376
- undefined,
377
- this,
378
- ),
379
- jsxDEV_7x81h0kn(
380
- "div",
381
- {
382
- className: styles.carouselWrapper,
383
- "aria-roledescription": "carousel",
384
- "aria-label": "Projects carousel",
385
- children: [
386
- jsxDEV_7x81h0kn(
387
- Slider,
388
- {
389
- ref: sliderRef,
390
- ...settings,
391
- children: projects.map((project, index) =>
392
- jsxDEV_7x81h0kn(
393
- "div",
394
- {
395
- className: styles.carouselSlide,
396
- "data-project-id": project.id,
397
- "aria-roledescription": "slide",
398
- "aria-label": `Project ${index + 1} of ${projects.length}: ${project.title}`,
399
- style: { "--card-index": index },
400
- children: jsxDEV_7x81h0kn(
401
- "div",
402
- {
403
- className: `${styles.carouselCard} ${project.featured ? styles.featuredCard : ""}`,
404
- children: [
405
- project.state &&
406
- jsxDEV_7x81h0kn(
407
- "div",
408
- {
409
- className:
410
- styles.projectStateBadge,
411
- title: `Project status: ${getProjectStateInfo(project.state).label}`,
412
- children: jsxDEV_7x81h0kn(
413
- "span",
414
- {
415
- className: `${styles.projectStateLabel} ${getProjectStateInfo(project.state).className}`,
416
- children:
417
- getProjectStateInfo(
418
- project.state,
419
- ).label,
420
- },
421
- undefined,
422
- false,
423
- undefined,
424
- this,
425
- ),
426
- },
427
- undefined,
428
- false,
429
- undefined,
430
- this,
431
- ),
432
- jsxDEV_7x81h0kn(
433
- "div",
434
- {
435
- className:
436
- styles.projectImageContainer,
437
- style: {
438
- backgroundColor:
439
- project.bg ||
440
- "rgba(var(--ifm-color-primary-rgb), 0.05)",
441
- },
442
- children: [
443
- jsxDEV_7x81h0kn(
444
- "img",
445
- {
446
- src: project.icon,
447
- alt: project.title,
448
- className:
449
- styles.projectImage,
450
- loading: "lazy",
451
- },
452
- undefined,
453
- false,
454
- undefined,
455
- this,
456
- ),
457
- project.tags?.length > 0 &&
458
- (() => {
459
- const extraCount =
460
- project.tags.length - 3;
461
- return jsxDEV_7x81h0kn(
462
- "div",
463
- {
464
- className:
465
- styles.projectTags,
466
- children: [
467
- project.tags
468
- .slice(0, 3)
469
- .map((tag) =>
470
- jsxDEV_7x81h0kn(
471
- "span",
472
- {
473
- className:
474
- styles.projectTag,
475
- children:
476
- tag,
477
- },
478
- tag,
479
- false,
480
- undefined,
481
- this,
482
- ),
483
- ),
484
- extraCount > 0 &&
485
- jsxDEV_7x81h0kn(
486
- Tooltip,
487
- {
488
- msg: project.tags
489
- .slice(3)
490
- .join(", "),
491
- underline: false,
492
- gap: 13,
493
- children:
494
- jsxDEV_7x81h0kn(
495
- "span",
496
- {
497
- className: `${styles.projectTag} ${styles.extraTagBtn}`,
498
- children:
499
- [
500
- "+",
501
- extraCount,
502
- ],
503
- },
504
- undefined,
505
- true,
506
- undefined,
507
- this,
508
- ),
509
- },
510
- undefined,
511
- false,
512
- undefined,
513
- this,
514
- ),
515
- ],
516
- },
517
- undefined,
518
- true,
519
- undefined,
520
- this,
521
- );
522
- })(),
523
- project.featured &&
524
- jsxDEV_7x81h0kn(
525
- "div",
526
- {
527
- className:
528
- styles.featuredBadge,
529
- title:
530
- "Featured Project",
531
- "aria-label":
532
- "Featured project",
533
- children:
534
- jsxDEV_7x81h0kn(
535
- FaStar,
536
- {
537
- "aria-hidden":
538
- "true",
539
- },
540
- undefined,
541
- false,
542
- undefined,
543
- this,
544
- ),
545
- },
546
- undefined,
547
- false,
548
- undefined,
549
- this,
550
- ),
551
- ],
552
- },
553
- undefined,
554
- true,
555
- undefined,
556
- this,
557
- ),
558
- jsxDEV_7x81h0kn(
559
- "div",
560
- {
561
- className:
562
- styles.projectContent,
563
- children: [
564
- jsxDEV_7x81h0kn(
565
- "h3",
566
- {
567
- className:
568
- styles.projectTitle,
569
- children: project.title,
570
- },
571
- undefined,
572
- false,
573
- undefined,
574
- this,
575
- ),
576
- jsxDEV_7x81h0kn(
577
- "p",
578
- {
579
- className:
580
- styles.projectDescription,
581
- children: project.desc,
582
- },
583
- undefined,
584
- false,
585
- undefined,
586
- this,
587
- ),
588
- ],
589
- },
590
- undefined,
591
- true,
592
- undefined,
593
- this,
594
- ),
595
- jsxDEV_7x81h0kn(
596
- "div",
597
- {
598
- className: styles.projectLinks,
599
- children: [
600
- renderProjectLink(
601
- project.website,
602
- FaGlobe,
603
- "Website",
604
- `Visit ${project.title} website`,
605
- ),
606
- renderProjectLink(
607
- project.repo,
608
- FaCode,
609
- "Source",
610
- `Repository with source code`,
611
- ),
612
- renderProjectLink(
613
- project.demo,
614
- FaPlay,
615
- "Demo",
616
- `Live demo for ${project.title}`,
617
- ),
618
- ],
619
- },
620
- undefined,
621
- true,
622
- undefined,
623
- this,
624
- ),
625
- ],
626
- },
627
- undefined,
628
- true,
629
- undefined,
630
- this,
631
- ),
632
- },
633
- project.id || project.title + index,
634
- false,
635
- undefined,
636
- this,
637
- ),
638
- ),
639
- },
640
- undefined,
641
- false,
642
- undefined,
643
- this,
644
- ),
645
- jsxDEV_7x81h0kn(
646
- "div",
647
- {
648
- className: styles.desktopDotsContainer,
649
- children: renderNavigationDots(),
650
- },
651
- undefined,
652
- false,
653
- undefined,
654
- this,
655
- ),
656
- jsxDEV_7x81h0kn(
657
- "div",
658
- {
659
- className: styles.mobileNavigationControls,
660
- children:
661
- totalPages > 1 &&
662
- jsxDEV_7x81h0kn(
663
- Fragment_8vg9x3sq,
664
- {
665
- children: [
666
- jsxDEV_7x81h0kn(
667
- "button",
668
- {
669
- className: `${styles.carouselControl} ${styles.prevButton} ${atBeginning ? styles.disabledButton : ""}`,
670
- onClick: goToPrev,
671
- "aria-label":
672
- "View previous projects",
673
- "aria-disabled": atBeginning,
674
- type: "button",
675
- disabled: atBeginning,
676
- children: jsxDEV_7x81h0kn(
677
- FaChevronLeft,
678
- { "aria-hidden": "true" },
679
- undefined,
680
- false,
681
- undefined,
682
- this,
683
- ),
684
- },
685
- undefined,
686
- false,
687
- undefined,
688
- this,
689
- ),
690
- jsxDEV_7x81h0kn(
691
- "div",
692
- {
693
- className:
694
- styles.dotsScrollContainer,
695
- ref: dotsContainerRef,
696
- children: renderNavigationDots(),
697
- },
698
- undefined,
699
- false,
700
- undefined,
701
- this,
702
- ),
703
- jsxDEV_7x81h0kn(
704
- "button",
705
- {
706
- className: `${styles.carouselControl} ${styles.nextButton} ${atEnd ? styles.disabledButton : ""}`,
707
- onClick: goToNext,
708
- "aria-label": "View next projects",
709
- "aria-disabled": atEnd,
710
- type: "button",
711
- disabled: atEnd,
712
- children: jsxDEV_7x81h0kn(
713
- FaChevronRight,
714
- { "aria-hidden": "true" },
715
- undefined,
716
- false,
717
- undefined,
718
- this,
719
- ),
720
- },
721
- undefined,
722
- false,
723
- undefined,
724
- this,
725
- ),
726
- ],
727
- },
728
- undefined,
729
- true,
730
- undefined,
731
- this,
732
- ),
733
- },
734
- undefined,
735
- false,
736
- undefined,
737
- this,
738
- ),
739
- ],
740
- },
741
- undefined,
742
- true,
743
- undefined,
744
- this,
745
- ),
746
- projects.length > slidesToShow &&
747
- jsxDEV_7x81h0kn(
748
- "button",
749
- {
750
- className: `${styles.carouselControl} ${styles.nextButton} ${styles.desktopOnly} ${atEnd ? styles.disabledButton : ""}`,
751
- onClick: goToNext,
752
- "aria-label": "View next projects",
753
- "aria-disabled": atEnd,
754
- type: "button",
755
- disabled: atEnd,
756
- children: jsxDEV_7x81h0kn(
757
- FaChevronRight,
758
- {},
759
- undefined,
760
- false,
761
- undefined,
762
- this,
763
- ),
764
- },
765
- undefined,
766
- false,
767
- undefined,
768
- this,
769
- ),
770
- ],
771
- },
772
- undefined,
773
- true,
774
- undefined,
775
- this,
776
- ),
777
- ],
778
- },
779
- undefined,
780
- true,
781
- undefined,
782
- this,
783
- ),
784
- },
785
- undefined,
786
- false,
787
- undefined,
788
- this,
274
+
275
+ return (
276
+ <div
277
+ id={id}
278
+ ref={sectionRef}
279
+ className={`${styles.projectsSection} ${isVisible ? "is-visible" : ""} ${className || ""}`}
280
+ role="region"
281
+ aria-label="Projects section"
282
+ >
283
+ <div className={styles.projectsContainer}>
284
+ {/* Heading */}
285
+ <div className={styles.projectsHeader}>
286
+ <h2 className={styles.projectsTitle}>{displayHeading}</h2>
287
+ <p className={styles.projectsSubtitle}>{displaySubheading}</p>
288
+ </div>
289
+
290
+ {/* Projects carousel or empty state */}
291
+ {projects.length === 0 ? (
292
+ <div className={styles.noProjects}>
293
+ <p>No projects to display.</p>
294
+ </div>
295
+ ) : (
296
+ <div className={styles.carouselContainer}>
297
+ {/* Desktop prev button */}
298
+ {projects.length > slidesToShow && (
299
+ <button
300
+ className={`${styles.carouselControl} ${styles.prevButton} ${styles.desktopOnly} ${atBeginning ? styles.disabledButton : ""}`}
301
+ onClick={goToPrev}
302
+ aria-label="View previous projects"
303
+ aria-disabled={atBeginning}
304
+ type="button"
305
+ disabled={atBeginning}
306
+ >
307
+ <FaChevronLeft aria-hidden="true" />
308
+ </button>
309
+ )}
310
+
311
+ <div
312
+ className={styles.carouselWrapper}
313
+ aria-roledescription="carousel"
314
+ aria-label="Projects carousel"
315
+ >
316
+ <Slider ref={sliderRef} {...settings}>
317
+ {projects.map((project, index) => {
318
+ const stateInfo = getProjectStateInfo(project.state);
319
+ const extraCount = project.tags?.length - 3;
320
+ return (
321
+ <div
322
+ key={project.id || project.title + index}
323
+ className={styles.carouselSlide}
324
+ data-project-id={project.id}
325
+ aria-roledescription="slide"
326
+ aria-label={`Project ${index + 1} of ${projects.length}: ${project.title}`}
327
+ style={{ "--card-index": index }}
328
+ >
329
+ <div
330
+ className={`${styles.carouselCard} ${project.featured ? styles.featuredCard : ""}`}
331
+ >
332
+ {/* State badge */}
333
+ {project.state && (
334
+ <div
335
+ className={styles.projectStateBadge}
336
+ title={`Project status: ${stateInfo.label}`}
337
+ >
338
+ <span
339
+ className={`${styles.projectStateLabel} ${stateInfo.className}`}
340
+ >
341
+ {stateInfo.label}
342
+ </span>
343
+ </div>
344
+ )}
345
+
346
+ {/* Image + tags */}
347
+ <div
348
+ className={styles.projectImageContainer}
349
+ style={{
350
+ backgroundColor:
351
+ project.bg ||
352
+ "rgba(var(--ifm-color-primary-rgb), 0.05)",
353
+ }}
354
+ >
355
+ <img
356
+ src={project.icon}
357
+ alt={project.title}
358
+ className={styles.projectImage}
359
+ loading="lazy"
360
+ />
361
+ {project.tags?.length > 0 &&
362
+ (() => {
363
+ return (
364
+ <div className={styles.projectTags}>
365
+ {project.tags.slice(0, 3).map((tag) => (
366
+ <span
367
+ key={tag}
368
+ className={styles.projectTag}
369
+ >
370
+ {tag}
371
+ </span>
372
+ ))}
373
+ {extraCount > 0 && (
374
+ <Tooltip
375
+ msg={project.tags.slice(3).join(", ")}
376
+ underline={false}
377
+ gap={13}
378
+ >
379
+ <span
380
+ className={`${styles.projectTag} ${styles.extraTagBtn}`}
381
+ >
382
+ +{extraCount}
383
+ </span>
384
+ </Tooltip>
385
+ )}
386
+ </div>
387
+ );
388
+ })()}
389
+ {project.featured && (
390
+ <div
391
+ className={styles.featuredBadge}
392
+ title="Featured Project"
393
+ aria-label="Featured project"
394
+ >
395
+ <FaStar aria-hidden="true" />
396
+ </div>
397
+ )}
398
+ </div>
399
+
400
+ {/* Title + description */}
401
+ <div className={styles.projectContent}>
402
+ <h3 className={styles.projectTitle}>
403
+ {project.title}
404
+ </h3>
405
+ <p className={styles.projectDescription}>
406
+ {project.desc}
407
+ </p>
408
+ </div>
409
+
410
+ {/* Links */}
411
+ <div className={styles.projectLinks}>
412
+ {renderProjectLink(
413
+ project.website,
414
+ FaGlobe,
415
+ "Website",
416
+ `Visit ${project.title} website`,
417
+ )}
418
+ {renderProjectLink(
419
+ project.repo,
420
+ FaCode,
421
+ "Source",
422
+ `Repository with source code`,
423
+ )}
424
+ {renderProjectLink(
425
+ project.demo,
426
+ FaPlay,
427
+ "Demo",
428
+ `Live demo for ${project.title}`,
429
+ )}
430
+ </div>
431
+ </div>
432
+ </div>
433
+ );
434
+ })}
435
+ </Slider>
436
+
437
+ {/* Desktop dots */}
438
+ <div className={styles.desktopDotsContainer}>
439
+ {renderNavigationDots()}
440
+ </div>
441
+
442
+ {/* Mobile navigation */}
443
+ <div className={styles.mobileNavigationControls}>
444
+ {totalPages > 1 && (
445
+ <>
446
+ <button
447
+ className={`${styles.carouselControl} ${styles.prevButton} ${atBeginning ? styles.disabledButton : ""}`}
448
+ onClick={goToPrev}
449
+ aria-label="View previous projects"
450
+ aria-disabled={atBeginning}
451
+ type="button"
452
+ disabled={atBeginning}
453
+ >
454
+ <FaChevronLeft aria-hidden="true" />
455
+ </button>
456
+ <div
457
+ className={styles.dotsScrollContainer}
458
+ ref={dotsContainerRef}
459
+ >
460
+ {renderNavigationDots()}
461
+ </div>
462
+ <button
463
+ className={`${styles.carouselControl} ${styles.nextButton} ${atEnd ? styles.disabledButton : ""}`}
464
+ onClick={goToNext}
465
+ aria-label="View next projects"
466
+ aria-disabled={atEnd}
467
+ type="button"
468
+ disabled={atEnd}
469
+ >
470
+ <FaChevronRight aria-hidden="true" />
471
+ </button>
472
+ </>
473
+ )}
474
+ </div>
475
+ </div>
476
+
477
+ {/* Desktop next button */}
478
+ {projects.length > slidesToShow && (
479
+ <button
480
+ className={`${styles.carouselControl} ${styles.nextButton} ${styles.desktopOnly} ${atEnd ? styles.disabledButton : ""}`}
481
+ onClick={goToNext}
482
+ aria-label="View next projects"
483
+ aria-disabled={atEnd}
484
+ type="button"
485
+ disabled={atEnd}
486
+ >
487
+ <FaChevronRight />
488
+ </button>
489
+ )}
490
+ </div>
491
+ )}
492
+ </div>
493
+ </div>
789
494
  );
790
495
  }