@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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @reblu/site-blocks
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 76e64f3: Free-preview lessons for the public, and a redesigned course programme (REB-1135).
8
+ - New RSC-safe `LessonPreview`: the hosted video player in an `<iframe>` plus the lesson body through the sanitising Markdown pipeline, over `getLessonPreview()` from `@reblu/site-next/academy-server`. Spanish copy overridable via `DEFAULT_LESSON_PREVIEW_LABELS`. The SITE's CSP needs `frame-src https://player.mux.com`.
9
+ - `CourseSyllabus` redesign: rotating chevron icon, a video/reading icon per lesson, lesson durations as «m:ss» / «h:mm:ss» aligned right, the course total in the summary, «Expandir todo / Contraer todo», and «Vista previa gratuita» as a call to action through the new `getPreviewHref`, `previewHrefTemplate` or `onPreview` props. Every existing `data-slot` is kept; new slots: `course-syllabus-header`, `course-syllabus-toggle-all`, `course-syllabus-chevron`, `course-syllabus-lesson-icon`, `course-syllabus-lesson-duration`, `course-syllabus-preview`. New labels: `expandAll`, `collapseAll`, `videoLesson`, `textLesson`. Visible change: lesson durations read «10:00» instead of «10 min».
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [76e64f3]
14
+ - @reblu/site-contracts@1.21.0
15
+
3
16
  ## 0.20.0
4
17
 
5
18
  ### Minor Changes
package/README.md CHANGED
@@ -25,6 +25,7 @@ colour on the red background. See `docs/theming/theming.md`.
25
25
  | `sanitizeHtml` | `.` | Sanitises an HTML string before `dangerouslySetInnerHTML` — the **HTML boundary**. See "Security boundaries" below. |
26
26
  | `MarkdownRenderer` | `.` | Canonical Blog renderer: `marked` → **hast-util-sanitize** (XSS) → component map, with a YouTube-domain allowlist. RSC-safe. |
27
27
  | `CourseDescription` | `.` | Drop-in for the rich-text course `description` (`client.academy.bySlug()`). Server Component. See "Rich-text descriptions". |
28
+ | `LessonPreview`, `DEFAULT_LESSON_PREVIEW_LABELS` | `.` | A public free-preview lesson: hosted video player + sanitised body (`getLessonPreview()`). Server Component. See "Free-preview lesson". |
28
29
  | `Treatment`, `ParentPage`, `ProfileCard`, `ProductCard` | `.` | Presentational organisms over the SITE API contracts. RSC-safe. `Specialty`: deprecated alias, see below. |
29
30
  | `ProfessionalFields` | `.` | The tenant's own professional fields (`professional.fields`) on a team card or profile. RSC-safe. See below. |
30
31
  | `ShopFilters` | `.` | Plug-and-play shop filter chrome over `GET /api/site/shop/facets`. RSC-safe. See "Shop filters" below. |
@@ -906,40 +907,109 @@ It renders the package's `ProductCard` by default. To use your own card, pass
906
907
  `headingClassName` replaces the heading's typography (see the className rule
907
908
  above). It renders nothing for an empty list.
908
909
 
909
- ## Course programme — `CourseSyllabus` (REB-1134)
910
+ ## Course programme — `CourseSyllabus` (REB-1134, redesigned in REB-1135)
910
911
 
911
912
  The programme of a public course page: every module, and inside each module the
912
913
  lessons that make it up. The header reads «Programa» and «3 módulos · 12
913
- lecciones». Each module row shows its lesson count and, when every lesson in it
914
- has a duration, the module's total («4 lecciones · 1 h 20 min»). Opening a
915
- module lists its lessons in order, with each lesson's duration and a «Vista
916
- previa gratuita» badge on free-preview lessons.
914
+ lecciones · 2 h 40 min», with an «Expandir todo / Contraer todo» control. Each
915
+ module row shows a number badge, its lesson count and, when every lesson in it
916
+ has a duration, the module's total («4 lecciones · 1 h 20 min»), and a chevron
917
+ that rotates when open. Opening a module lists its lessons in order: a video or
918
+ reading icon, the title, the duration right-aligned as «m:ss» / «h:mm:ss», and on
919
+ free-preview lessons «Vista previa gratuita» as a call to action.
917
920
 
918
921
  ```tsx
919
922
  import { CourseSyllabus } from '@reblu/site-blocks/client'
920
923
 
921
924
  // `course` is an `AcademyCourseDetail` from `client.academy.bySlug(slug)`.
922
- // It works from a Server Component page: every prop is serialisable.
923
- <CourseSyllabus modules={course.modules} />
925
+ // From a Server Component page, use the serialisable template:
926
+ <CourseSyllabus
927
+ modules={course.modules}
928
+ previewHrefTemplate={`/formacion/${course.slug}/preview/{lessonSlug}`}
929
+ />
930
+
931
+ // From a Client Component you can pass functions instead:
932
+ <CourseSyllabus
933
+ modules={course.modules}
934
+ getPreviewHref={(lesson) => `/formacion/${course.slug}/preview/${lesson.slug}`}
935
+ // or: onPreview={(lesson) => openPreviewModal(lesson.slug)}
936
+ />
924
937
  ```
925
938
 
939
+ - **Free preview call to action.** `getPreviewHref(lesson)` (or the
940
+ serialisable `previewHrefTemplate`, with `{lessonSlug}` and `{lessonId}`)
941
+ renders a link; `onPreview(lesson)` renders a button (e.g. to open a modal).
942
+ Both apply only to lessons with `isFreePreview`. With neither, the flag is a
943
+ plain label. The link/button is named «Vista previa gratuita: {título}». Point
944
+ it at a page that renders `LessonPreview` (see "Free-preview lesson").
926
945
  - **Public data only.** It renders what the API serves. The API returns
927
946
  published lessons only, and never a video, playback or attachment id, a
928
947
  signed URL or the lesson content.
929
948
  - **Accessibility.** The block is a `region` named by its heading. A module
930
949
  with lessons is a `<button>` with `aria-expanded` and `aria-controls`
931
- pointing at its lesson list. Tab reaches it; Enter and Space open and close
932
- it. Modules open independently and start closed.
950
+ pointing at its lesson list. Tab reaches «Expandir todo», then each module;
951
+ Enter and Space open and close it, and an open module's preview links follow
952
+ it in the tab order. Modules open independently and start closed. Icons are
953
+ `aria-hidden`; each lesson's type is announced as «Vídeo» / «Lectura».
954
+ Durations are `<time datetime="PT10M0S">`.
933
955
  - **Modules without lessons** render as a plain row with the title, not a
934
956
  button. A payload without `lessons` renders the same way.
935
957
  - **Copy.** The defaults are Spanish (`DEFAULT_COURSE_SYLLABUS_LABELS`).
936
958
  Override any of them with `labels={{ heading, moduleSingular, modulePlural,
937
- lessonSingular, lessonPlural, freePreview, hours, minutes }}`.
938
- - **Styling.** `headingLevel` (default `2`), `headingClassName` (replaces the
959
+ lessonSingular, lessonPlural, freePreview, hours, minutes, expandAll,
960
+ collapseAll, videoLesson, textLesson }}`.
961
+ - **Styling.** Uses only the SDK tokens (`border`, `card`, `muted`, `primary`,
962
+ `ring`). `headingLevel` (default `2`), `headingClassName` (replaces the
939
963
  heading's typography, see the className rule above) and `className`. Style
940
- the parts with `[data-slot="course-syllabus"]`, `course-syllabus-module`,
941
- `course-syllabus-lessons` and `course-syllabus-lesson`. It renders nothing
942
- for a course without modules.
964
+ the parts with `[data-slot="course-syllabus"]`, `course-syllabus-header`,
965
+ `course-syllabus-summary`, `course-syllabus-toggle-all`,
966
+ `course-syllabus-module`, `course-syllabus-chevron`, `course-syllabus-lessons`,
967
+ `course-syllabus-lesson`, `course-syllabus-lesson-icon` (`data-type="video" |
968
+ "text"`), `course-syllabus-lesson-title`, `course-syllabus-lesson-duration`
969
+ and `course-syllabus-preview`. No slot was renamed in REB-1135. It renders
970
+ nothing for a course without modules.
971
+
972
+ ## Free-preview lesson — `LessonPreview` (REB-1135)
973
+
974
+ A lesson the owner marked «Vista previa gratuita», for visitors who have not
975
+ paid or enrolled: the video and the lesson body. Fetch it on the server with
976
+ `getLessonPreview` (`@reblu/site-next/academy-server`), which returns `null`
977
+ for any lesson the public may not see.
978
+
979
+ ```tsx
980
+ // app/formacion/[slug]/preview/[lessonSlug]/page.tsx — a Server Component
981
+ import { notFound } from 'next/navigation'
982
+ import { LessonPreview } from '@reblu/site-blocks'
983
+ import { getLessonPreview } from '@reblu/site-next/academy-server'
984
+ import { client } from '@/lib/reblu' // your server-side RebluClient
985
+
986
+ export const dynamic = 'force-dynamic' // the video token is short-lived: never ISR
987
+
988
+ export default async function Page({ params }) {
989
+ const { slug, lessonSlug } = await params
990
+ const lesson = await getLessonPreview(client, slug, lessonSlug)
991
+ if (!lesson) notFound()
992
+ return <LessonPreview lesson={lesson} headingLevel={1} />
993
+ }
994
+ ```
995
+
996
+ - **Player.** `video.embedUrl` is the provider's hosted player, already carrying
997
+ its short-lived token, rendered in an `<iframe>` named «Vídeo: {título}» —
998
+ no video library in the SDK. Only `https:` URLs are embedded. Add
999
+ `frame-src https://player.mux.com` to the SITE's Content-Security-Policy.
1000
+ `video` is `null` for a text-only lesson (no player is rendered).
1001
+ - **Body.** `content` is Markdown, rendered through the same sanitising
1002
+ pipeline as `CourseDescription`: no raw HTML, no scripts, no `javascript:`
1003
+ links. Its headings sit one level below the lesson title. An empty body
1004
+ renders nothing.
1005
+ - **Semantics.** An `<article>` named by the title; the duration is a
1006
+ `<time>`. Server Component (RSC-safe), from the root entry.
1007
+ - **Copy.** Spanish defaults (`DEFAULT_LESSON_PREVIEW_LABELS`), override with
1008
+ `labels={{ badge, player, duration }}`.
1009
+ - **Styling.** `headingLevel` (default `2`), `headingId`, `headingClassName`,
1010
+ `contentClassName`, `className`; slots `lesson-preview`,
1011
+ `lesson-preview-badge`, `lesson-preview-duration`, `lesson-preview-player`,
1012
+ `lesson-preview-content`.
943
1013
 
944
1014
  ## Page blocks
945
1015
 
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
  }