@reblu/site-blocks 0.20.0 → 0.21.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.d.cts CHANGED
@@ -307,21 +307,29 @@ declare function ProductCarousel<T extends {
307
307
 
308
308
  /**
309
309
  * `CourseSyllabus` — the drop-in programme of a public course page, over
310
- * `AcademyCourseDetail.modules` (`client.academy.bySlug()`), REB-1134.
310
+ * `AcademyCourseDetail.modules` (`client.academy.bySlug()`), REB-1134; redesigned
311
+ * in REB-1135.
311
312
  *
312
- * - The header reads the heading, then «N módulos · M lecciones».
313
+ * - The header reads the heading, then «N módulos · M lecciones · total», and
314
+ * carries an «Expandir todo / Contraer todo» control.
313
315
  * - Each module with lessons is a `<button>` with `aria-expanded` and
314
316
  * `aria-controls`, showing its lesson count and, when every lesson has one,
315
- * the module's total duration. Expanding it lists the lessons in the order
316
- * the API returns them, with their duration and the free-preview flag.
317
+ * the module's total duration, with a chevron that rotates when open.
318
+ * - Each lesson row shows a type icon (video / reading, named for screen
319
+ * readers), its title, its duration as «m:ss» / «h:mm:ss», and — on a
320
+ * free-preview lesson — «Vista previa gratuita» as a call to action:
321
+ * a link (`getPreviewHref` / `previewHrefTemplate`), a button (`onPreview`),
322
+ * or a plain label when the SITE wires neither.
317
323
  * - A module without lessons is a plain row (title only), never a button.
318
324
  *
319
- * It renders only what the API serves. The API returns published lessons only
320
- * and no video, playback or attachment data, so nothing here can leak it.
325
+ * It renders only what the API serves: published lessons, no video, playback
326
+ * or attachment data. The preview itself is fetched by the page it links to
327
+ * (`getLessonPreview` in `@reblu/site-next/academy-server`).
321
328
  *
322
- * Client Component: it holds the open/closed state of each module. Every prop
323
- * is serialisable (copy is plain strings), so a Server Component page can
324
- * render it directly.
329
+ * Client Component: it holds the open/closed state. Copy and
330
+ * `previewHrefTemplate` are serialisable, so a Server Component page can render
331
+ * it directly; `getPreviewHref` / `onPreview` are functions, so they need a
332
+ * Client Component parent.
325
333
  */
326
334
  interface CourseSyllabusLabels {
327
335
  heading: string;
@@ -329,12 +337,18 @@ interface CourseSyllabusLabels {
329
337
  modulePlural: string;
330
338
  lessonSingular: string;
331
339
  lessonPlural: string;
332
- /** Badge on a lesson with `isFreePreview`. */
340
+ /** Call to action (or label) on a lesson with `isFreePreview`. */
333
341
  freePreview: string;
334
- /** Hours unit in durations («1 h 15 min»). */
342
+ /** Hours unit in module/course totals («1 h 15 min»). */
335
343
  hours: string;
336
- /** Minutes unit in durations. */
344
+ /** Minutes unit in module/course totals. */
337
345
  minutes: string;
346
+ expandAll: string;
347
+ collapseAll: string;
348
+ /** Screen-reader name of the video lesson icon. */
349
+ videoLesson: string;
350
+ /** Screen-reader name of the text (reading) lesson icon. */
351
+ textLesson: string;
338
352
  }
339
353
  declare const DEFAULT_COURSE_SYLLABUS_LABELS: CourseSyllabusLabels;
340
354
  /** A module as served. `lessons` is optional so an older payload still renders. */
@@ -351,8 +365,21 @@ interface CourseSyllabusProps {
351
365
  /** Replaces the heading's default typography (see the className rule in the README). */
352
366
  headingClassName?: string;
353
367
  className?: string;
368
+ /**
369
+ * Link target of a free-preview lesson, e.g. `/cursos/${slug}/preview/${l.slug}`.
370
+ * Called only for lessons with `isFreePreview`. Return `undefined` to keep the label.
371
+ */
372
+ getPreviewHref?: (lesson: AcademyLessonPublic) => string | undefined;
373
+ /**
374
+ * Serialisable alternative to `getPreviewHref` for Server Component pages:
375
+ * `{lessonSlug}` and `{lessonId}` are replaced (URL-encoded). Ignored when
376
+ * `getPreviewHref` is given.
377
+ */
378
+ previewHrefTemplate?: string;
379
+ /** Renders the call to action as a `<button>` (e.g. to open a modal). Used when no href resolves. */
380
+ onPreview?: (lesson: AcademyLessonPublic) => void;
354
381
  }
355
- declare function CourseSyllabus({ modules, labels: labelOverrides, headingLevel, headingClassName, className, }: CourseSyllabusProps): react_jsx_runtime.JSX.Element | null;
382
+ declare function CourseSyllabus({ modules, labels: labelOverrides, headingLevel, headingClassName, className, getPreviewHref, previewHrefTemplate, onPreview, }: CourseSyllabusProps): react_jsx_runtime.JSX.Element | null;
356
383
 
357
384
  /**
358
385
  * `DataRequestForm` — drop-in public GDPR rights form (REB-980) over
package/dist/client.d.ts CHANGED
@@ -307,21 +307,29 @@ declare function ProductCarousel<T extends {
307
307
 
308
308
  /**
309
309
  * `CourseSyllabus` — the drop-in programme of a public course page, over
310
- * `AcademyCourseDetail.modules` (`client.academy.bySlug()`), REB-1134.
310
+ * `AcademyCourseDetail.modules` (`client.academy.bySlug()`), REB-1134; redesigned
311
+ * in REB-1135.
311
312
  *
312
- * - The header reads the heading, then «N módulos · M lecciones».
313
+ * - The header reads the heading, then «N módulos · M lecciones · total», and
314
+ * carries an «Expandir todo / Contraer todo» control.
313
315
  * - Each module with lessons is a `<button>` with `aria-expanded` and
314
316
  * `aria-controls`, showing its lesson count and, when every lesson has one,
315
- * the module's total duration. Expanding it lists the lessons in the order
316
- * the API returns them, with their duration and the free-preview flag.
317
+ * the module's total duration, with a chevron that rotates when open.
318
+ * - Each lesson row shows a type icon (video / reading, named for screen
319
+ * readers), its title, its duration as «m:ss» / «h:mm:ss», and — on a
320
+ * free-preview lesson — «Vista previa gratuita» as a call to action:
321
+ * a link (`getPreviewHref` / `previewHrefTemplate`), a button (`onPreview`),
322
+ * or a plain label when the SITE wires neither.
317
323
  * - A module without lessons is a plain row (title only), never a button.
318
324
  *
319
- * It renders only what the API serves. The API returns published lessons only
320
- * and no video, playback or attachment data, so nothing here can leak it.
325
+ * It renders only what the API serves: published lessons, no video, playback
326
+ * or attachment data. The preview itself is fetched by the page it links to
327
+ * (`getLessonPreview` in `@reblu/site-next/academy-server`).
321
328
  *
322
- * Client Component: it holds the open/closed state of each module. Every prop
323
- * is serialisable (copy is plain strings), so a Server Component page can
324
- * render it directly.
329
+ * Client Component: it holds the open/closed state. Copy and
330
+ * `previewHrefTemplate` are serialisable, so a Server Component page can render
331
+ * it directly; `getPreviewHref` / `onPreview` are functions, so they need a
332
+ * Client Component parent.
325
333
  */
326
334
  interface CourseSyllabusLabels {
327
335
  heading: string;
@@ -329,12 +337,18 @@ interface CourseSyllabusLabels {
329
337
  modulePlural: string;
330
338
  lessonSingular: string;
331
339
  lessonPlural: string;
332
- /** Badge on a lesson with `isFreePreview`. */
340
+ /** Call to action (or label) on a lesson with `isFreePreview`. */
333
341
  freePreview: string;
334
- /** Hours unit in durations («1 h 15 min»). */
342
+ /** Hours unit in module/course totals («1 h 15 min»). */
335
343
  hours: string;
336
- /** Minutes unit in durations. */
344
+ /** Minutes unit in module/course totals. */
337
345
  minutes: string;
346
+ expandAll: string;
347
+ collapseAll: string;
348
+ /** Screen-reader name of the video lesson icon. */
349
+ videoLesson: string;
350
+ /** Screen-reader name of the text (reading) lesson icon. */
351
+ textLesson: string;
338
352
  }
339
353
  declare const DEFAULT_COURSE_SYLLABUS_LABELS: CourseSyllabusLabels;
340
354
  /** A module as served. `lessons` is optional so an older payload still renders. */
@@ -351,8 +365,21 @@ interface CourseSyllabusProps {
351
365
  /** Replaces the heading's default typography (see the className rule in the README). */
352
366
  headingClassName?: string;
353
367
  className?: string;
368
+ /**
369
+ * Link target of a free-preview lesson, e.g. `/cursos/${slug}/preview/${l.slug}`.
370
+ * Called only for lessons with `isFreePreview`. Return `undefined` to keep the label.
371
+ */
372
+ getPreviewHref?: (lesson: AcademyLessonPublic) => string | undefined;
373
+ /**
374
+ * Serialisable alternative to `getPreviewHref` for Server Component pages:
375
+ * `{lessonSlug}` and `{lessonId}` are replaced (URL-encoded). Ignored when
376
+ * `getPreviewHref` is given.
377
+ */
378
+ previewHrefTemplate?: string;
379
+ /** Renders the call to action as a `<button>` (e.g. to open a modal). Used when no href resolves. */
380
+ onPreview?: (lesson: AcademyLessonPublic) => void;
354
381
  }
355
- declare function CourseSyllabus({ modules, labels: labelOverrides, headingLevel, headingClassName, className, }: CourseSyllabusProps): react_jsx_runtime.JSX.Element | null;
382
+ declare function CourseSyllabus({ modules, labels: labelOverrides, headingLevel, headingClassName, className, getPreviewHref, previewHrefTemplate, onPreview, }: CourseSyllabusProps): react_jsx_runtime.JSX.Element | null;
356
383
 
357
384
  /**
358
385
  * `DataRequestForm` — drop-in public GDPR rights form (REB-980) over
package/dist/client.js CHANGED
@@ -707,7 +707,7 @@ function slotClassName(structural, defaults, consumer) {
707
707
  }
708
708
 
709
709
  // src/components/course-syllabus.tsx
710
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
710
+ import { Fragment, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
711
711
  var DEFAULT_COURSE_SYLLABUS_LABELS = {
712
712
  heading: "Programa",
713
713
  moduleSingular: "m\xF3dulo",
@@ -716,42 +716,104 @@ var DEFAULT_COURSE_SYLLABUS_LABELS = {
716
716
  lessonPlural: "lecciones",
717
717
  freePreview: "Vista previa gratuita",
718
718
  hours: "h",
719
- minutes: "min"
719
+ minutes: "min",
720
+ expandAll: "Expandir todo",
721
+ collapseAll: "Contraer todo",
722
+ videoLesson: "V\xEDdeo",
723
+ textLesson: "Lectura"
720
724
  };
721
725
  var SEPARATOR = " \xB7 ";
722
726
  function count(n, singular, plural) {
723
727
  return `${n} ${n === 1 ? singular : plural}`;
724
728
  }
725
- function formatDuration(seconds, labels) {
729
+ function formatTotal(seconds, labels) {
726
730
  const totalMinutes = Math.max(1, Math.round(seconds / 60));
727
731
  const hours = Math.floor(totalMinutes / 60);
728
732
  const minutes = totalMinutes % 60;
729
733
  if (hours === 0) return `${minutes} ${labels.minutes}`;
730
734
  return minutes === 0 ? `${hours} ${labels.hours}` : `${hours} ${labels.hours} ${minutes} ${labels.minutes}`;
731
735
  }
732
- function moduleSeconds(lessons) {
736
+ function splitSeconds(seconds) {
737
+ const whole = Math.round(seconds);
738
+ return [Math.floor(whole / 3600), Math.floor(whole % 3600 / 60), whole % 60];
739
+ }
740
+ function formatClock(seconds) {
741
+ const [h, m, s] = splitSeconds(seconds);
742
+ const ss = String(s).padStart(2, "0");
743
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
744
+ }
745
+ function isoDuration(seconds) {
746
+ const [h, m, s] = splitSeconds(seconds);
747
+ return `PT${h > 0 ? `${h}H` : ""}${m}M${s}S`;
748
+ }
749
+ function hasDuration(lesson) {
750
+ return typeof lesson.durationSeconds === "number" && lesson.durationSeconds > 0;
751
+ }
752
+ function totalSeconds(lessons) {
753
+ if (lessons.length === 0) return null;
733
754
  let total = 0;
734
755
  for (const l of lessons) {
735
- if (typeof l.durationSeconds !== "number" || l.durationSeconds <= 0) return null;
756
+ if (!hasDuration(l)) return null;
736
757
  total += l.durationSeconds;
737
758
  }
738
759
  return total;
739
760
  }
761
+ function fillTemplate(template, lesson) {
762
+ return template.replaceAll("{lessonSlug}", encodeURIComponent(lesson.slug)).replaceAll("{lessonId}", encodeURIComponent(lesson.id));
763
+ }
764
+ var ICON_PROPS = {
765
+ "aria-hidden": true,
766
+ viewBox: "0 0 24 24",
767
+ fill: "none",
768
+ stroke: "currentColor",
769
+ strokeWidth: 2,
770
+ strokeLinecap: "round",
771
+ strokeLinejoin: "round"
772
+ };
773
+ function LessonIcon({ type }) {
774
+ return /* @__PURE__ */ jsx8(
775
+ "svg",
776
+ {
777
+ ...ICON_PROPS,
778
+ "data-slot": "course-syllabus-lesson-icon",
779
+ "data-type": type,
780
+ className: "size-4 shrink-0 text-muted-foreground",
781
+ children: type === "video" ? /* @__PURE__ */ jsxs6(Fragment, { children: [
782
+ /* @__PURE__ */ jsx8("circle", { cx: "12", cy: "12", r: "9" }),
783
+ /* @__PURE__ */ jsx8("path", { d: "m10 8.5 5 3.5-5 3.5z" })
784
+ ] }) : /* @__PURE__ */ jsxs6(Fragment, { children: [
785
+ /* @__PURE__ */ jsx8("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }),
786
+ /* @__PURE__ */ jsx8("path", { d: "M14 3v5h5M9 13h6M9 17h4" })
787
+ ] })
788
+ }
789
+ );
790
+ }
791
+ function PlayIcon() {
792
+ return /* @__PURE__ */ jsx8("svg", { ...ICON_PROPS, className: "size-3.5 shrink-0", children: /* @__PURE__ */ jsx8("path", { d: "m7 4 13 8-13 8z", fill: "currentColor" }) });
793
+ }
794
+ 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";
740
795
  function CourseSyllabus({
741
796
  modules,
742
797
  labels: labelOverrides,
743
798
  headingLevel = 2,
744
799
  headingClassName,
745
- className
800
+ className,
801
+ getPreviewHref,
802
+ previewHrefTemplate,
803
+ onPreview
746
804
  }) {
747
805
  const labels = { ...DEFAULT_COURSE_SYLLABUS_LABELS, ...labelOverrides };
748
806
  const baseId = useId2();
749
807
  const [open, setOpen] = useState6(() => /* @__PURE__ */ new Set());
750
808
  if (modules.length === 0) return null;
751
- const totalLessons = modules.reduce((sum, m) => sum + (m.lessons?.length ?? 0), 0);
809
+ const expandable = modules.filter((m) => (m.lessons?.length ?? 0) > 0).map((m) => m.id);
810
+ const allLessons = modules.flatMap((m) => m.lessons ?? []);
811
+ const courseSeconds = totalSeconds(allLessons);
752
812
  const summary = [count(modules.length, labels.moduleSingular, labels.modulePlural)];
753
- if (totalLessons > 0)
754
- summary.push(count(totalLessons, labels.lessonSingular, labels.lessonPlural));
813
+ if (allLessons.length > 0)
814
+ summary.push(count(allLessons.length, labels.lessonSingular, labels.lessonPlural));
815
+ if (courseSeconds !== null) summary.push(formatTotal(courseSeconds, labels));
816
+ const allOpen = expandable.length > 0 && expandable.every((id) => open.has(id));
755
817
  const headingId = `${baseId}-heading`;
756
818
  const Tag = `h${headingLevel}`;
757
819
  function toggle(id) {
@@ -762,6 +824,44 @@ function CourseSyllabus({
762
824
  return next;
763
825
  });
764
826
  }
827
+ function previewHref(lesson) {
828
+ if (getPreviewHref) return getPreviewHref(lesson);
829
+ return previewHrefTemplate ? fillTemplate(previewHrefTemplate, lesson) : void 0;
830
+ }
831
+ function renderPreview(lesson) {
832
+ const name = `${labels.freePreview}: ${lesson.title}`;
833
+ const href = previewHref(lesson);
834
+ if (href) {
835
+ return /* @__PURE__ */ jsxs6("a", { href, "aria-label": name, "data-slot": "course-syllabus-preview", className: PREVIEW_CTA, children: [
836
+ /* @__PURE__ */ jsx8(PlayIcon, {}),
837
+ labels.freePreview
838
+ ] });
839
+ }
840
+ if (onPreview) {
841
+ return /* @__PURE__ */ jsxs6(
842
+ "button",
843
+ {
844
+ type: "button",
845
+ "aria-label": name,
846
+ "data-slot": "course-syllabus-preview",
847
+ onClick: () => onPreview(lesson),
848
+ className: cn8(PREVIEW_CTA, "cursor-pointer"),
849
+ children: [
850
+ /* @__PURE__ */ jsx8(PlayIcon, {}),
851
+ labels.freePreview
852
+ ]
853
+ }
854
+ );
855
+ }
856
+ return /* @__PURE__ */ jsx8(
857
+ "span",
858
+ {
859
+ "data-slot": "course-syllabus-preview",
860
+ className: "rounded-full bg-muted px-2.5 py-0.5 text-xs font-medium text-muted-foreground",
861
+ children: labels.freePreview
862
+ }
863
+ );
864
+ }
765
865
  return /* @__PURE__ */ jsxs6(
766
866
  "section",
767
867
  {
@@ -769,24 +869,43 @@ function CourseSyllabus({
769
869
  "aria-labelledby": headingId,
770
870
  className: cn8("space-y-4", className),
771
871
  children: [
772
- /* @__PURE__ */ jsxs6("div", { className: "space-y-1", children: [
773
- /* @__PURE__ */ jsx8(
774
- Tag,
775
- {
776
- id: headingId,
777
- className: slotClassName("", "text-xl font-semibold", headingClassName),
778
- children: labels.heading
779
- }
780
- ),
781
- /* @__PURE__ */ jsx8("p", { "data-slot": "course-syllabus-summary", className: "text-sm text-muted-foreground", children: summary.join(SEPARATOR) })
782
- ] }),
783
- /* @__PURE__ */ jsx8("ol", { className: "flex list-none flex-col gap-2.5 p-0", children: modules.map((module, index) => {
872
+ /* @__PURE__ */ jsxs6(
873
+ "div",
874
+ {
875
+ "data-slot": "course-syllabus-header",
876
+ className: "flex flex-wrap items-end justify-between gap-x-4 gap-y-2",
877
+ children: [
878
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-1", children: [
879
+ /* @__PURE__ */ jsx8(
880
+ Tag,
881
+ {
882
+ id: headingId,
883
+ className: slotClassName("", "text-xl font-semibold", headingClassName),
884
+ children: labels.heading
885
+ }
886
+ ),
887
+ /* @__PURE__ */ jsx8("p", { "data-slot": "course-syllabus-summary", className: "text-sm text-muted-foreground", children: summary.join(SEPARATOR) })
888
+ ] }),
889
+ expandable.length > 0 && /* @__PURE__ */ jsx8(
890
+ "button",
891
+ {
892
+ type: "button",
893
+ "data-slot": "course-syllabus-toggle-all",
894
+ onClick: () => setOpen(allOpen ? /* @__PURE__ */ new Set() : new Set(expandable)),
895
+ 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",
896
+ children: allOpen ? labels.collapseAll : labels.expandAll
897
+ }
898
+ )
899
+ ]
900
+ }
901
+ ),
902
+ /* @__PURE__ */ jsx8("ol", { className: "list-none divide-y divide-border overflow-hidden rounded-xl border border-border bg-card p-0", children: modules.map((module, index) => {
784
903
  const lessons = module.lessons ?? [];
785
904
  const number = /* @__PURE__ */ jsx8(
786
905
  "span",
787
906
  {
788
907
  "aria-hidden": "true",
789
- className: "flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-sm font-semibold text-primary",
908
+ className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-semibold text-primary tabular-nums",
790
909
  children: index + 1
791
910
  }
792
911
  );
@@ -795,7 +914,7 @@ function CourseSyllabus({
795
914
  "li",
796
915
  {
797
916
  "data-slot": "course-syllabus-module",
798
- className: "flex items-center gap-3.5 rounded-lg border border-border bg-card px-4 py-3.5",
917
+ className: "flex items-center gap-4 px-4 py-4 sm:px-5",
799
918
  children: [
800
919
  number,
801
920
  /* @__PURE__ */ jsx8("span", { className: "font-semibold leading-snug", children: module.title })
@@ -806,69 +925,85 @@ function CourseSyllabus({
806
925
  }
807
926
  const isOpen = open.has(module.id);
808
927
  const panelId = `${baseId}-module-${index}`;
809
- const seconds = moduleSeconds(lessons);
928
+ const seconds = totalSeconds(lessons);
810
929
  const meta = [count(lessons.length, labels.lessonSingular, labels.lessonPlural)];
811
- if (seconds !== null) meta.push(formatDuration(seconds, labels));
812
- return /* @__PURE__ */ jsxs6(
813
- "li",
814
- {
815
- "data-slot": "course-syllabus-module",
816
- className: "rounded-lg border border-border bg-card",
817
- children: [
818
- /* @__PURE__ */ jsxs6(
819
- "button",
820
- {
821
- type: "button",
822
- "aria-expanded": isOpen,
823
- "aria-controls": panelId,
824
- onClick: () => toggle(module.id),
825
- 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",
826
- children: [
827
- number,
828
- /* @__PURE__ */ jsxs6("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
829
- /* @__PURE__ */ jsx8("span", { className: "font-semibold leading-snug", children: module.title }),
830
- /* @__PURE__ */ jsx8("span", { className: "text-sm text-muted-foreground", children: meta.join(SEPARATOR) })
831
- ] }),
832
- /* @__PURE__ */ jsx8(
833
- "span",
834
- {
835
- "aria-hidden": "true",
836
- className: cn8(
837
- "shrink-0 text-muted-foreground transition-transform motion-reduce:transition-none",
838
- isOpen && "rotate-180"
839
- ),
840
- children: "\u25BE"
841
- }
842
- )
843
- ]
844
- }
930
+ if (seconds !== null) meta.push(formatTotal(seconds, labels));
931
+ return /* @__PURE__ */ jsxs6("li", { "data-slot": "course-syllabus-module", children: [
932
+ /* @__PURE__ */ jsxs6(
933
+ "button",
934
+ {
935
+ type: "button",
936
+ "aria-expanded": isOpen,
937
+ "aria-controls": panelId,
938
+ onClick: () => toggle(module.id),
939
+ className: cn8(
940
+ "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",
941
+ isOpen && "bg-muted/40"
845
942
  ),
846
- /* @__PURE__ */ jsx8(
847
- "ol",
848
- {
849
- id: panelId,
850
- hidden: !isOpen,
851
- "data-slot": "course-syllabus-lessons",
852
- className: "list-none space-y-2 border-t border-border px-4 py-3 pl-[3.75rem]",
853
- children: lessons.map((lesson) => /* @__PURE__ */ jsxs6(
854
- "li",
855
- {
856
- "data-slot": "course-syllabus-lesson",
857
- className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm",
858
- children: [
859
- /* @__PURE__ */ jsx8("span", { "data-slot": "course-syllabus-lesson-title", className: "flex-1", children: lesson.title }),
860
- lesson.isFreePreview && /* @__PURE__ */ jsx8("span", { className: "rounded-full bg-muted px-2 py-0.5 text-xs font-medium", children: labels.freePreview }),
861
- typeof lesson.durationSeconds === "number" && lesson.durationSeconds > 0 && /* @__PURE__ */ jsx8("span", { className: "text-muted-foreground tabular-nums", children: formatDuration(lesson.durationSeconds, labels) })
862
- ]
863
- },
864
- lesson.id
865
- ))
866
- }
867
- )
868
- ]
869
- },
870
- module.id
871
- );
943
+ children: [
944
+ number,
945
+ /* @__PURE__ */ jsxs6("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
946
+ /* @__PURE__ */ jsx8("span", { className: "font-semibold leading-snug", children: module.title }),
947
+ /* @__PURE__ */ jsx8("span", { className: "text-sm text-muted-foreground", children: meta.join(SEPARATOR) })
948
+ ] }),
949
+ /* @__PURE__ */ jsx8(
950
+ "svg",
951
+ {
952
+ ...ICON_PROPS,
953
+ "data-slot": "course-syllabus-chevron",
954
+ className: cn8(
955
+ "size-5 shrink-0 text-muted-foreground transition-transform duration-200 motion-reduce:transition-none",
956
+ isOpen && "rotate-180"
957
+ ),
958
+ children: /* @__PURE__ */ jsx8("path", { d: "m6 9 6 6 6-6" })
959
+ }
960
+ )
961
+ ]
962
+ }
963
+ ),
964
+ /* @__PURE__ */ jsx8(
965
+ "ol",
966
+ {
967
+ id: panelId,
968
+ hidden: !isOpen,
969
+ "data-slot": "course-syllabus-lessons",
970
+ className: "list-none border-t border-border p-0 py-1",
971
+ children: lessons.map((lesson) => {
972
+ const type = lesson.hasVideo ? "video" : "text";
973
+ return /* @__PURE__ */ jsxs6(
974
+ "li",
975
+ {
976
+ "data-slot": "course-syllabus-lesson",
977
+ 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",
978
+ children: [
979
+ /* @__PURE__ */ jsx8(LessonIcon, { type }),
980
+ /* @__PURE__ */ jsx8("span", { className: "sr-only", children: type === "video" ? labels.videoLesson : labels.textLesson }),
981
+ /* @__PURE__ */ jsx8(
982
+ "span",
983
+ {
984
+ "data-slot": "course-syllabus-lesson-title",
985
+ className: "min-w-0 flex-1 leading-snug",
986
+ children: lesson.title
987
+ }
988
+ ),
989
+ lesson.isFreePreview && renderPreview(lesson),
990
+ hasDuration(lesson) && /* @__PURE__ */ jsx8(
991
+ "time",
992
+ {
993
+ "data-slot": "course-syllabus-lesson-duration",
994
+ dateTime: isoDuration(lesson.durationSeconds),
995
+ className: "ml-auto shrink-0 text-muted-foreground tabular-nums",
996
+ children: formatClock(lesson.durationSeconds)
997
+ }
998
+ )
999
+ ]
1000
+ },
1001
+ lesson.id
1002
+ );
1003
+ })
1004
+ }
1005
+ )
1006
+ ] }, module.id);
872
1007
  }) })
873
1008
  ]
874
1009
  }
@@ -881,7 +1016,7 @@ import { cn as cn9 } from "@reblu/site-ui/utils";
881
1016
  import {
882
1017
  DATA_REQUEST_TYPES
883
1018
  } from "@reblu/site-contracts";
884
- import { Fragment, jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1019
+ import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
885
1020
  var DATA_REQUEST_FIELD_LIMITS = Object.freeze({ name: 200, email: 320, details: 5e3 });
886
1021
  var DEFAULT_DATA_REQUEST_FORM_LABELS = {
887
1022
  requestTypeLabel: "Derecho que quieres ejercer",
@@ -907,7 +1042,7 @@ var DEFAULT_DATA_REQUEST_FORM_LABELS = {
907
1042
  nameRequired: "Indica tu nombre.",
908
1043
  emailInvalid: "Introduce un email v\xE1lido.",
909
1044
  consentRequired: "Debes aceptar el tratamiento de tus datos para gestionar la solicitud.",
910
- success: (deadline) => /* @__PURE__ */ jsxs7(Fragment, { children: [
1045
+ success: (deadline) => /* @__PURE__ */ jsxs7(Fragment2, { children: [
911
1046
  "Hemos recibido tu solicitud. Te responderemos como m\xE1ximo el ",
912
1047
  deadline,
913
1048
  "."