@reblu/site-blocks 0.20.0 → 0.22.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
@@ -763,42 +763,104 @@ var DEFAULT_COURSE_SYLLABUS_LABELS = {
763
763
  lessonPlural: "lecciones",
764
764
  freePreview: "Vista previa gratuita",
765
765
  hours: "h",
766
- minutes: "min"
766
+ minutes: "min",
767
+ expandAll: "Expandir todo",
768
+ collapseAll: "Contraer todo",
769
+ videoLesson: "V\xEDdeo",
770
+ textLesson: "Lectura"
767
771
  };
768
772
  var SEPARATOR = " \xB7 ";
769
773
  function count(n, singular, plural) {
770
774
  return `${n} ${n === 1 ? singular : plural}`;
771
775
  }
772
- function formatDuration(seconds, labels) {
776
+ function formatTotal(seconds, labels) {
773
777
  const totalMinutes = Math.max(1, Math.round(seconds / 60));
774
778
  const hours = Math.floor(totalMinutes / 60);
775
779
  const minutes = totalMinutes % 60;
776
780
  if (hours === 0) return `${minutes} ${labels.minutes}`;
777
781
  return minutes === 0 ? `${hours} ${labels.hours}` : `${hours} ${labels.hours} ${minutes} ${labels.minutes}`;
778
782
  }
779
- function moduleSeconds(lessons) {
783
+ function splitSeconds(seconds) {
784
+ const whole = Math.round(seconds);
785
+ return [Math.floor(whole / 3600), Math.floor(whole % 3600 / 60), whole % 60];
786
+ }
787
+ function formatClock(seconds) {
788
+ const [h, m, s] = splitSeconds(seconds);
789
+ const ss = String(s).padStart(2, "0");
790
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
791
+ }
792
+ function isoDuration(seconds) {
793
+ const [h, m, s] = splitSeconds(seconds);
794
+ return `PT${h > 0 ? `${h}H` : ""}${m}M${s}S`;
795
+ }
796
+ function hasDuration(lesson) {
797
+ return typeof lesson.durationSeconds === "number" && lesson.durationSeconds > 0;
798
+ }
799
+ function totalSeconds(lessons) {
800
+ if (lessons.length === 0) return null;
780
801
  let total = 0;
781
802
  for (const l of lessons) {
782
- if (typeof l.durationSeconds !== "number" || l.durationSeconds <= 0) return null;
803
+ if (!hasDuration(l)) return null;
783
804
  total += l.durationSeconds;
784
805
  }
785
806
  return total;
786
807
  }
808
+ function fillTemplate(template, lesson) {
809
+ return template.replaceAll("{lessonSlug}", encodeURIComponent(lesson.slug)).replaceAll("{lessonId}", encodeURIComponent(lesson.id));
810
+ }
811
+ var ICON_PROPS = {
812
+ "aria-hidden": true,
813
+ viewBox: "0 0 24 24",
814
+ fill: "none",
815
+ stroke: "currentColor",
816
+ strokeWidth: 2,
817
+ strokeLinecap: "round",
818
+ strokeLinejoin: "round"
819
+ };
820
+ function LessonIcon({ type }) {
821
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
822
+ "svg",
823
+ {
824
+ ...ICON_PROPS,
825
+ "data-slot": "course-syllabus-lesson-icon",
826
+ "data-type": type,
827
+ className: "size-4 shrink-0 text-muted-foreground",
828
+ children: type === "video" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
829
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "12", cy: "12", r: "9" }),
830
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "m10 8.5 5 3.5-5 3.5z" })
831
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
832
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }),
833
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M14 3v5h5M9 13h6M9 17h4" })
834
+ ] })
835
+ }
836
+ );
837
+ }
838
+ function PlayIcon() {
839
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("svg", { ...ICON_PROPS, className: "size-3.5 shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "m7 4 13 8-13 8z", fill: "currentColor" }) });
840
+ }
841
+ var PREVIEW_CTA = "inline-flex items-center gap-1.5 rounded-full border border-primary/40 bg-primary/10 px-3 py-1 text-xs font-semibold text-primary transition-colors hover:bg-primary hover:text-primary-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring motion-reduce:transition-none";
787
842
  function CourseSyllabus({
788
843
  modules,
789
844
  labels: labelOverrides,
790
845
  headingLevel = 2,
791
846
  headingClassName,
792
- className
847
+ className,
848
+ getPreviewHref,
849
+ previewHrefTemplate,
850
+ onPreview
793
851
  }) {
794
852
  const labels = { ...DEFAULT_COURSE_SYLLABUS_LABELS, ...labelOverrides };
795
853
  const baseId = (0, import_react5.useId)();
796
854
  const [open, setOpen] = (0, import_react5.useState)(() => /* @__PURE__ */ new Set());
797
855
  if (modules.length === 0) return null;
798
- const totalLessons = modules.reduce((sum, m) => sum + (m.lessons?.length ?? 0), 0);
856
+ const expandable = modules.filter((m) => (m.lessons?.length ?? 0) > 0).map((m) => m.id);
857
+ const allLessons = modules.flatMap((m) => m.lessons ?? []);
858
+ const courseSeconds = totalSeconds(allLessons);
799
859
  const summary = [count(modules.length, labels.moduleSingular, labels.modulePlural)];
800
- if (totalLessons > 0)
801
- summary.push(count(totalLessons, labels.lessonSingular, labels.lessonPlural));
860
+ if (allLessons.length > 0)
861
+ summary.push(count(allLessons.length, labels.lessonSingular, labels.lessonPlural));
862
+ if (courseSeconds !== null) summary.push(formatTotal(courseSeconds, labels));
863
+ const allOpen = expandable.length > 0 && expandable.every((id) => open.has(id));
802
864
  const headingId = `${baseId}-heading`;
803
865
  const Tag = `h${headingLevel}`;
804
866
  function toggle(id) {
@@ -809,6 +871,44 @@ function CourseSyllabus({
809
871
  return next;
810
872
  });
811
873
  }
874
+ function previewHref(lesson) {
875
+ if (getPreviewHref) return getPreviewHref(lesson);
876
+ return previewHrefTemplate ? fillTemplate(previewHrefTemplate, lesson) : void 0;
877
+ }
878
+ function renderPreview(lesson) {
879
+ const name = `${labels.freePreview}: ${lesson.title}`;
880
+ const href = previewHref(lesson);
881
+ if (href) {
882
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("a", { href, "aria-label": name, "data-slot": "course-syllabus-preview", className: PREVIEW_CTA, children: [
883
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PlayIcon, {}),
884
+ labels.freePreview
885
+ ] });
886
+ }
887
+ if (onPreview) {
888
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
889
+ "button",
890
+ {
891
+ type: "button",
892
+ "aria-label": name,
893
+ "data-slot": "course-syllabus-preview",
894
+ onClick: () => onPreview(lesson),
895
+ className: (0, import_utils8.cn)(PREVIEW_CTA, "cursor-pointer"),
896
+ children: [
897
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PlayIcon, {}),
898
+ labels.freePreview
899
+ ]
900
+ }
901
+ );
902
+ }
903
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
904
+ "span",
905
+ {
906
+ "data-slot": "course-syllabus-preview",
907
+ className: "rounded-full bg-muted px-2.5 py-0.5 text-xs font-medium text-muted-foreground",
908
+ children: labels.freePreview
909
+ }
910
+ );
911
+ }
812
912
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
813
913
  "section",
814
914
  {
@@ -816,24 +916,43 @@ function CourseSyllabus({
816
916
  "aria-labelledby": headingId,
817
917
  className: (0, import_utils8.cn)("space-y-4", className),
818
918
  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) => {
919
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
920
+ "div",
921
+ {
922
+ "data-slot": "course-syllabus-header",
923
+ className: "flex flex-wrap items-end justify-between gap-x-4 gap-y-2",
924
+ children: [
925
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "space-y-1", children: [
926
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
927
+ Tag,
928
+ {
929
+ id: headingId,
930
+ className: slotClassName("", "text-xl font-semibold", headingClassName),
931
+ children: labels.heading
932
+ }
933
+ ),
934
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { "data-slot": "course-syllabus-summary", className: "text-sm text-muted-foreground", children: summary.join(SEPARATOR) })
935
+ ] }),
936
+ expandable.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
937
+ "button",
938
+ {
939
+ type: "button",
940
+ "data-slot": "course-syllabus-toggle-all",
941
+ onClick: () => setOpen(allOpen ? /* @__PURE__ */ new Set() : new Set(expandable)),
942
+ className: "cursor-pointer rounded-md px-2 py-1 text-sm font-semibold text-primary underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
943
+ children: allOpen ? labels.collapseAll : labels.expandAll
944
+ }
945
+ )
946
+ ]
947
+ }
948
+ ),
949
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ol", { className: "list-none divide-y divide-border overflow-hidden rounded-xl border border-border bg-card p-0", children: modules.map((module2, index) => {
831
950
  const lessons = module2.lessons ?? [];
832
951
  const number = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
833
952
  "span",
834
953
  {
835
954
  "aria-hidden": "true",
836
- className: "flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-sm font-semibold text-primary",
955
+ className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-semibold text-primary tabular-nums",
837
956
  children: index + 1
838
957
  }
839
958
  );
@@ -842,7 +961,7 @@ function CourseSyllabus({
842
961
  "li",
843
962
  {
844
963
  "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",
964
+ className: "flex items-center gap-4 px-4 py-4 sm:px-5",
846
965
  children: [
847
966
  number,
848
967
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-semibold leading-snug", children: module2.title })
@@ -853,69 +972,85 @@ function CourseSyllabus({
853
972
  }
854
973
  const isOpen = open.has(module2.id);
855
974
  const panelId = `${baseId}-module-${index}`;
856
- const seconds = moduleSeconds(lessons);
975
+ const seconds = totalSeconds(lessons);
857
976
  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
- }
977
+ if (seconds !== null) meta.push(formatTotal(seconds, labels));
978
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("li", { "data-slot": "course-syllabus-module", children: [
979
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
980
+ "button",
981
+ {
982
+ type: "button",
983
+ "aria-expanded": isOpen,
984
+ "aria-controls": panelId,
985
+ onClick: () => toggle(module2.id),
986
+ className: (0, import_utils8.cn)(
987
+ "flex w-full cursor-pointer items-center gap-4 px-4 py-4 text-left transition-colors hover:bg-muted/60 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring motion-reduce:transition-none sm:px-5",
988
+ isOpen && "bg-muted/40"
892
989
  ),
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
- );
990
+ children: [
991
+ number,
992
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
993
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-semibold leading-snug", children: module2.title }),
994
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-sm text-muted-foreground", children: meta.join(SEPARATOR) })
995
+ ] }),
996
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
997
+ "svg",
998
+ {
999
+ ...ICON_PROPS,
1000
+ "data-slot": "course-syllabus-chevron",
1001
+ className: (0, import_utils8.cn)(
1002
+ "size-5 shrink-0 text-muted-foreground transition-transform duration-200 motion-reduce:transition-none",
1003
+ isOpen && "rotate-180"
1004
+ ),
1005
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "m6 9 6 6 6-6" })
1006
+ }
1007
+ )
1008
+ ]
1009
+ }
1010
+ ),
1011
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1012
+ "ol",
1013
+ {
1014
+ id: panelId,
1015
+ hidden: !isOpen,
1016
+ "data-slot": "course-syllabus-lessons",
1017
+ className: "list-none border-t border-border p-0 py-1",
1018
+ children: lessons.map((lesson) => {
1019
+ const type = lesson.hasVideo ? "video" : "text";
1020
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1021
+ "li",
1022
+ {
1023
+ "data-slot": "course-syllabus-lesson",
1024
+ className: "flex flex-wrap items-center gap-x-3 gap-y-1.5 px-4 py-2.5 text-sm sm:pl-[4.25rem] sm:pr-5",
1025
+ children: [
1026
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LessonIcon, { type }),
1027
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "sr-only", children: type === "video" ? labels.videoLesson : labels.textLesson }),
1028
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1029
+ "span",
1030
+ {
1031
+ "data-slot": "course-syllabus-lesson-title",
1032
+ className: "min-w-0 flex-1 leading-snug",
1033
+ children: lesson.title
1034
+ }
1035
+ ),
1036
+ lesson.isFreePreview && renderPreview(lesson),
1037
+ hasDuration(lesson) && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1038
+ "time",
1039
+ {
1040
+ "data-slot": "course-syllabus-lesson-duration",
1041
+ dateTime: isoDuration(lesson.durationSeconds),
1042
+ className: "ml-auto shrink-0 text-muted-foreground tabular-nums",
1043
+ children: formatClock(lesson.durationSeconds)
1044
+ }
1045
+ )
1046
+ ]
1047
+ },
1048
+ lesson.id
1049
+ );
1050
+ })
1051
+ }
1052
+ )
1053
+ ] }, module2.id);
919
1054
  }) })
920
1055
  ]
921
1056
  }