@opensite/ui 0.7.9 → 0.8.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.
@@ -4,14 +4,119 @@ import React__default from 'react';
4
4
  import { motion } from 'framer-motion';
5
5
  import { clsx } from 'clsx';
6
6
  import { twMerge } from 'tailwind-merge';
7
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
8
  import { cva } from 'class-variance-authority';
8
- import { jsx, jsxs } from 'react/jsx-runtime';
9
9
  import { Img } from '@page-speed/img';
10
10
 
11
11
  // components/blocks/carousel/carousel-horizontal-cards.tsx
12
12
  function cn(...inputs) {
13
13
  return twMerge(clsx(inputs));
14
14
  }
15
+ var svgCache = /* @__PURE__ */ new Map();
16
+ function DynamicIcon({
17
+ name,
18
+ size = 28,
19
+ color,
20
+ className,
21
+ alt
22
+ }) {
23
+ const [svgContent, setSvgContent] = React.useState(null);
24
+ const [isLoading, setIsLoading] = React.useState(true);
25
+ const [error, setError] = React.useState(null);
26
+ const { url, iconName } = React.useMemo(() => {
27
+ const separator = name.includes("/") ? "/" : ":";
28
+ const [prefix, iconName2] = name.split(separator);
29
+ const baseUrl = `https://icons.opensite.ai/api/icon/${prefix}/${iconName2}?format=svg&width=${size}&height=${size}`;
30
+ return {
31
+ url: baseUrl,
32
+ iconName: iconName2
33
+ };
34
+ }, [name, size]);
35
+ React.useEffect(() => {
36
+ let isMounted = true;
37
+ const fetchSvg = async () => {
38
+ const cached = svgCache.get(url);
39
+ if (cached) {
40
+ if (isMounted) {
41
+ setSvgContent(cached);
42
+ setIsLoading(false);
43
+ }
44
+ return;
45
+ }
46
+ try {
47
+ setIsLoading(true);
48
+ setError(null);
49
+ const response = await fetch(url);
50
+ if (!response.ok) {
51
+ throw new Error(`Failed to fetch icon: ${response.status}`);
52
+ }
53
+ let svg = await response.text();
54
+ svg = processSvgForCurrentColor(svg);
55
+ svgCache.set(url, svg);
56
+ if (isMounted) {
57
+ setSvgContent(svg);
58
+ setIsLoading(false);
59
+ }
60
+ } catch (err) {
61
+ if (isMounted) {
62
+ setError(err instanceof Error ? err.message : "Failed to load icon");
63
+ setIsLoading(false);
64
+ }
65
+ }
66
+ };
67
+ fetchSvg();
68
+ return () => {
69
+ isMounted = false;
70
+ };
71
+ }, [url]);
72
+ if (isLoading) {
73
+ return /* @__PURE__ */ jsx(
74
+ "span",
75
+ {
76
+ className: cn("inline-block", className),
77
+ style: { width: size, height: size },
78
+ "aria-hidden": "true"
79
+ }
80
+ );
81
+ }
82
+ if (error || !svgContent) {
83
+ return /* @__PURE__ */ jsx(
84
+ "span",
85
+ {
86
+ className: cn("inline-block", className),
87
+ style: { width: size, height: size },
88
+ role: "img",
89
+ "aria-label": alt || iconName
90
+ }
91
+ );
92
+ }
93
+ return /* @__PURE__ */ jsx(
94
+ "span",
95
+ {
96
+ className: cn("inline-flex items-center justify-center", className),
97
+ style: {
98
+ width: size,
99
+ height: size,
100
+ color: color || "inherit"
101
+ },
102
+ role: "img",
103
+ "aria-label": alt || iconName,
104
+ dangerouslySetInnerHTML: { __html: svgContent }
105
+ }
106
+ );
107
+ }
108
+ function processSvgForCurrentColor(svg) {
109
+ let processed = svg;
110
+ processed = processed.replace(
111
+ /stroke=["'](#000000|#000|black)["']/gi,
112
+ 'stroke="currentColor"'
113
+ );
114
+ processed = processed.replace(
115
+ /fill=["'](#000000|#000|black)["']/gi,
116
+ 'fill="currentColor"'
117
+ );
118
+ return processed;
119
+ }
15
120
  function normalizePhoneNumber(input) {
16
121
  const trimmed = input.trim();
17
122
  if (trimmed.toLowerCase().startsWith("tel:")) {
@@ -430,110 +535,49 @@ var Pressable = React.forwardRef(
430
535
  }
431
536
  );
432
537
  Pressable.displayName = "Pressable";
433
- var svgCache = /* @__PURE__ */ new Map();
434
- function DynamicIcon({
435
- name,
436
- size = 28,
437
- color,
538
+ function CarouselPagination({
539
+ onPrevious,
540
+ onNext,
541
+ canScrollPrevious = true,
542
+ canScrollNext = true,
543
+ iconSize = 24,
438
544
  className,
439
- alt
545
+ buttonClassName,
546
+ previousIcon = "lucide/arrow-left",
547
+ nextIcon = "lucide/arrow-right",
548
+ previousAriaLabel = "Previous",
549
+ nextAriaLabel = "Next"
440
550
  }) {
441
- const [svgContent, setSvgContent] = React.useState(null);
442
- const [isLoading, setIsLoading] = React.useState(true);
443
- const [error, setError] = React.useState(null);
444
- const { url, iconName } = React.useMemo(() => {
445
- const separator = name.includes("/") ? "/" : ":";
446
- const [prefix, iconName2] = name.split(separator);
447
- const baseUrl = `https://icons.opensite.ai/api/icon/${prefix}/${iconName2}?format=svg&width=${size}&height=${size}`;
448
- return {
449
- url: baseUrl,
450
- iconName: iconName2
451
- };
452
- }, [name, size]);
453
- React.useEffect(() => {
454
- let isMounted = true;
455
- const fetchSvg = async () => {
456
- const cached = svgCache.get(url);
457
- if (cached) {
458
- if (isMounted) {
459
- setSvgContent(cached);
460
- setIsLoading(false);
461
- }
462
- return;
463
- }
464
- try {
465
- setIsLoading(true);
466
- setError(null);
467
- const response = await fetch(url);
468
- if (!response.ok) {
469
- throw new Error(`Failed to fetch icon: ${response.status}`);
470
- }
471
- let svg = await response.text();
472
- svg = processSvgForCurrentColor(svg);
473
- svgCache.set(url, svg);
474
- if (isMounted) {
475
- setSvgContent(svg);
476
- setIsLoading(false);
477
- }
478
- } catch (err) {
479
- if (isMounted) {
480
- setError(err instanceof Error ? err.message : "Failed to load icon");
481
- setIsLoading(false);
482
- }
483
- }
484
- };
485
- fetchSvg();
486
- return () => {
487
- isMounted = false;
488
- };
489
- }, [url]);
490
- if (isLoading) {
491
- return /* @__PURE__ */ jsx(
492
- "span",
551
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex justify-end gap-2", className), children: [
552
+ /* @__PURE__ */ jsx(
553
+ Pressable,
493
554
  {
494
- className: cn("inline-block", className),
495
- style: { width: size, height: size },
496
- "aria-hidden": "true"
555
+ onClick: onPrevious,
556
+ disabled: !canScrollPrevious,
557
+ "aria-label": previousAriaLabel,
558
+ asButton: true,
559
+ className: cn(
560
+ "relative z-40 flex h-10 w-10 items-center justify-center rounded-full disabled:opacity-50",
561
+ buttonClassName
562
+ ),
563
+ children: /* @__PURE__ */ jsx(DynamicIcon, { name: previousIcon, size: iconSize })
497
564
  }
498
- );
499
- }
500
- if (error || !svgContent) {
501
- return /* @__PURE__ */ jsx(
502
- "span",
565
+ ),
566
+ /* @__PURE__ */ jsx(
567
+ Pressable,
503
568
  {
504
- className: cn("inline-block", className),
505
- style: { width: size, height: size },
506
- role: "img",
507
- "aria-label": alt || iconName
569
+ onClick: onNext,
570
+ disabled: !canScrollNext,
571
+ "aria-label": nextAriaLabel,
572
+ asButton: true,
573
+ className: cn(
574
+ "relative z-40 flex h-10 w-10 items-center justify-center rounded-full disabled:opacity-50",
575
+ buttonClassName
576
+ ),
577
+ children: /* @__PURE__ */ jsx(DynamicIcon, { name: nextIcon, size: iconSize })
508
578
  }
509
- );
510
- }
511
- return /* @__PURE__ */ jsx(
512
- "span",
513
- {
514
- className: cn("inline-flex items-center justify-center", className),
515
- style: {
516
- width: size,
517
- height: size,
518
- color: color || "inherit"
519
- },
520
- role: "img",
521
- "aria-label": alt || iconName,
522
- dangerouslySetInnerHTML: { __html: svgContent }
523
- }
524
- );
525
- }
526
- function processSvgForCurrentColor(svg) {
527
- let processed = svg;
528
- processed = processed.replace(
529
- /stroke=["'](#000000|#000|black)["']/gi,
530
- 'stroke="currentColor"'
531
- );
532
- processed = processed.replace(
533
- /fill=["'](#000000|#000|black)["']/gi,
534
- 'fill="currentColor"'
535
- );
536
- return processed;
579
+ )
580
+ ] });
537
581
  }
538
582
  var maxWidthStyles = {
539
583
  sm: "max-w-screen-sm",
@@ -932,28 +976,22 @@ function CarouselHorizontalCards({
932
976
  const carouselRef = React.useRef(null);
933
977
  const [isAtStart, setIsAtStart] = React.useState(true);
934
978
  const [isAtEnd, setIsAtEnd] = React.useState(false);
935
- const getCardWidth = React.useCallback(() => {
936
- if (typeof window === "undefined") return 320;
937
- if (window.innerWidth >= 1024) return 400;
938
- if (window.innerWidth >= 640) return 360;
939
- return 320;
940
- }, []);
941
- const scroll = (direction) => {
979
+ const scrollLeft = () => {
980
+ if (carouselRef.current) {
981
+ carouselRef.current.scrollBy({ left: -300, behavior: "smooth" });
982
+ }
983
+ };
984
+ const scrollRight = () => {
942
985
  if (carouselRef.current) {
943
- const { scrollLeft } = carouselRef.current;
944
- const cardWidth = getCardWidth();
945
- const gap = 16;
946
- const scrollAmount = cardWidth + gap;
947
- const newScrollLeft = direction === "left" ? scrollLeft - scrollAmount : scrollLeft + scrollAmount;
948
- carouselRef.current.scrollTo({ left: newScrollLeft, behavior: "smooth" });
986
+ carouselRef.current.scrollBy({ left: 300, behavior: "smooth" });
949
987
  }
950
988
  };
951
989
  React.useEffect(() => {
952
990
  const checkScrollPosition = () => {
953
991
  if (carouselRef.current) {
954
- const { scrollLeft, scrollWidth, clientWidth } = carouselRef.current;
955
- setIsAtStart(scrollLeft < 10);
956
- setIsAtEnd(scrollLeft + clientWidth >= scrollWidth - 10);
992
+ const { scrollLeft: scrollLeft2, scrollWidth, clientWidth } = carouselRef.current;
993
+ setIsAtStart(scrollLeft2 < 10);
994
+ setIsAtEnd(scrollLeft2 + clientWidth >= scrollWidth - 10);
957
995
  }
958
996
  };
959
997
  const currentRef = carouselRef.current;
@@ -969,41 +1007,6 @@ function CarouselHorizontalCards({
969
1007
  window.removeEventListener("resize", checkScrollPosition);
970
1008
  };
971
1009
  }, [items]);
972
- const renderItems = () => {
973
- if (itemsSlot) return itemsSlot;
974
- if (!items || items.length === 0) return null;
975
- return items.map((item, index) => /* @__PURE__ */ jsx(
976
- motion.div,
977
- {
978
- className: cn(
979
- "group w-[320px] shrink-0 snap-start sm:w-[360px] lg:w-[400px]",
980
- item.className
981
- ),
982
- initial: { opacity: 0, y: 20 },
983
- animate: { opacity: 1, y: 0 },
984
- transition: { duration: 0.5, delay: index * 0.1 },
985
- children: /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-lg border bg-card text-card-foreground shadow-sm transition-shadow hover:shadow-md", children: [
986
- /* @__PURE__ */ jsx(
987
- Img,
988
- {
989
- alt: typeof item.title === "string" ? item.title : `Card ${index + 1}`,
990
- className: cn("aspect-video w-full object-cover transition-transform group-hover:scale-105", item.imageClassName),
991
- src: item.imageSrc,
992
- optixFlowConfig
993
- }
994
- ),
995
- /* @__PURE__ */ jsxs("div", { className: "p-4", children: [
996
- item.title && (typeof item.title === "string" ? /* @__PURE__ */ jsx("h3", { className: "text-md font-semibold leading-tight text-card-foreground", children: item.title }) : /* @__PURE__ */ jsx("div", { children: item.title })),
997
- (item.count !== void 0 || item.countLabel) && /* @__PURE__ */ jsxs("div", { className: "mt-4", children: [
998
- item.count !== void 0 && /* @__PURE__ */ jsx("p", { className: "text-xl font-bold", children: item.count }),
999
- item.countLabel && (typeof item.countLabel === "string" ? /* @__PURE__ */ jsx("p", { className: "text-xs font-medium uppercase tracking-wider text-muted-foreground", children: item.countLabel }) : /* @__PURE__ */ jsx("div", { children: item.countLabel }))
1000
- ] })
1001
- ] })
1002
- ] })
1003
- },
1004
- item.id
1005
- ));
1006
- };
1007
1010
  return /* @__PURE__ */ jsx(
1008
1011
  Section,
1009
1012
  {
@@ -1014,74 +1017,143 @@ function CarouselHorizontalCards({
1014
1017
  patternOpacity,
1015
1018
  "aria-labelledby": "carousel-title",
1016
1019
  children: /* @__PURE__ */ jsxs("div", { className: cn("container mx-auto px-4 md:px-6", containerClassName), children: [
1017
- /* @__PURE__ */ jsx("div", { className: cn("mb-8 flex items-center justify-between gap-4", headerClassName), children: /* @__PURE__ */ jsxs("div", { children: [
1018
- heading && /* @__PURE__ */ jsxs("a", { href: headingHref, className: "group inline-flex items-center", children: [
1019
- typeof heading === "string" ? /* @__PURE__ */ jsx(
1020
- "h2",
1021
- {
1022
- id: "carousel-title",
1023
- className: cn("text-2xl font-bold tracking-tight text-card-foreground md:text-3xl", headingClassName),
1024
- children: heading
1025
- }
1026
- ) : /* @__PURE__ */ jsx("div", { className: headingClassName, children: heading }),
1027
- /* @__PURE__ */ jsx(
1028
- DynamicIcon,
1029
- {
1030
- name: "lucide/chevron-right",
1031
- size: 24,
1032
- className: "ml-2 flex-shrink-0 self-center transition-transform group-hover:translate-x-1"
1033
- }
1034
- )
1035
- ] }),
1036
- subtitle && (typeof subtitle === "string" ? /* @__PURE__ */ jsx("p", { className: cn("mt-1 text-muted-foreground", subtitleClassName), children: subtitle }) : /* @__PURE__ */ jsx("div", { className: subtitleClassName, children: subtitle }))
1037
- ] }) }),
1038
- /* @__PURE__ */ jsxs("div", { className: "relative", children: [
1020
+ /* @__PURE__ */ jsx(
1021
+ "div",
1022
+ {
1023
+ className: cn(
1024
+ "mb-8 flex items-center justify-between gap-4",
1025
+ headerClassName
1026
+ ),
1027
+ children: /* @__PURE__ */ jsxs("div", { children: [
1028
+ heading && /* @__PURE__ */ jsxs("a", { href: headingHref, className: "group inline-flex items-center", children: [
1029
+ typeof heading === "string" ? /* @__PURE__ */ jsx(
1030
+ "h2",
1031
+ {
1032
+ id: "carousel-title",
1033
+ className: cn(
1034
+ "text-2xl font-bold tracking-tight text-card-foreground md:text-3xl",
1035
+ headingClassName
1036
+ ),
1037
+ children: heading
1038
+ }
1039
+ ) : /* @__PURE__ */ jsx("div", { className: headingClassName, children: heading }),
1040
+ /* @__PURE__ */ jsx(
1041
+ DynamicIcon,
1042
+ {
1043
+ name: "lucide/chevron-right",
1044
+ size: 24,
1045
+ className: "ml-2 shrink-0 self-center transition-transform group-hover:translate-x-1"
1046
+ }
1047
+ )
1048
+ ] }),
1049
+ subtitle && (typeof subtitle === "string" ? /* @__PURE__ */ jsx(
1050
+ "p",
1051
+ {
1052
+ className: cn(
1053
+ "mt-1 text-muted-foreground",
1054
+ subtitleClassName
1055
+ ),
1056
+ children: subtitle
1057
+ }
1058
+ ) : /* @__PURE__ */ jsx("div", { className: subtitleClassName, children: subtitle }))
1059
+ ] })
1060
+ }
1061
+ ),
1062
+ /* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
1039
1063
  /* @__PURE__ */ jsx(
1040
1064
  "div",
1041
1065
  {
1066
+ className: "flex w-full overflow-x-scroll overscroll-x-auto scroll-smooth py-4 [scrollbar-width:none] md:py-6",
1042
1067
  ref: carouselRef,
1043
- className: cn(
1044
- "scrollbar-hide flex w-full space-x-4 overflow-x-auto pb-4",
1045
- "snap-x snap-mandatory scroll-pl-0",
1046
- carouselClassName
1047
- ),
1048
- children: renderItems()
1068
+ children: /* @__PURE__ */ jsxs(
1069
+ "div",
1070
+ {
1071
+ className: cn(
1072
+ "flex flex-row justify-start gap-4 pl-4",
1073
+ carouselClassName
1074
+ ),
1075
+ children: [
1076
+ items?.map((item, index) => /* @__PURE__ */ jsx(
1077
+ motion.div,
1078
+ {
1079
+ className: cn(
1080
+ "rounded-lg last:pr-[5%] md:last:pr-[33%]"
1081
+ ),
1082
+ initial: { opacity: 0, y: 20 },
1083
+ animate: { opacity: 1, y: 0 },
1084
+ transition: { duration: 0.5, delay: 0.2 * index, ease: "easeOut" },
1085
+ children: /* @__PURE__ */ jsx(
1086
+ "div",
1087
+ {
1088
+ className: cn(
1089
+ "group w-56 shrink-0 md:w-96",
1090
+ item.className
1091
+ ),
1092
+ children: /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-lg border bg-card text-card-foreground shadow-sm transition-shadow hover:shadow-md", children: [
1093
+ /* @__PURE__ */ jsx(
1094
+ Img,
1095
+ {
1096
+ alt: typeof item.title === "string" ? item.title : `Card ${index + 1}`,
1097
+ className: cn(
1098
+ "aspect-video w-full object-cover transition-transform group-hover:scale-105",
1099
+ item.imageClassName
1100
+ ),
1101
+ src: item.imageSrc,
1102
+ optixFlowConfig
1103
+ }
1104
+ ),
1105
+ /* @__PURE__ */ jsxs("div", { className: "p-4", children: [
1106
+ item.title && (typeof item.title === "string" ? /* @__PURE__ */ jsx("h3", { className: "text-md font-semibold leading-tight text-card-foreground", children: item.title }) : /* @__PURE__ */ jsx("div", { children: item.title })),
1107
+ (item.count !== void 0 || item.countLabel) && /* @__PURE__ */ jsxs("div", { className: "mt-4", children: [
1108
+ item.count !== void 0 && /* @__PURE__ */ jsx("p", { className: "text-xl font-bold", children: item.count }),
1109
+ item.countLabel && (typeof item.countLabel === "string" ? /* @__PURE__ */ jsx("p", { className: "text-xs font-medium uppercase tracking-wider text-muted-foreground", children: item.countLabel }) : /* @__PURE__ */ jsx("div", { children: item.countLabel }))
1110
+ ] }),
1111
+ item.actions && item.actions.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-4 flex flex-wrap gap-2", children: item.actions.map((action, actionIndex) => {
1112
+ const {
1113
+ label,
1114
+ icon,
1115
+ iconAfter,
1116
+ children,
1117
+ className: actionClassName,
1118
+ asButton,
1119
+ ...pressableProps
1120
+ } = action;
1121
+ return /* @__PURE__ */ jsx(
1122
+ Pressable,
1123
+ {
1124
+ asButton,
1125
+ className: actionClassName,
1126
+ ...pressableProps,
1127
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
1128
+ icon,
1129
+ label,
1130
+ iconAfter
1131
+ ] })
1132
+ },
1133
+ actionIndex
1134
+ );
1135
+ }) })
1136
+ ] })
1137
+ ] })
1138
+ }
1139
+ )
1140
+ },
1141
+ item.id
1142
+ )),
1143
+ itemsSlot
1144
+ ]
1145
+ }
1146
+ )
1049
1147
  }
1050
1148
  ),
1051
- !isAtStart && /* @__PURE__ */ jsx(
1052
- Pressable,
1053
- {
1054
- onClick: () => scroll("left"),
1055
- className: cn(
1056
- "absolute left-4 top-1/2 z-10 -translate-y-1/2",
1057
- "flex h-12 w-12 items-center justify-center",
1058
- "rounded-full border border-border/50 bg-background shadow-lg",
1059
- "text-foreground transition-all duration-200",
1060
- "hover:bg-accent hover:shadow-xl hover:scale-105",
1061
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
1062
- navigationClassName
1063
- ),
1064
- "aria-label": "Scroll left",
1065
- asButton: true,
1066
- children: /* @__PURE__ */ jsx(DynamicIcon, { name: "lucide/chevron-left", size: 24 })
1067
- }
1068
- ),
1069
- !isAtEnd && /* @__PURE__ */ jsx(
1070
- Pressable,
1149
+ /* @__PURE__ */ jsx(
1150
+ CarouselPagination,
1071
1151
  {
1072
- onClick: () => scroll("right"),
1073
- className: cn(
1074
- "absolute right-4 top-1/2 z-10 -translate-y-1/2",
1075
- "flex h-12 w-12 items-center justify-center",
1076
- "rounded-full border border-border/50 bg-background shadow-lg",
1077
- "text-foreground transition-all duration-200",
1078
- "hover:bg-accent hover:shadow-xl hover:scale-105",
1079
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
1080
- navigationClassName
1081
- ),
1082
- "aria-label": "Scroll right",
1083
- asButton: true,
1084
- children: /* @__PURE__ */ jsx(DynamicIcon, { name: "lucide/chevron-right", size: 24 })
1152
+ onPrevious: scrollLeft,
1153
+ onNext: scrollRight,
1154
+ canScrollPrevious: !isAtStart,
1155
+ canScrollNext: !isAtEnd,
1156
+ className: cn("mr-0 md:mr-10", navigationClassName)
1085
1157
  }
1086
1158
  )
1087
1159
  ] })