@reblu/site-blocks 0.18.0 → 0.20.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.
package/dist/client.cjs CHANGED
@@ -34,10 +34,14 @@ __export(client_exports, {
34
34
  AddToCartButton: () => AddToCartButton,
35
35
  CartIcon: () => CartIcon,
36
36
  Collection: () => Collection,
37
+ CourseSyllabus: () => CourseSyllabus,
37
38
  DATA_REQUEST_FIELD_LIMITS: () => DATA_REQUEST_FIELD_LIMITS,
38
39
  DATA_REQUEST_TYPES: () => import_site_contracts.DATA_REQUEST_TYPES,
40
+ DEFAULT_COURSE_SYLLABUS_LABELS: () => DEFAULT_COURSE_SYLLABUS_LABELS,
39
41
  DEFAULT_DATA_REQUEST_FORM_LABELS: () => DEFAULT_DATA_REQUEST_FORM_LABELS,
42
+ DEFAULT_PRODUCT_CAROUSEL_LABELS: () => DEFAULT_PRODUCT_CAROUSEL_LABELS,
40
43
  DataRequestForm: () => DataRequestForm,
44
+ ProductCarousel: () => ProductCarousel,
41
45
  ShareButtons: () => ShareButtons,
42
46
  useCollection: () => useCollection
43
47
  });
@@ -226,7 +230,7 @@ function Collection({
226
230
  ] })
227
231
  ] });
228
232
  }
229
- var PAGE_BTN_CLASS = "inline-flex h-9 items-center rounded-md border border-border px-4 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
233
+ var PAGE_BTN_CLASS = "inline-flex h-9 cursor-pointer items-center rounded-md border border-border px-4 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
230
234
  function FacetChip({ label, pressed, color, onClick }) {
231
235
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
232
236
  "button",
@@ -236,7 +240,7 @@ function FacetChip({ label, pressed, color, onClick }) {
236
240
  onClick,
237
241
  style: color ? { "--facet-color": color } : void 0,
238
242
  className: (0, import_utils.cn)(
239
- "inline-flex items-center rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
243
+ "inline-flex cursor-pointer items-center rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
240
244
  pressed ? color ? "border-transparent bg-[var(--facet-color)] text-white" : "border-transparent bg-primary text-primary-foreground" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"
241
245
  ),
242
246
  children: label
@@ -321,7 +325,7 @@ function ShareButtons({ url, title, className, labels }) {
321
325
  {
322
326
  type: "button",
323
327
  onClick: handleCopy,
324
- className: (0, import_utils2.cn)(LINK_CLASS, "w-auto gap-2 px-3", copied && "border-primary text-primary"),
328
+ className: (0, import_utils2.cn)(LINK_CLASS, "w-auto cursor-pointer gap-2 px-3", copied && "border-primary text-primary"),
325
329
  "aria-label": t.copy,
326
330
  children: [
327
331
  copied ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CheckIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CopyIcon, {}),
@@ -442,7 +446,7 @@ function AddToCartButton({
442
446
  "data-slot": "add-to-cart-button",
443
447
  "data-status": status,
444
448
  className: (0, import_utils3.cn)(
445
- "inline-flex items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
449
+ "inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
446
450
  isDisabled ? "cursor-not-allowed bg-muted text-muted-foreground" : "bg-primary text-primary-foreground hover:bg-primary/90",
447
451
  status === "error" && "bg-destructive text-destructive-foreground",
448
452
  className
@@ -458,7 +462,7 @@ var import_utils4 = require("@reblu/site-ui/utils");
458
462
  var import_jsx_runtime4 = require("react/jsx-runtime");
459
463
  var DEFAULT_LABELS4 = {
460
464
  cart: "Carrito",
461
- itemsSuffix: (count) => `${count} art\xEDculo${count === 1 ? "" : "s"}`
465
+ itemsSuffix: (count2) => `${count2} art\xEDculo${count2 === 1 ? "" : "s"}`
462
466
  };
463
467
  var BUMP_DURATION_MS = 400;
464
468
  function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }) {
@@ -527,11 +531,402 @@ function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }) {
527
531
  );
528
532
  }
529
533
 
530
- // src/components/data-request-form.tsx
534
+ // src/components/product-carousel.tsx
531
535
  var import_react4 = require("react");
536
+ var import_button_variants = require("@reblu/site-ui/button-variants");
537
+ var import_utils6 = require("@reblu/site-ui/utils");
538
+
539
+ // src/components/_heading.tsx
540
+ var import_jsx_runtime5 = require("react/jsx-runtime");
541
+ function Heading({
542
+ level,
543
+ className,
544
+ children
545
+ }) {
546
+ const Tag = `h${level}`;
547
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Tag, { className, children });
548
+ }
549
+
550
+ // src/components/product-card.tsx
532
551
  var import_utils5 = require("@reblu/site-ui/utils");
552
+
553
+ // src/lib/format-price.ts
554
+ function formatPrice(cents, currency = "EUR", locale = "es-ES") {
555
+ return new Intl.NumberFormat(locale, {
556
+ style: "currency",
557
+ currency
558
+ }).format(cents / 100);
559
+ }
560
+
561
+ // src/components/product-card.tsx
562
+ var import_jsx_runtime6 = require("react/jsx-runtime");
563
+ function ProductCard({ product, href, currencyCode = "EUR", locale, className }) {
564
+ const image = product.images.find((i) => i.isPrimary) ?? product.images[0];
565
+ const hasDiscount = product.comparePrice != null && product.comparePrice > product.price;
566
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
567
+ "article",
568
+ {
569
+ className: (0, import_utils5.cn)(
570
+ "group flex flex-col overflow-hidden rounded-lg border bg-card transition-shadow hover:shadow-lg",
571
+ className
572
+ ),
573
+ "data-slot": "product-card",
574
+ children: [
575
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "relative aspect-square overflow-hidden bg-muted", children: image ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
576
+ "img",
577
+ {
578
+ src: image.url,
579
+ alt: image.alt ?? product.name,
580
+ loading: "lazy",
581
+ className: "h-full w-full object-cover transition-transform group-hover:scale-105"
582
+ }
583
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex h-full items-center justify-center text-4xl text-muted-foreground/30", "aria-hidden": "true", children: "\u{1F6CD}\uFE0F" }) }),
584
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-1 flex-col gap-2 p-4", children: [
585
+ product.category && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "inline-flex w-fit items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground", children: product.category.name }),
586
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { className: "text-base font-semibold leading-tight", children: href ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("a", { href, className: "transition-colors hover:text-primary", children: product.name }) : product.name }),
587
+ product.shortDescription && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "line-clamp-2 text-sm text-muted-foreground", children: product.shortDescription }),
588
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mt-auto flex items-baseline gap-2 pt-2", children: [
589
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-lg font-bold text-foreground", children: formatPrice(product.price, currencyCode, locale) }),
590
+ hasDiscount && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-sm text-muted-foreground line-through", children: formatPrice(product.comparePrice, currencyCode, locale) })
591
+ ] })
592
+ ] })
593
+ ]
594
+ }
595
+ );
596
+ }
597
+
598
+ // src/components/product-carousel.tsx
599
+ var import_jsx_runtime7 = require("react/jsx-runtime");
600
+ var DEFAULT_PRODUCT_CAROUSEL_LABELS = {
601
+ region: "Productos relacionados",
602
+ roleDescription: "carrusel",
603
+ previous: "Anterior",
604
+ next: "Siguiente"
605
+ };
606
+ var EDGE_EPSILON = 1;
607
+ function prefersReducedMotion() {
608
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false;
609
+ }
610
+ function ChevronIcon({ direction }) {
611
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
612
+ "svg",
613
+ {
614
+ "aria-hidden": "true",
615
+ viewBox: "0 0 24 24",
616
+ fill: "none",
617
+ stroke: "currentColor",
618
+ strokeWidth: 2,
619
+ strokeLinecap: "round",
620
+ strokeLinejoin: "round",
621
+ className: "rtl:-scale-x-100",
622
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: direction === "left" ? "m15 18-6-6 6-6" : "m9 18 6-6-6-6" })
623
+ }
624
+ );
625
+ }
626
+ function ProductCarousel(props) {
627
+ const { products, heading, headingLevel = 2, headingClassName, labels, className, itemClassName } = props;
628
+ const t = { ...DEFAULT_PRODUCT_CAROUSEL_LABELS, ...labels };
629
+ const trackId = (0, import_react4.useId)();
630
+ const headingId = (0, import_react4.useId)();
631
+ const trackRef = (0, import_react4.useRef)(null);
632
+ const [canPrev, setCanPrev] = (0, import_react4.useState)(false);
633
+ const [canNext, setCanNext] = (0, import_react4.useState)(false);
634
+ const update = (0, import_react4.useCallback)(() => {
635
+ const el = trackRef.current;
636
+ if (!el) return;
637
+ const max = el.scrollWidth - el.clientWidth;
638
+ const pos = Math.abs(el.scrollLeft);
639
+ setCanPrev(pos > EDGE_EPSILON);
640
+ setCanNext(pos < max - EDGE_EPSILON);
641
+ }, []);
642
+ (0, import_react4.useEffect)(() => {
643
+ const el = trackRef.current;
644
+ if (!el) return;
645
+ update();
646
+ el.addEventListener("scroll", update, { passive: true });
647
+ let observer;
648
+ if (typeof ResizeObserver !== "undefined") {
649
+ observer = new ResizeObserver(update);
650
+ observer.observe(el);
651
+ } else {
652
+ window.addEventListener("resize", update);
653
+ }
654
+ return () => {
655
+ el.removeEventListener("scroll", update);
656
+ if (observer) observer.disconnect();
657
+ else window.removeEventListener("resize", update);
658
+ };
659
+ }, [update, products.length]);
660
+ const scrollByPage = (direction) => {
661
+ const el = trackRef.current;
662
+ if (!el) return;
663
+ const rtl = typeof getComputedStyle === "function" && getComputedStyle(el).direction === "rtl";
664
+ el.scrollBy({
665
+ left: direction * el.clientWidth * (rtl ? -1 : 1),
666
+ behavior: prefersReducedMotion() ? "auto" : "smooth"
667
+ });
668
+ };
669
+ if (products.length === 0) return null;
670
+ const scrollable = canPrev || canNext;
671
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "data-slot": "product-carousel", className: (0, import_utils6.cn)("flex flex-col gap-4", className), children: [
672
+ (heading || scrollable) && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center justify-between gap-4", children: [
673
+ heading ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { id: headingId, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Heading, { level: headingLevel, className: headingClassName ?? "text-xl font-semibold", children: heading }) }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", {}),
674
+ scrollable && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hidden gap-2 sm:flex", "data-slot": "product-carousel-controls", children: [
675
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
676
+ "button",
677
+ {
678
+ type: "button",
679
+ className: (0, import_utils6.cn)((0, import_button_variants.buttonVariants)({ variant: "outline", size: "icon" }), "rounded-full"),
680
+ "aria-label": t.previous,
681
+ "aria-controls": trackId,
682
+ disabled: !canPrev,
683
+ onClick: () => scrollByPage(-1),
684
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChevronIcon, { direction: "left" })
685
+ }
686
+ ),
687
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
688
+ "button",
689
+ {
690
+ type: "button",
691
+ className: (0, import_utils6.cn)((0, import_button_variants.buttonVariants)({ variant: "outline", size: "icon" }), "rounded-full"),
692
+ "aria-label": t.next,
693
+ "aria-controls": trackId,
694
+ disabled: !canNext,
695
+ onClick: () => scrollByPage(1),
696
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChevronIcon, { direction: "right" })
697
+ }
698
+ )
699
+ ] })
700
+ ] }),
701
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
702
+ "div",
703
+ {
704
+ ref: trackRef,
705
+ id: trackId,
706
+ role: "region",
707
+ "aria-roledescription": t.roleDescription,
708
+ ...heading ? { "aria-labelledby": headingId } : { "aria-label": t.region },
709
+ tabIndex: 0,
710
+ "data-slot": "product-carousel-track",
711
+ className: "snap-x snap-mandatory overflow-x-auto overscroll-x-contain scroll-smooth rounded-lg pb-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:scroll-auto",
712
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { role: "list", className: "m-0 flex list-none gap-4 p-0", children: products.map((product, index) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
713
+ "li",
714
+ {
715
+ className: (0, import_utils6.cn)(
716
+ "shrink-0 snap-start basis-[calc((100%-1rem)/1.5)] sm:basis-[calc((100%-2rem)/3)] lg:basis-[calc((100%-3rem)/4)]",
717
+ itemClassName
718
+ ),
719
+ children: props.renderProduct ? props.renderProduct(product, index) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
720
+ ProductCard,
721
+ {
722
+ product,
723
+ href: props.productHref?.(product),
724
+ currencyCode: props.currencyCode,
725
+ locale: props.locale,
726
+ className: "h-full"
727
+ }
728
+ )
729
+ },
730
+ product.id
731
+ )) })
732
+ }
733
+ )
734
+ ] });
735
+ }
736
+
737
+ // src/components/course-syllabus.tsx
738
+ var import_react5 = require("react");
739
+ var import_utils8 = require("@reblu/site-ui/utils");
740
+
741
+ // src/lib/slot-class-name.ts
742
+ var import_utils7 = require("@reblu/site-ui/utils");
743
+ var MERGE_SAFE_TEXT = /(?:^|:)!?text-(?:left|center|right|justify|start|end|wrap|nowrap|balance|pretty|ellipsis|clip)$/;
744
+ var TEXT_UTILITY = /(?:^|:)!?-?text-/;
745
+ function bypassesMerge(token) {
746
+ return TEXT_UTILITY.test(token) && !MERGE_SAFE_TEXT.test(token);
747
+ }
748
+ function slotClassName(structural, defaults, consumer) {
749
+ if (consumer === void 0 || consumer.trim() === "") return (0, import_utils7.cn)(structural, defaults);
750
+ const tokens = consumer.split(/\s+/).filter(Boolean);
751
+ const verbatim = tokens.filter(bypassesMerge);
752
+ const merged = (0, import_utils7.cn)(structural, tokens.filter((token) => !bypassesMerge(token)));
753
+ return [merged, ...verbatim].filter(Boolean).join(" ");
754
+ }
755
+
756
+ // src/components/course-syllabus.tsx
757
+ var import_jsx_runtime8 = require("react/jsx-runtime");
758
+ var DEFAULT_COURSE_SYLLABUS_LABELS = {
759
+ heading: "Programa",
760
+ moduleSingular: "m\xF3dulo",
761
+ modulePlural: "m\xF3dulos",
762
+ lessonSingular: "lecci\xF3n",
763
+ lessonPlural: "lecciones",
764
+ freePreview: "Vista previa gratuita",
765
+ hours: "h",
766
+ minutes: "min"
767
+ };
768
+ var SEPARATOR = " \xB7 ";
769
+ function count(n, singular, plural) {
770
+ return `${n} ${n === 1 ? singular : plural}`;
771
+ }
772
+ function formatDuration(seconds, labels) {
773
+ const totalMinutes = Math.max(1, Math.round(seconds / 60));
774
+ const hours = Math.floor(totalMinutes / 60);
775
+ const minutes = totalMinutes % 60;
776
+ if (hours === 0) return `${minutes} ${labels.minutes}`;
777
+ return minutes === 0 ? `${hours} ${labels.hours}` : `${hours} ${labels.hours} ${minutes} ${labels.minutes}`;
778
+ }
779
+ function moduleSeconds(lessons) {
780
+ let total = 0;
781
+ for (const l of lessons) {
782
+ if (typeof l.durationSeconds !== "number" || l.durationSeconds <= 0) return null;
783
+ total += l.durationSeconds;
784
+ }
785
+ return total;
786
+ }
787
+ function CourseSyllabus({
788
+ modules,
789
+ labels: labelOverrides,
790
+ headingLevel = 2,
791
+ headingClassName,
792
+ className
793
+ }) {
794
+ const labels = { ...DEFAULT_COURSE_SYLLABUS_LABELS, ...labelOverrides };
795
+ const baseId = (0, import_react5.useId)();
796
+ const [open, setOpen] = (0, import_react5.useState)(() => /* @__PURE__ */ new Set());
797
+ if (modules.length === 0) return null;
798
+ const totalLessons = modules.reduce((sum, m) => sum + (m.lessons?.length ?? 0), 0);
799
+ const summary = [count(modules.length, labels.moduleSingular, labels.modulePlural)];
800
+ if (totalLessons > 0)
801
+ summary.push(count(totalLessons, labels.lessonSingular, labels.lessonPlural));
802
+ const headingId = `${baseId}-heading`;
803
+ const Tag = `h${headingLevel}`;
804
+ function toggle(id) {
805
+ setOpen((prev) => {
806
+ const next = new Set(prev);
807
+ if (next.has(id)) next.delete(id);
808
+ else next.add(id);
809
+ return next;
810
+ });
811
+ }
812
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
813
+ "section",
814
+ {
815
+ "data-slot": "course-syllabus",
816
+ "aria-labelledby": headingId,
817
+ className: (0, import_utils8.cn)("space-y-4", className),
818
+ children: [
819
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "space-y-1", children: [
820
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
821
+ Tag,
822
+ {
823
+ id: headingId,
824
+ className: slotClassName("", "text-xl font-semibold", headingClassName),
825
+ children: labels.heading
826
+ }
827
+ ),
828
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { "data-slot": "course-syllabus-summary", className: "text-sm text-muted-foreground", children: summary.join(SEPARATOR) })
829
+ ] }),
830
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ol", { className: "flex list-none flex-col gap-2.5 p-0", children: modules.map((module2, index) => {
831
+ const lessons = module2.lessons ?? [];
832
+ const number = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
833
+ "span",
834
+ {
835
+ "aria-hidden": "true",
836
+ className: "flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-sm font-semibold text-primary",
837
+ children: index + 1
838
+ }
839
+ );
840
+ if (lessons.length === 0) {
841
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
842
+ "li",
843
+ {
844
+ "data-slot": "course-syllabus-module",
845
+ className: "flex items-center gap-3.5 rounded-lg border border-border bg-card px-4 py-3.5",
846
+ children: [
847
+ number,
848
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-semibold leading-snug", children: module2.title })
849
+ ]
850
+ },
851
+ module2.id
852
+ );
853
+ }
854
+ const isOpen = open.has(module2.id);
855
+ const panelId = `${baseId}-module-${index}`;
856
+ const seconds = moduleSeconds(lessons);
857
+ const meta = [count(lessons.length, labels.lessonSingular, labels.lessonPlural)];
858
+ if (seconds !== null) meta.push(formatDuration(seconds, labels));
859
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
860
+ "li",
861
+ {
862
+ "data-slot": "course-syllabus-module",
863
+ className: "rounded-lg border border-border bg-card",
864
+ children: [
865
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
866
+ "button",
867
+ {
868
+ type: "button",
869
+ "aria-expanded": isOpen,
870
+ "aria-controls": panelId,
871
+ onClick: () => toggle(module2.id),
872
+ className: "flex w-full cursor-pointer items-center gap-3.5 rounded-lg px-4 py-3.5 text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
873
+ children: [
874
+ number,
875
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
876
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-semibold leading-snug", children: module2.title }),
877
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-sm text-muted-foreground", children: meta.join(SEPARATOR) })
878
+ ] }),
879
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
880
+ "span",
881
+ {
882
+ "aria-hidden": "true",
883
+ className: (0, import_utils8.cn)(
884
+ "shrink-0 text-muted-foreground transition-transform motion-reduce:transition-none",
885
+ isOpen && "rotate-180"
886
+ ),
887
+ children: "\u25BE"
888
+ }
889
+ )
890
+ ]
891
+ }
892
+ ),
893
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
894
+ "ol",
895
+ {
896
+ id: panelId,
897
+ hidden: !isOpen,
898
+ "data-slot": "course-syllabus-lessons",
899
+ className: "list-none space-y-2 border-t border-border px-4 py-3 pl-[3.75rem]",
900
+ children: lessons.map((lesson) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
901
+ "li",
902
+ {
903
+ "data-slot": "course-syllabus-lesson",
904
+ className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm",
905
+ children: [
906
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { "data-slot": "course-syllabus-lesson-title", className: "flex-1", children: lesson.title }),
907
+ lesson.isFreePreview && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "rounded-full bg-muted px-2 py-0.5 text-xs font-medium", children: labels.freePreview }),
908
+ typeof lesson.durationSeconds === "number" && lesson.durationSeconds > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-muted-foreground tabular-nums", children: formatDuration(lesson.durationSeconds, labels) })
909
+ ]
910
+ },
911
+ lesson.id
912
+ ))
913
+ }
914
+ )
915
+ ]
916
+ },
917
+ module2.id
918
+ );
919
+ }) })
920
+ ]
921
+ }
922
+ );
923
+ }
924
+
925
+ // src/components/data-request-form.tsx
926
+ var import_react6 = require("react");
927
+ var import_utils9 = require("@reblu/site-ui/utils");
533
928
  var import_site_contracts = require("@reblu/site-contracts");
534
- var import_jsx_runtime5 = require("react/jsx-runtime");
929
+ var import_jsx_runtime9 = require("react/jsx-runtime");
535
930
  var DATA_REQUEST_FIELD_LIMITS = Object.freeze({ name: 200, email: 320, details: 5e3 });
536
931
  var DEFAULT_DATA_REQUEST_FORM_LABELS = {
537
932
  requestTypeLabel: "Derecho que quieres ejercer",
@@ -557,7 +952,7 @@ var DEFAULT_DATA_REQUEST_FORM_LABELS = {
557
952
  nameRequired: "Indica tu nombre.",
558
953
  emailInvalid: "Introduce un email v\xE1lido.",
559
954
  consentRequired: "Debes aceptar el tratamiento de tus datos para gestionar la solicitud.",
560
- success: (deadline) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
955
+ success: (deadline) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
561
956
  "Hemos recibido tu solicitud. Te responderemos como m\xE1ximo el ",
562
957
  deadline,
563
958
  "."
@@ -606,18 +1001,18 @@ function DataRequestForm({
606
1001
  ...labels,
607
1002
  requestTypes: { ...DEFAULT_DATA_REQUEST_FORM_LABELS.requestTypes, ...labels?.requestTypes }
608
1003
  };
609
- const id = (0, import_react4.useId)();
610
- const [requestType, setRequestType] = (0, import_react4.useState)("");
611
- const [name, setName] = (0, import_react4.useState)("");
612
- const [email, setEmail] = (0, import_react4.useState)("");
613
- const [details, setDetails] = (0, import_react4.useState)("");
614
- const [consent, setConsent] = (0, import_react4.useState)(false);
615
- const [errors, setErrors] = (0, import_react4.useState)({});
616
- const [formError, setFormError] = (0, import_react4.useState)(null);
617
- const [submitting, setSubmitting] = (0, import_react4.useState)(false);
618
- const [ack, setAck] = (0, import_react4.useState)(null);
619
- const statusRef = (0, import_react4.useRef)(null);
620
- (0, import_react4.useEffect)(() => {
1004
+ const id = (0, import_react6.useId)();
1005
+ const [requestType, setRequestType] = (0, import_react6.useState)("");
1006
+ const [name, setName] = (0, import_react6.useState)("");
1007
+ const [email, setEmail] = (0, import_react6.useState)("");
1008
+ const [details, setDetails] = (0, import_react6.useState)("");
1009
+ const [consent, setConsent] = (0, import_react6.useState)(false);
1010
+ const [errors, setErrors] = (0, import_react6.useState)({});
1011
+ const [formError, setFormError] = (0, import_react6.useState)(null);
1012
+ const [submitting, setSubmitting] = (0, import_react6.useState)(false);
1013
+ const [ack, setAck] = (0, import_react6.useState)(null);
1014
+ const statusRef = (0, import_react6.useRef)(null);
1015
+ (0, import_react6.useEffect)(() => {
621
1016
  if (ack) statusRef.current?.focus();
622
1017
  }, [ack]);
623
1018
  async function handleSubmit(event) {
@@ -668,21 +1063,21 @@ function DataRequestForm({
668
1063
  dateStyle: "long",
669
1064
  timeStyle: "short"
670
1065
  }).format(new Date(ack.slaDueAt));
671
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1066
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
672
1067
  "div",
673
1068
  {
674
1069
  ref: statusRef,
675
1070
  tabIndex: -1,
676
1071
  role: "status",
677
1072
  "data-slot": "data-request-form-success",
678
- className: (0, import_utils5.cn)("rounded-lg border border-border p-6 text-sm", className),
1073
+ className: (0, import_utils9.cn)("rounded-lg border border-border p-6 text-sm", className),
679
1074
  children: t.success(deadline)
680
1075
  }
681
1076
  );
682
1077
  }
683
1078
  const errorId = (field) => errors[field] ? `${id}-${field}-error` : void 0;
684
- const fieldError = (field) => errors[field] ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { id: `${id}-${field}-error`, className: "text-xs text-destructive", children: errors[field] }) : null;
685
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1079
+ const fieldError = (field) => errors[field] ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { id: `${id}-${field}-error`, className: "text-xs text-destructive", children: errors[field] }) : null;
1080
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
686
1081
  "form",
687
1082
  {
688
1083
  noValidate: true,
@@ -690,11 +1085,11 @@ function DataRequestForm({
690
1085
  onFocusCapture: onInteract,
691
1086
  onPointerDown: onInteract,
692
1087
  "data-slot": "data-request-form",
693
- className: (0, import_utils5.cn)("space-y-4", className),
1088
+ className: (0, import_utils9.cn)("space-y-4", className),
694
1089
  children: [
695
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
696
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { htmlFor: `${id}-type`, className: "text-sm font-medium", children: t.requestTypeLabel }),
697
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1090
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-1", children: [
1091
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { htmlFor: `${id}-type`, className: "text-sm font-medium", children: t.requestTypeLabel }),
1092
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
698
1093
  "select",
699
1094
  {
700
1095
  id: `${id}-type`,
@@ -702,18 +1097,18 @@ function DataRequestForm({
702
1097
  onChange: (e) => setRequestType(e.target.value),
703
1098
  "aria-invalid": errors.requestType ? true : void 0,
704
1099
  "aria-describedby": errorId("requestType"),
705
- className: (0, import_utils5.cn)(FIELD_CLASS, errors.requestType ? "border-destructive" : "border-border"),
1100
+ className: (0, import_utils9.cn)(FIELD_CLASS, errors.requestType ? "border-destructive" : "border-border"),
706
1101
  children: [
707
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: t.requestTypePlaceholder }),
708
- requestTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, children: t.requestTypes[type] }, type))
1102
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: "", children: t.requestTypePlaceholder }),
1103
+ requestTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: type, children: t.requestTypes[type] }, type))
709
1104
  ]
710
1105
  }
711
1106
  ),
712
1107
  fieldError("requestType")
713
1108
  ] }),
714
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
715
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { htmlFor: `${id}-name`, className: "text-sm font-medium", children: t.nameLabel }),
716
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1109
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-1", children: [
1110
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { htmlFor: `${id}-name`, className: "text-sm font-medium", children: t.nameLabel }),
1111
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
717
1112
  "input",
718
1113
  {
719
1114
  id: `${id}-name`,
@@ -724,14 +1119,14 @@ function DataRequestForm({
724
1119
  onChange: (e) => setName(e.target.value),
725
1120
  "aria-invalid": errors.name ? true : void 0,
726
1121
  "aria-describedby": errorId("name"),
727
- className: (0, import_utils5.cn)(FIELD_CLASS, errors.name ? "border-destructive" : "border-border")
1122
+ className: (0, import_utils9.cn)(FIELD_CLASS, errors.name ? "border-destructive" : "border-border")
728
1123
  }
729
1124
  ),
730
1125
  fieldError("name")
731
1126
  ] }),
732
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
733
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { htmlFor: `${id}-email`, className: "text-sm font-medium", children: t.emailLabel }),
734
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1127
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-1", children: [
1128
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { htmlFor: `${id}-email`, className: "text-sm font-medium", children: t.emailLabel }),
1129
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
735
1130
  "input",
736
1131
  {
737
1132
  id: `${id}-email`,
@@ -742,14 +1137,14 @@ function DataRequestForm({
742
1137
  onChange: (e) => setEmail(e.target.value),
743
1138
  "aria-invalid": errors.email ? true : void 0,
744
1139
  "aria-describedby": errorId("email"),
745
- className: (0, import_utils5.cn)(FIELD_CLASS, errors.email ? "border-destructive" : "border-border")
1140
+ className: (0, import_utils9.cn)(FIELD_CLASS, errors.email ? "border-destructive" : "border-border")
746
1141
  }
747
1142
  ),
748
1143
  fieldError("email")
749
1144
  ] }),
750
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
751
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { htmlFor: `${id}-details`, className: "text-sm font-medium", children: t.detailsLabel }),
752
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1145
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-1", children: [
1146
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { htmlFor: `${id}-details`, className: "text-sm font-medium", children: t.detailsLabel }),
1147
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
753
1148
  "textarea",
754
1149
  {
755
1150
  id: `${id}-details`,
@@ -758,13 +1153,13 @@ function DataRequestForm({
758
1153
  value: details,
759
1154
  onChange: (e) => setDetails(e.target.value),
760
1155
  placeholder: t.detailsPlaceholder,
761
- className: (0, import_utils5.cn)(FIELD_CLASS, "border-border")
1156
+ className: (0, import_utils9.cn)(FIELD_CLASS, "border-border")
762
1157
  }
763
1158
  )
764
1159
  ] }),
765
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
766
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-start gap-2", children: [
767
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1160
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-1", children: [
1161
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-start gap-2", children: [
1162
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
768
1163
  "input",
769
1164
  {
770
1165
  id: `${id}-consent`,
@@ -776,17 +1171,17 @@ function DataRequestForm({
776
1171
  className: "mt-1"
777
1172
  }
778
1173
  ),
779
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { htmlFor: `${id}-consent`, className: "text-sm text-foreground/80", children: t.consent })
1174
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { htmlFor: `${id}-consent`, className: "text-sm text-foreground/80", children: t.consent })
780
1175
  ] }),
781
1176
  fieldError("consent")
782
1177
  ] }),
783
- formError && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { role: "alert", className: "text-sm text-destructive", children: formError }),
784
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1178
+ formError && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "alert", className: "text-sm text-destructive", children: formError }),
1179
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
785
1180
  "button",
786
1181
  {
787
1182
  type: "submit",
788
1183
  disabled: submitting,
789
- className: "rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground disabled:opacity-60",
1184
+ className: "cursor-pointer rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground disabled:cursor-not-allowed disabled:opacity-60",
790
1185
  children: submitting ? t.submitting : t.submit
791
1186
  }
792
1187
  )
@@ -799,10 +1194,14 @@ function DataRequestForm({
799
1194
  AddToCartButton,
800
1195
  CartIcon,
801
1196
  Collection,
1197
+ CourseSyllabus,
802
1198
  DATA_REQUEST_FIELD_LIMITS,
803
1199
  DATA_REQUEST_TYPES,
1200
+ DEFAULT_COURSE_SYLLABUS_LABELS,
804
1201
  DEFAULT_DATA_REQUEST_FORM_LABELS,
1202
+ DEFAULT_PRODUCT_CAROUSEL_LABELS,
805
1203
  DataRequestForm,
1204
+ ProductCarousel,
806
1205
  ShareButtons,
807
1206
  useCollection
808
1207
  });