@burdenoff/microfe-buildmyiq 2026.1029.6 → 2026.1030.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"LessonPlayerPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAgEhC,eAAO,MAAM,gBAAgB,EAAE,EAuhB9B,CAAC"}
1
+ {"version":3,"file":"LessonPlayerPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAgEhC,eAAO,MAAM,gBAAgB,EAAE,EAqjB9B,CAAC"}
@@ -9,215 +9,233 @@ import { EmptyState as s } from "../../components/EmptyState.js";
9
9
  import { ErrorView as c, LoadingView as l } from "../../components/StateViews.js";
10
10
  import { MiniBar as ee } from "../../components/MiniBar.js";
11
11
  import { ProgressRing as te } from "../../components/Viz.js";
12
- import { useBuiCompleteMyEnrollment as ne, useBuiCourse as re, useBuiMyEnrollment as u, useBuiRecordMyLessonProgress as ie } from "../../hooks/useBuildmyiqApi.js";
12
+ import { useBuiCompleteMyEnrollment as ne, useBuiCourse as re, useBuiMyEnrollment as ie, useBuiRecordMyLessonProgress as ae } from "../../hooks/useBuildmyiqApi.js";
13
13
  import "../../hooks/index.js";
14
- import { FileLink as ae } from "../../components/FileLink.js";
15
- import { TutorPanel as oe } from "../../components/TutorPanel.js";
16
- import { OfflineLessonControl as se } from "../../components/OfflineLessonControl.js";
14
+ import { FileLink as oe } from "../../components/FileLink.js";
15
+ import { TutorPanel as se } from "../../components/TutorPanel.js";
16
+ import { OfflineLessonControl as ce } from "../../components/OfflineLessonControl.js";
17
17
  import "../../components/index.js";
18
- import { useEffect as ce, useMemo as d, useRef as le, useState as f } from "react";
19
- import { ArrowLeft as ue, BookOpen as de, Check as fe, CheckCircle2 as p, ChevronLeft as pe, ChevronRight as me, FileText as m, HelpCircle as h, ListChecks as he, Loader2 as g, MousePointerClick as _, PenLine as v, PlayCircle as y, Radio as b, Trophy as x } from "lucide-react";
20
- import { useParams as ge, useSearchParams as _e } from "react-router";
21
- import { Fragment as ve, jsx as S, jsxs as C } from "react/jsx-runtime";
22
- import { Button as w, GlassCard as T } from "@burdenoff/fe-libs/ui";
18
+ import { useEffect as le, useMemo as u, useRef as ue, useState as d } from "react";
19
+ import { ArrowLeft as de, BookOpen as fe, Check as pe, CheckCircle2 as f, ChevronLeft as me, ChevronRight as he, FileText as p, HelpCircle as m, ListChecks as ge, Loader2 as h, MousePointerClick as g, PenLine as _, PlayCircle as v, Radio as y, Trophy as _e } from "lucide-react";
20
+ import { useParams as ve, useSearchParams as ye } from "react-router";
21
+ import { Fragment as b, jsx as x, jsxs as S } from "react/jsx-runtime";
22
+ import { Button as C, GlassCard as w } from "@burdenoff/fe-libs/ui";
23
23
  //#region src/buildmyiq/pages/learning/LessonPlayerPage.tsx
24
- var E = {
25
- ARTICLE: m,
26
- VIDEO: y,
27
- QUIZ: h,
28
- ASSIGNMENT: v,
29
- INTERACTIVE: _,
30
- LIVE_SESSION: b
31
- }, D = (e) => [...e].sort((e, t) => e.position - t.position), O = () => {
32
- let { enrollmentId: h } = ge(), [_] = _e(), v = _.get("lesson"), { navigateTo: y } = e(), b = t(), O = u(h), k = O.data ?? null, A = k?.courseId, j = re(A), M = j.data ?? null, N = ie(), P = ne(), F = N.mutate, I = le(/* @__PURE__ */ new Set()), [L, R] = f(null), [z, B] = f(!1), V = d(() => {
33
- if (!M) return [];
34
- let e = /* @__PURE__ */ new Set(), t = D(M.modules ?? []), n = [];
24
+ var T = {
25
+ ARTICLE: p,
26
+ VIDEO: v,
27
+ QUIZ: m,
28
+ ASSIGNMENT: _,
29
+ INTERACTIVE: g,
30
+ LIVE_SESSION: y
31
+ }, E = (e) => [...e].sort((e, t) => e.position - t.position), D = () => {
32
+ let { enrollmentId: m } = ve(), [g] = ye(), _ = g.get("lesson"), { navigateTo: v } = e(), y = t(), D = ie(m), O = D.data ?? null, k = O?.courseId, A = re(k), j = A.data ?? null, M = ae(), N = ne(), P = M.mutate, F = ue(/* @__PURE__ */ new Set()), [I, L] = d(null), [R, z] = d(!1), [B, V] = d(null), H = u(() => {
33
+ if (!j) return [];
34
+ let e = /* @__PURE__ */ new Set(), t = E(j.modules ?? []), n = [];
35
35
  for (let r of t) {
36
- let t = D(r.lessons ?? []).filter((t) => e.has(t.id) ? !1 : (e.add(t.id), !0));
36
+ let t = E(r.lessons ?? []).filter((t) => e.has(t.id) ? !1 : (e.add(t.id), !0));
37
37
  t.length > 0 && n.push({
38
38
  id: r.id,
39
39
  title: r.title,
40
40
  lessons: t
41
41
  });
42
42
  }
43
- let r = D(M.lessons ?? []).filter((t) => e.has(t.id) ? !1 : (e.add(t.id), !0));
43
+ let r = E(j.lessons ?? []).filter((t) => e.has(t.id) ? !1 : (e.add(t.id), !0));
44
44
  return r.length > 0 && n.push({
45
45
  id: "__standalone",
46
- title: b("common.lessons", "Lessons"),
46
+ title: y("common.lessons", "Lessons"),
47
47
  lessons: r
48
48
  }), n;
49
- }, [M, b]), H = d(() => V.flatMap((e) => e.lessons), [V]), ye = d(() => new Map(H.map((e) => [e.id, e.title])), [H]), U = d(() => {
49
+ }, [j, y]), U = u(() => H.flatMap((e) => e.lessons), [H]), be = u(() => new Map(U.map((e) => [e.id, e.title])), [U]), W = u(() => {
50
50
  let e = /* @__PURE__ */ new Map();
51
- for (let t of k?.lessonProgress ?? []) e.set(t.lessonId, t.status);
51
+ for (let t of O?.lessonProgress ?? []) e.set(t.lessonId, t.status);
52
52
  return e;
53
- }, [k]), W = d(() => H.length === 0 ? null : L && H.some((e) => e.id === L) ? L : v && H.some((e) => e.id === v) ? v : H[0].id, [
54
- H,
55
- L,
56
- v
53
+ }, [O]), G = u(() => U.length === 0 ? null : I && U.some((e) => e.id === I) ? I : _ && U.some((e) => e.id === _) ? _ : U[0].id, [
54
+ U,
55
+ I,
56
+ _
57
57
  ]);
58
- ce(() => {
59
- if (!h || !W) return;
60
- let e = U.get(W);
61
- e === "COMPLETED" || e === "IN_PROGRESS" || I.current.has(W) || (I.current.add(W), F({
62
- enrollmentId: h,
63
- lessonId: W,
58
+ le(() => {
59
+ if (!m || !G) return;
60
+ let e = W.get(G);
61
+ e === "COMPLETED" || e === "IN_PROGRESS" || F.current.has(G) || (F.current.add(G), P({
62
+ enrollmentId: m,
63
+ lessonId: G,
64
64
  status: "IN_PROGRESS"
65
65
  }));
66
66
  }, [
67
- h,
67
+ m,
68
+ G,
68
69
  W,
69
- U,
70
- F
70
+ P
71
71
  ]);
72
- let G = H.findIndex((e) => e.id === W), K = G >= 0 ? H[G] : null, q = (K ? U.get(K.id) : void 0) === "COMPLETED", J = H.filter((e) => U.get(e.id) === "COMPLETED").length, be = H.length > 0 && J === H.length, xe = k?.status === "COMPLETED", Y = (e) => {
73
- R(e), B(!1);
74
- }, X = (e) => {
75
- if (G < 0) return;
76
- let t = H[G + e];
77
- t && Y(t.id);
78
- }, Z = async () => {
79
- if (!h || !K) return;
80
- await N.mutateAsync({
81
- enrollmentId: h,
82
- lessonId: K.id,
83
- status: "COMPLETED"
84
- });
85
- let e = H[G + 1];
86
- e && R(e.id);
87
- }, Se = async () => {
88
- h && await P.mutateAsync({ id: h });
72
+ let K = U.findIndex((e) => e.id === G), q = K >= 0 ? U[K] : null, J = (q ? W.get(q.id) : void 0) === "COMPLETED", Y = U.filter((e) => W.get(e.id) === "COMPLETED").length, xe = U.length > 0 && Y === U.length, Se = O?.status === "COMPLETED", X = (e) => {
73
+ L(e), z(!1), V(null);
74
+ }, Z = (e) => {
75
+ if (K < 0) return;
76
+ let t = U[K + e];
77
+ t && X(t.id);
78
+ }, Ce = async () => {
79
+ if (!m || !q) return;
80
+ V(null);
81
+ try {
82
+ await M.mutateAsync({
83
+ enrollmentId: m,
84
+ lessonId: q.id,
85
+ status: "COMPLETED"
86
+ });
87
+ } catch (e) {
88
+ V(e instanceof Error && e.message ? e.message : y("buildmyiq.lessonPlayer.markCompleteError", "Could not mark this lesson complete."));
89
+ return;
90
+ }
91
+ let e = U[K + 1];
92
+ e && L(e.id);
93
+ }, we = async () => {
94
+ if (m) {
95
+ V(null);
96
+ try {
97
+ await N.mutateAsync({ id: m });
98
+ } catch (e) {
99
+ V(e instanceof Error && e.message ? e.message : y("buildmyiq.lessonPlayer.finishCourseError", "Could not finish this course."));
100
+ }
101
+ }
89
102
  };
90
- if (!h) return /* @__PURE__ */ S(r, {
91
- title: b("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
92
- children: /* @__PURE__ */ S(s, {
103
+ if (!m) return /* @__PURE__ */ x(r, {
104
+ title: y("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
105
+ children: /* @__PURE__ */ x(s, {
93
106
  illustration: "empty-data",
94
- title: b("buildmyiq.lessonPlayer.noEnrollmentTitle", "No enrollment selected"),
95
- description: b("buildmyiq.lessonPlayer.noEnrollmentDescription", "Open a course from My Learning to start the lesson player."),
96
- action: /* @__PURE__ */ S(w, {
97
- onClick: () => y("learning"),
98
- children: b("buildmyiq.lessonPlayer.backToLearning", "Back to My Learning")
107
+ title: y("buildmyiq.lessonPlayer.noEnrollmentTitle", "No enrollment selected"),
108
+ description: y("buildmyiq.lessonPlayer.noEnrollmentDescription", "Open a course from My Learning to start the lesson player."),
109
+ action: /* @__PURE__ */ x(C, {
110
+ onClick: () => v("learning"),
111
+ children: y("buildmyiq.lessonPlayer.backToLearning", "Back to My Learning")
99
112
  })
100
113
  })
101
114
  });
102
- let Ce = O.isLoading || !!A && j.isLoading, Q = /* @__PURE__ */ C(w, {
115
+ let Te = D.isLoading || !!k && A.isLoading, Q = /* @__PURE__ */ S(C, {
103
116
  variant: "ghost",
104
117
  size: "sm",
105
- onClick: () => y("learning"),
118
+ onClick: () => v("learning"),
106
119
  children: [
107
- /* @__PURE__ */ S(ue, { className: "mr-1 h-4 w-4" }),
120
+ /* @__PURE__ */ x(de, { className: "mr-1 h-4 w-4" }),
108
121
  " ",
109
- b("buildmyiq.nav.learning", "My Learning")
122
+ y("buildmyiq.nav.learning", "My Learning")
110
123
  ]
111
124
  });
112
- if (Ce) return /* @__PURE__ */ S(r, {
113
- title: b("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
125
+ if (Te) return /* @__PURE__ */ x(r, {
126
+ title: y("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
114
127
  actions: Q,
115
- children: /* @__PURE__ */ S(l, { label: b("buildmyiq.lessonPlayer.loading", "Loading your lesson…") })
128
+ children: /* @__PURE__ */ x(l, { label: y("buildmyiq.lessonPlayer.loading", "Loading your lesson…") })
116
129
  });
117
- if (O.isError) return /* @__PURE__ */ S(r, {
118
- title: b("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
130
+ if (D.isError) return /* @__PURE__ */ x(r, {
131
+ title: y("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
119
132
  actions: Q,
120
- children: /* @__PURE__ */ S(c, {
121
- title: b("buildmyiq.lessonPlayer.enrollmentErrorTitle", "Couldn't load this enrollment"),
122
- message: O.error instanceof Error ? O.error.message : void 0,
123
- onRetry: () => O.refetch()
133
+ children: /* @__PURE__ */ x(c, {
134
+ title: y("buildmyiq.lessonPlayer.enrollmentErrorTitle", "Couldn't load this enrollment"),
135
+ message: D.error instanceof Error ? D.error.message : void 0,
136
+ onRetry: () => D.refetch()
124
137
  })
125
138
  });
126
- if (!k) return /* @__PURE__ */ S(r, {
127
- title: b("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
139
+ if (!O) return /* @__PURE__ */ x(r, {
140
+ title: y("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
128
141
  actions: Q,
129
- children: /* @__PURE__ */ S(s, {
142
+ children: /* @__PURE__ */ x(s, {
130
143
  illustration: "empty-data",
131
- title: b("buildmyiq.lessonPlayer.enrollmentNotFoundTitle", "Enrollment not found"),
132
- description: b("buildmyiq.lessonPlayer.enrollmentNotFoundDescription", "This enrollment may have been removed or is not available to you."),
133
- action: /* @__PURE__ */ S(w, {
134
- onClick: () => y("learning"),
135
- children: b("buildmyiq.lessonPlayer.backToLearning", "Back to My Learning")
144
+ title: y("buildmyiq.lessonPlayer.enrollmentNotFoundTitle", "Enrollment not found"),
145
+ description: y("buildmyiq.lessonPlayer.enrollmentNotFoundDescription", "This enrollment may have been removed or is not available to you."),
146
+ action: /* @__PURE__ */ x(C, {
147
+ onClick: () => v("learning"),
148
+ children: y("buildmyiq.lessonPlayer.backToLearning", "Back to My Learning")
136
149
  })
137
150
  })
138
151
  });
139
- if (j.isError) return /* @__PURE__ */ S(r, {
140
- title: b("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
152
+ if (A.isError) return /* @__PURE__ */ x(r, {
153
+ title: y("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
141
154
  actions: Q,
142
- children: /* @__PURE__ */ S(c, {
143
- title: b("buildmyiq.lessonPlayer.courseErrorTitle", "Couldn't load the course"),
144
- message: j.error instanceof Error ? j.error.message : void 0,
145
- onRetry: () => j.refetch()
155
+ children: /* @__PURE__ */ x(c, {
156
+ title: y("buildmyiq.lessonPlayer.courseErrorTitle", "Couldn't load the course"),
157
+ message: A.error instanceof Error ? A.error.message : void 0,
158
+ onRetry: () => A.refetch()
146
159
  })
147
160
  });
148
- let $ = Math.round(k.progressPct ?? 0), we = K ? E[K.type] : m;
149
- return /* @__PURE__ */ S(r, {
150
- title: M?.title ?? b("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
161
+ let $ = Math.round(O.progressPct ?? 0), Ee = q ? T[q.type] : p;
162
+ return /* @__PURE__ */ x(r, {
163
+ title: j?.title ?? y("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
151
164
  actions: Q,
152
- children: /* @__PURE__ */ C("div", {
165
+ children: /* @__PURE__ */ S("div", {
153
166
  className: "flex flex-col gap-5",
154
167
  children: [
155
- /* @__PURE__ */ C(T, {
168
+ /* @__PURE__ */ S(w, {
156
169
  treatment: "glass",
157
170
  glow: $ > 0,
158
171
  className: "flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:gap-5",
159
172
  children: [
160
- /* @__PURE__ */ S(te, {
173
+ /* @__PURE__ */ x(te, {
161
174
  value: $,
162
175
  size: 64,
163
176
  className: "shrink-0"
164
177
  }),
165
- /* @__PURE__ */ C("div", {
178
+ /* @__PURE__ */ S("div", {
166
179
  className: "min-w-0 flex-1",
167
- children: [/* @__PURE__ */ C("div", {
180
+ children: [/* @__PURE__ */ S("div", {
168
181
  className: "flex flex-wrap items-center gap-2",
169
- children: [/* @__PURE__ */ S(o, { status: k.status }), /* @__PURE__ */ S("span", {
182
+ children: [/* @__PURE__ */ x(o, { status: O.status }), /* @__PURE__ */ x("span", {
170
183
  className: "text-sm text-text-secondary tabular-nums",
171
- children: b("buildmyiq.lessonPlayer.progressSummary", "{{completed}} of {{total}} lessons complete", {
172
- completed: J,
173
- total: H.length
184
+ children: y("buildmyiq.lessonPlayer.progressSummary", "{{completed}} of {{total}} lessons complete", {
185
+ completed: Y,
186
+ total: U.length
174
187
  })
175
188
  })]
176
- }), /* @__PURE__ */ S(ee, {
189
+ }), /* @__PURE__ */ x(ee, {
177
190
  value: $,
178
191
  className: "mt-3"
179
192
  })]
180
193
  }),
181
- be && !xe && /* @__PURE__ */ C(w, {
194
+ xe && !Se && /* @__PURE__ */ S(C, {
182
195
  className: "shrink-0 max-sm:w-full",
183
- onClick: Se,
184
- disabled: P.isPending,
185
- children: [P.isPending ? /* @__PURE__ */ S(g, { className: "mr-1 h-4 w-4 animate-spin" }) : /* @__PURE__ */ S(x, { className: "mr-1 h-4 w-4" }), b("buildmyiq.lessonPlayer.finishCourse", "Finish course")]
196
+ onClick: we,
197
+ disabled: N.isPending,
198
+ children: [N.isPending ? /* @__PURE__ */ x(h, { className: "mr-1 h-4 w-4 animate-spin" }) : /* @__PURE__ */ x(_e, { className: "mr-1 h-4 w-4" }), y("buildmyiq.lessonPlayer.finishCourse", "Finish course")]
186
199
  })
187
200
  ]
188
201
  }),
189
- /* @__PURE__ */ C(w, {
202
+ B && /* @__PURE__ */ x("p", {
203
+ role: "alert",
204
+ className: "text-sm text-status-error-text",
205
+ children: B
206
+ }),
207
+ /* @__PURE__ */ S(C, {
190
208
  variant: "secondary",
191
209
  size: "sm",
192
210
  className: "w-full lg:hidden",
193
- onClick: () => B((e) => !e),
194
- children: [/* @__PURE__ */ S(he, { className: "mr-1 h-4 w-4" }), z ? b("buildmyiq.lessonPlayer.hideLessons", "Hide lessons") : b("buildmyiq.lessonPlayer.showLessons", "Lessons ({{completed}}/{{total}})", {
195
- completed: J,
196
- total: H.length
211
+ onClick: () => z((e) => !e),
212
+ children: [/* @__PURE__ */ x(ge, { className: "mr-1 h-4 w-4" }), R ? y("buildmyiq.lessonPlayer.hideLessons", "Hide lessons") : y("buildmyiq.lessonPlayer.showLessons", "Lessons ({{completed}}/{{total}})", {
213
+ completed: Y,
214
+ total: U.length
197
215
  })]
198
216
  }),
199
- /* @__PURE__ */ C("div", {
217
+ /* @__PURE__ */ S("div", {
200
218
  className: "flex flex-col gap-5 lg:flex-row",
201
- children: [/* @__PURE__ */ S("aside", {
202
- className: n("lg:block lg:w-80 lg:shrink-0", z ? "block" : "hidden"),
203
- children: /* @__PURE__ */ S(T, {
219
+ children: [/* @__PURE__ */ x("aside", {
220
+ className: n("lg:block lg:w-80 lg:shrink-0", R ? "block" : "hidden"),
221
+ children: /* @__PURE__ */ x(w, {
204
222
  treatment: "plain",
205
223
  className: "overflow-hidden p-0 lg:sticky lg:top-4",
206
- children: H.length === 0 ? /* @__PURE__ */ S("div", {
224
+ children: U.length === 0 ? /* @__PURE__ */ x("div", {
207
225
  className: "p-4 text-sm text-text-secondary",
208
- children: b("buildmyiq.lessonPlayer.noLessons", "This course has no lessons yet.")
209
- }) : /* @__PURE__ */ C(ve, { children: [/* @__PURE__ */ S("nav", {
226
+ children: y("buildmyiq.lessonPlayer.noLessons", "This course has no lessons yet.")
227
+ }) : /* @__PURE__ */ S(b, { children: [/* @__PURE__ */ x("nav", {
210
228
  className: "max-h-[68vh] overflow-auto p-3",
211
- children: V.map((e) => {
212
- let t = e.lessons.filter((e) => U.get(e.id) === "COMPLETED").length;
213
- return /* @__PURE__ */ C("div", {
229
+ children: H.map((e) => {
230
+ let t = e.lessons.filter((e) => W.get(e.id) === "COMPLETED").length;
231
+ return /* @__PURE__ */ S("div", {
214
232
  className: "mb-4 last:mb-0",
215
- children: [/* @__PURE__ */ C("div", {
233
+ children: [/* @__PURE__ */ S("div", {
216
234
  className: "mb-2 flex items-center justify-between gap-2 px-1",
217
- children: [/* @__PURE__ */ S("p", {
235
+ children: [/* @__PURE__ */ x("p", {
218
236
  className: "min-w-0 truncate text-sm font-semibold text-text-primary",
219
237
  children: e.title
220
- }), /* @__PURE__ */ C("span", {
238
+ }), /* @__PURE__ */ S("span", {
221
239
  className: "shrink-0 text-xs font-medium tabular-nums text-text-secondary",
222
240
  children: [
223
241
  t,
@@ -225,33 +243,33 @@ var E = {
225
243
  e.lessons.length
226
244
  ]
227
245
  })]
228
- }), /* @__PURE__ */ S("ul", {
246
+ }), /* @__PURE__ */ x("ul", {
229
247
  className: "flex flex-col",
230
248
  children: e.lessons.map((t, r) => {
231
- let o = U.get(t.id) === "COMPLETED", s = t.id === W, c = r === e.lessons.length - 1, l = E[t.type];
232
- return /* @__PURE__ */ C("li", {
249
+ let o = W.get(t.id) === "COMPLETED", s = t.id === G, c = r === e.lessons.length - 1, l = T[t.type];
250
+ return /* @__PURE__ */ S("li", {
233
251
  className: "flex gap-3",
234
- children: [/* @__PURE__ */ C("div", {
252
+ children: [/* @__PURE__ */ S("div", {
235
253
  className: "flex flex-col items-center pt-1.5",
236
- children: [o ? /* @__PURE__ */ S("span", {
254
+ children: [o ? /* @__PURE__ */ x("span", {
237
255
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-status-success-text)]",
238
- children: /* @__PURE__ */ S(fe, { className: "h-3.5 w-3.5 text-status-success-bg-subtle" })
239
- }) : s ? /* @__PURE__ */ S("span", {
256
+ children: /* @__PURE__ */ x(pe, { className: "h-3.5 w-3.5 text-status-success-bg-subtle" })
257
+ }) : s ? /* @__PURE__ */ x("span", {
240
258
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] ring-2 ring-[var(--color-accent-teal)]",
241
- children: /* @__PURE__ */ S(l, { className: "h-3.5 w-3.5 text-[var(--color-accent-teal)]" })
242
- }) : /* @__PURE__ */ S("span", {
259
+ children: /* @__PURE__ */ x(l, { className: "h-3.5 w-3.5 text-[var(--color-accent-teal)]" })
260
+ }) : /* @__PURE__ */ x("span", {
243
261
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border-seam bg-bg-sunken",
244
- children: /* @__PURE__ */ S(l, { className: "h-3.5 w-3.5 text-text-secondary opacity-70" })
245
- }), !c && /* @__PURE__ */ S("span", { className: n("my-1 w-px flex-1", o ? "bg-[color-mix(in_srgb,var(--color-status-success-text)_45%,transparent)]" : "bg-border-seam") })]
246
- }), /* @__PURE__ */ C("button", {
262
+ children: /* @__PURE__ */ x(l, { className: "h-3.5 w-3.5 text-text-secondary opacity-70" })
263
+ }), !c && /* @__PURE__ */ x("span", { className: n("my-1 w-px flex-1", o ? "bg-[color-mix(in_srgb,var(--color-status-success-text)_45%,transparent)]" : "bg-border-seam") })]
264
+ }), /* @__PURE__ */ S("button", {
247
265
  type: "button",
248
- onClick: () => Y(t.id),
266
+ onClick: () => X(t.id),
249
267
  className: n("mb-1 min-w-0 flex-1 rounded-lg px-3 py-2 text-left transition-colors", s ? "bg-[var(--color-accent-soft)]" : "hover:bg-bg-sunken"),
250
268
  "aria-current": s ? "true" : void 0,
251
- children: [/* @__PURE__ */ S("span", {
269
+ children: [/* @__PURE__ */ x("span", {
252
270
  className: n("block truncate text-sm", s ? "font-medium text-text-primary" : o ? "text-text-secondary" : "text-text-primary"),
253
271
  children: t.title
254
- }), /* @__PURE__ */ C("span", {
272
+ }), /* @__PURE__ */ S("span", {
255
273
  className: "mt-0.5 block truncate text-xs text-text-secondary",
256
274
  children: [a(t.type), t.durationMin > 0 ? ` · ${i(t.durationMin)}` : ""]
257
275
  })]
@@ -261,131 +279,131 @@ var E = {
261
279
  })]
262
280
  }, e.id);
263
281
  })
264
- }), A && /* @__PURE__ */ S("div", {
282
+ }), k && /* @__PURE__ */ x("div", {
265
283
  className: "border-t border-border-seam p-3",
266
- children: /* @__PURE__ */ C(w, {
284
+ children: /* @__PURE__ */ S(C, {
267
285
  variant: "outline",
268
286
  size: "sm",
269
287
  className: "w-full",
270
- onClick: () => y(`catalog/${A}`),
288
+ onClick: () => v(`catalog/${k}`),
271
289
  children: [
272
- /* @__PURE__ */ S(de, { className: "mr-1.5 h-4 w-4" }),
290
+ /* @__PURE__ */ x(fe, { className: "mr-1.5 h-4 w-4" }),
273
291
  " ",
274
- b("buildmyiq.lessonPlayer.courseOverview", "Course overview")
292
+ y("buildmyiq.lessonPlayer.courseOverview", "Course overview")
275
293
  ]
276
294
  })
277
295
  })] })
278
296
  })
279
- }), /* @__PURE__ */ C("main", {
297
+ }), /* @__PURE__ */ S("main", {
280
298
  className: "min-w-0 flex-1",
281
- children: [K ? /* @__PURE__ */ C(T, {
299
+ children: [q ? /* @__PURE__ */ S(w, {
282
300
  treatment: "plain",
283
301
  className: "flex flex-col gap-4 p-5 sm:p-7",
284
302
  children: [
285
- /* @__PURE__ */ C("div", {
303
+ /* @__PURE__ */ S("div", {
286
304
  className: "flex flex-wrap items-center gap-2",
287
305
  children: [
288
- /* @__PURE__ */ C("span", {
306
+ /* @__PURE__ */ S("span", {
289
307
  className: "inline-flex items-center gap-1.5 rounded-full bg-[var(--color-accent-soft)] px-2.5 py-1 text-xs font-medium text-text-primary",
290
- children: [/* @__PURE__ */ S(we, { className: "h-3.5 w-3.5 text-[var(--color-accent-teal)]" }), a(K.type)]
308
+ children: [/* @__PURE__ */ x(Ee, { className: "h-3.5 w-3.5 text-[var(--color-accent-teal)]" }), a(q.type)]
291
309
  }),
292
- K.durationMin > 0 && /* @__PURE__ */ S("span", {
310
+ q.durationMin > 0 && /* @__PURE__ */ x("span", {
293
311
  className: "text-xs text-text-secondary",
294
- children: i(K.durationMin)
312
+ children: i(q.durationMin)
295
313
  }),
296
- q && /* @__PURE__ */ C("span", {
314
+ J && /* @__PURE__ */ S("span", {
297
315
  className: "inline-flex items-center gap-1 text-xs font-medium text-status-success-text",
298
316
  children: [
299
- /* @__PURE__ */ S(p, { className: "h-3.5 w-3.5" }),
317
+ /* @__PURE__ */ x(f, { className: "h-3.5 w-3.5" }),
300
318
  " ",
301
- b("buildmyiq.lessonPlayer.completedLabel", "Completed")
319
+ y("buildmyiq.lessonPlayer.completedLabel", "Completed")
302
320
  ]
303
321
  }),
304
- /* @__PURE__ */ S(se, { lesson: K }, K.id),
305
- /* @__PURE__ */ S("span", {
322
+ /* @__PURE__ */ x(ce, { lesson: q }, q.id),
323
+ /* @__PURE__ */ x("span", {
306
324
  className: "ml-auto text-xs tabular-nums text-text-secondary",
307
- children: b("buildmyiq.lessonPlayer.lessonPosition", "Lesson {{n}} of {{total}}", {
308
- n: G + 1,
309
- total: H.length
325
+ children: y("buildmyiq.lessonPlayer.lessonPosition", "Lesson {{n}} of {{total}}", {
326
+ n: K + 1,
327
+ total: U.length
310
328
  })
311
329
  })
312
330
  ]
313
331
  }),
314
- /* @__PURE__ */ S("h2", {
332
+ /* @__PURE__ */ x("h2", {
315
333
  className: "text-2xl font-semibold tracking-tight text-text-primary",
316
- children: K.title
334
+ children: q.title
317
335
  }),
318
- K.media ? /* @__PURE__ */ C("div", {
336
+ q.media ? /* @__PURE__ */ S("div", {
319
337
  className: "rounded-xl border border-border-seam bg-bg-sunken p-3",
320
- children: [/* @__PURE__ */ S("p", {
338
+ children: [/* @__PURE__ */ x("p", {
321
339
  className: "mb-2 text-xs font-semibold uppercase tracking-wide text-text-secondary",
322
- children: b("buildmyiq.lessonPlayer.resources", "Resources")
323
- }), /* @__PURE__ */ S(ae, { fileId: K.media.id })]
340
+ children: y("buildmyiq.lessonPlayer.resources", "Resources")
341
+ }), /* @__PURE__ */ x(oe, { fileId: q.media.id })]
324
342
  }) : null,
325
- K.content ? /* @__PURE__ */ S("p", {
343
+ q.content ? /* @__PURE__ */ x("p", {
326
344
  className: "max-w-prose whitespace-pre-wrap text-[15px] leading-7 text-text-primary",
327
- children: K.content
328
- }) : /* @__PURE__ */ S("p", {
345
+ children: q.content
346
+ }) : /* @__PURE__ */ x("p", {
329
347
  className: "text-sm text-text-secondary",
330
- children: K.media ? b("buildmyiq.lessonPlayer.noContentWithMedia", "This lesson has no written content beyond the attached media.") : b("buildmyiq.lessonPlayer.noContent", "This lesson has no written content")
348
+ children: q.media ? y("buildmyiq.lessonPlayer.noContentWithMedia", "This lesson has no written content beyond the attached media.") : y("buildmyiq.lessonPlayer.noContent", "This lesson has no written content")
331
349
  }),
332
- /* @__PURE__ */ C("div", {
350
+ /* @__PURE__ */ S("div", {
333
351
  className: "mt-2 flex flex-col gap-3 border-t border-border-seam pt-5 sm:flex-row sm:items-center sm:justify-between",
334
352
  children: [
335
- /* @__PURE__ */ C(w, {
353
+ /* @__PURE__ */ S(C, {
336
354
  variant: "outline",
337
355
  size: "sm",
338
356
  className: "max-sm:w-full",
339
- onClick: () => X(-1),
340
- disabled: G <= 0,
357
+ onClick: () => Z(-1),
358
+ disabled: K <= 0,
341
359
  children: [
342
- /* @__PURE__ */ S(pe, { className: "mr-1 h-4 w-4" }),
360
+ /* @__PURE__ */ x(me, { className: "mr-1 h-4 w-4" }),
343
361
  " ",
344
- b("buildmyiq.lessonPlayer.previous", "Previous")
362
+ y("buildmyiq.lessonPlayer.previous", "Previous")
345
363
  ]
346
364
  }),
347
- q ? /* @__PURE__ */ C("span", {
365
+ J ? /* @__PURE__ */ S("span", {
348
366
  className: "inline-flex items-center justify-center gap-2 rounded-lg bg-status-success-bg-subtle px-4 py-2 text-sm font-medium text-status-success-text max-sm:w-full",
349
367
  children: [
350
- /* @__PURE__ */ S(p, { className: "h-4 w-4" }),
368
+ /* @__PURE__ */ x(f, { className: "h-4 w-4" }),
351
369
  " ",
352
- b("buildmyiq.lessonPlayer.lessonComplete", "Lesson complete")
370
+ y("buildmyiq.lessonPlayer.lessonComplete", "Lesson complete")
353
371
  ]
354
- }) : /* @__PURE__ */ C(w, {
372
+ }) : /* @__PURE__ */ S(C, {
355
373
  size: "lg",
356
374
  className: "max-sm:w-full",
357
- onClick: Z,
358
- disabled: N.isPending,
359
- children: [N.isPending ? /* @__PURE__ */ S(g, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ S(p, { className: "mr-1.5 h-4 w-4" }), N.isPending ? b("buildmyiq.lessonPlayer.markingComplete", "Marking complete…") : b("buildmyiq.lessonPlayer.markComplete", "Mark complete")]
375
+ onClick: Ce,
376
+ disabled: M.isPending,
377
+ children: [M.isPending ? /* @__PURE__ */ x(h, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ x(f, { className: "mr-1.5 h-4 w-4" }), M.isPending ? y("buildmyiq.lessonPlayer.markingComplete", "Marking complete…") : y("buildmyiq.lessonPlayer.markComplete", "Mark complete")]
360
378
  }),
361
- /* @__PURE__ */ C(w, {
379
+ /* @__PURE__ */ S(C, {
362
380
  variant: "outline",
363
381
  size: "sm",
364
382
  className: "max-sm:w-full",
365
- onClick: () => X(1),
366
- disabled: G >= H.length - 1,
383
+ onClick: () => Z(1),
384
+ disabled: K >= U.length - 1,
367
385
  children: [
368
- b("buildmyiq.lessonPlayer.next", "Next"),
386
+ y("buildmyiq.lessonPlayer.next", "Next"),
369
387
  " ",
370
- /* @__PURE__ */ S(me, { className: "ml-1 h-4 w-4" })
388
+ /* @__PURE__ */ x(he, { className: "ml-1 h-4 w-4" })
371
389
  ]
372
390
  })
373
391
  ]
374
392
  })
375
393
  ]
376
- }) : /* @__PURE__ */ S(s, {
394
+ }) : /* @__PURE__ */ x(s, {
377
395
  illustration: "empty-data",
378
- title: b("buildmyiq.lessonPlayer.noLessonTitle", "No lesson to show"),
379
- description: b("buildmyiq.lessonPlayer.noLessonDescription", "This course doesn't have any lessons yet. Check back once the author adds content.")
380
- }), K && A && h && /* @__PURE__ */ S("div", {
396
+ title: y("buildmyiq.lessonPlayer.noLessonTitle", "No lesson to show"),
397
+ description: y("buildmyiq.lessonPlayer.noLessonDescription", "This course doesn't have any lessons yet. Check back once the author adds content.")
398
+ }), q && k && m && /* @__PURE__ */ x("div", {
381
399
  className: "mt-5",
382
- children: /* @__PURE__ */ S(oe, {
383
- courseId: A,
384
- enrollmentId: h,
385
- lessonId: K.id,
386
- lessonTitle: K.title,
387
- lessonTitlesById: ye
388
- }, A)
400
+ children: /* @__PURE__ */ x(se, {
401
+ courseId: k,
402
+ enrollmentId: m,
403
+ lessonId: q.id,
404
+ lessonTitle: q.title,
405
+ lessonTitlesById: be
406
+ }, k)
389
407
  })]
390
408
  })]
391
409
  })
@@ -394,6 +412,6 @@ var E = {
394
412
  });
395
413
  };
396
414
  //#endregion
397
- export { O as LessonPlayerPage };
415
+ export { D as LessonPlayerPage };
398
416
 
399
417
  //# sourceMappingURL=LessonPlayerPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"LessonPlayerPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"sourcesContent":["/**\n * LessonPlayerPage — the two-pane lesson player for a single enrollment.\n * @module buildmyiq/pages/learning\n *\n * Left pane: the course's module/lesson tree (from `buiCourse`) rendered as a\n * completion timeline driven by the enrollment's `lessonProgress`. Right pane:\n * the selected lesson (title, type, content text rendered safely, and a\n * `FileLink` for any attached media) with Prev / Mark-complete / Next controls.\n * Opening a lesson records IN_PROGRESS once; \"Mark complete\" records COMPLETED;\n * finishing every lesson transitions the enrollment to COMPLETED. All data is\n * live — no fabrication.\n */\n\nimport type { FC } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useParams, useSearchParams } from 'react-router';\nimport type { LucideIcon } from 'lucide-react';\nimport {\n ArrowLeft,\n BookOpen,\n Check,\n CheckCircle2,\n ChevronLeft,\n ChevronRight,\n FileText,\n HelpCircle,\n Loader2,\n ListChecks,\n MousePointerClick,\n PenLine,\n PlayCircle,\n Radio,\n Trophy,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n MiniBar,\n ProgressRing,\n LoadingView,\n ErrorView,\n EmptyState,\n FileLink,\n OfflineLessonControl,\n TutorPanel,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiMyEnrollment,\n useBuiCourse,\n useBuiRecordMyLessonProgress,\n useBuiCompleteMyEnrollment,\n} from '../../hooks';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { cn } from '../../utils/cn';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiLesson, BuiLessonType, BuiModule, BuiProgressStatus } from '../../types';\n\ninterface LessonGroup {\n id: string;\n title: string;\n lessons: BuiLesson[];\n}\n\nconst LESSON_ICON: Record<BuiLessonType, LucideIcon> = {\n ARTICLE: FileText,\n VIDEO: PlayCircle,\n QUIZ: HelpCircle,\n ASSIGNMENT: PenLine,\n INTERACTIVE: MousePointerClick,\n LIVE_SESSION: Radio,\n};\n\nconst sortByPosition = <T extends { position: number }>(items: T[]): T[] =>\n [...items].sort((a, b) => a.position - b.position);\n\nexport const LessonPlayerPage: FC = () => {\n const { enrollmentId } = useParams<{ enrollmentId: string }>();\n // `?lesson=<id>` deep-links straight to one lesson (used by the \"what's next\"\n // suggestion on My Learning). Purely an INITIAL anchor — once the learner\n // picks a lesson themselves, their pick wins.\n const [searchParams] = useSearchParams();\n const requestedLessonId = searchParams.get('lesson');\n const { navigateTo } = useBuildmyiq();\n const tr = useTr();\n\n const enrollmentQuery = useBuiMyEnrollment(enrollmentId);\n const enrollment = enrollmentQuery.data ?? null;\n const courseId = enrollment?.courseId;\n const courseQuery = useBuiCourse(courseId);\n const course = courseQuery.data ?? null;\n\n const recordProgress = useBuiRecordMyLessonProgress();\n const completeEnrollment = useBuiCompleteMyEnrollment();\n\n // react-query's mutate is referentially stable across renders, so it can be\n // used directly in effects without a render-time ref write.\n const recordMutate = recordProgress.mutate;\n const autoRecordedRef = useRef<Set<string>>(new Set());\n\n const [userSelectedId, setUserSelectedId] = useState<string | null>(null);\n const [showTree, setShowTree] = useState(false);\n\n // Grouped tree (modules → lessons) plus any standalone lessons, de-duplicated.\n const groups = useMemo<LessonGroup[]>(() => {\n if (!course) return [];\n const seen = new Set<string>();\n const modules: BuiModule[] = sortByPosition(course.modules ?? []);\n const result: LessonGroup[] = [];\n for (const m of modules) {\n const lessons = sortByPosition(m.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (lessons.length > 0) result.push({ id: m.id, title: m.title, lessons });\n }\n const standalone = sortByPosition(course.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (standalone.length > 0) {\n result.push({\n id: '__standalone',\n title: tr('common.lessons', 'Lessons'),\n lessons: standalone,\n });\n }\n return result;\n }, [course, tr]);\n\n // Flat, ordered lesson list for prev/next + completion checks.\n const orderedLessons = useMemo<BuiLesson[]>(() => groups.flatMap((g) => g.lessons), [groups]);\n // Lesson id → title for this course, so the tutor panel can NAME the lessons\n // its answers were grounded in instead of showing opaque ids.\n const lessonTitlesById = useMemo<ReadonlyMap<string, string>>(\n () => new Map(orderedLessons.map((lesson) => [lesson.id, lesson.title])),\n [orderedLessons]\n );\n\n const progressByLesson = useMemo(() => {\n const map = new Map<string, BuiProgressStatus>();\n for (const p of enrollment?.lessonProgress ?? []) map.set(p.lessonId, p.status);\n return map;\n }, [enrollment]);\n\n // Resolve the active lesson as DERIVED state: the user's pick if it still\n // exists in the course, otherwise the first lesson. Avoids a setState-in-\n // effect re-anchor loop.\n const selectedLessonId = useMemo<string | null>(() => {\n if (orderedLessons.length === 0) return null;\n if (userSelectedId && orderedLessons.some((l) => l.id === userSelectedId)) {\n return userSelectedId;\n }\n if (requestedLessonId && orderedLessons.some((l) => l.id === requestedLessonId)) {\n return requestedLessonId;\n }\n return orderedLessons[0].id;\n }, [orderedLessons, userSelectedId, requestedLessonId]);\n\n // Record IN_PROGRESS once when a not-yet-started lesson is opened.\n useEffect(() => {\n if (!enrollmentId || !selectedLessonId) return;\n const status = progressByLesson.get(selectedLessonId);\n if (status === 'COMPLETED' || status === 'IN_PROGRESS') return;\n if (autoRecordedRef.current.has(selectedLessonId)) return;\n autoRecordedRef.current.add(selectedLessonId);\n recordMutate({\n enrollmentId,\n lessonId: selectedLessonId,\n status: 'IN_PROGRESS',\n });\n }, [enrollmentId, selectedLessonId, progressByLesson, recordMutate]);\n\n const selectedIndex = orderedLessons.findIndex((l) => l.id === selectedLessonId);\n const selectedLesson = selectedIndex >= 0 ? orderedLessons[selectedIndex] : null;\n const selectedStatus = selectedLesson ? progressByLesson.get(selectedLesson.id) : undefined;\n const isSelectedComplete = selectedStatus === 'COMPLETED';\n\n const completedCount = orderedLessons.filter(\n (l) => progressByLesson.get(l.id) === 'COMPLETED'\n ).length;\n const allComplete = orderedLessons.length > 0 && completedCount === orderedLessons.length;\n const alreadyFinished = enrollment?.status === 'COMPLETED';\n\n const handleSelect = (lessonId: string) => {\n setUserSelectedId(lessonId);\n setShowTree(false);\n };\n\n const goTo = (offset: number) => {\n if (selectedIndex < 0) return;\n const next = orderedLessons[selectedIndex + offset];\n if (next) handleSelect(next.id);\n };\n\n const handleMarkComplete = async () => {\n if (!enrollmentId || !selectedLesson) return;\n await recordProgress.mutateAsync({\n enrollmentId,\n lessonId: selectedLesson.id,\n status: 'COMPLETED',\n });\n // Advance to the next incomplete lesson for a natural flow.\n const next = orderedLessons[selectedIndex + 1];\n if (next) setUserSelectedId(next.id);\n };\n\n const handleFinishCourse = async () => {\n if (!enrollmentId) return;\n await completeEnrollment.mutateAsync({ id: enrollmentId });\n };\n\n // ---- top-level states ----------------------------------------------------\n if (!enrollmentId) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.noEnrollmentTitle', 'No enrollment selected')}\n description={tr(\n 'buildmyiq.lessonPlayer.noEnrollmentDescription',\n 'Open a course from My Learning to start the lesson player.'\n )}\n action={\n <Button onClick={() => navigateTo('learning')}>\n {tr('buildmyiq.lessonPlayer.backToLearning', 'Back to My Learning')}\n </Button>\n }\n />\n </PageLayout>\n );\n }\n\n const isLoading = enrollmentQuery.isLoading || (Boolean(courseId) && courseQuery.isLoading);\n\n const backAction = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('learning')}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" /> {tr('buildmyiq.nav.learning', 'My Learning')}\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <LoadingView label={tr('buildmyiq.lessonPlayer.loading', 'Loading your lesson…')} />\n </PageLayout>\n );\n }\n\n if (enrollmentQuery.isError) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <ErrorView\n title={tr('buildmyiq.lessonPlayer.enrollmentErrorTitle', \"Couldn't load this enrollment\")}\n message={\n enrollmentQuery.error instanceof Error ? enrollmentQuery.error.message : undefined\n }\n onRetry={() => enrollmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!enrollment) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.enrollmentNotFoundTitle', 'Enrollment not found')}\n description={tr(\n 'buildmyiq.lessonPlayer.enrollmentNotFoundDescription',\n 'This enrollment may have been removed or is not available to you.'\n )}\n action={\n <Button onClick={() => navigateTo('learning')}>\n {tr('buildmyiq.lessonPlayer.backToLearning', 'Back to My Learning')}\n </Button>\n }\n />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <ErrorView\n title={tr('buildmyiq.lessonPlayer.courseErrorTitle', \"Couldn't load the course\")}\n message={courseQuery.error instanceof Error ? courseQuery.error.message : undefined}\n onRetry={() => courseQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n const progressPct = Math.round(enrollment.progressPct ?? 0);\n const SelectedIcon = selectedLesson ? LESSON_ICON[selectedLesson.type] : FileText;\n\n return (\n <PageLayout\n title={course?.title ?? tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')}\n actions={backAction}\n >\n <div className=\"flex flex-col gap-5\">\n {/* Header progress — ProgressRing hero + completion bar */}\n <GlassCard\n treatment=\"glass\"\n glow={progressPct > 0}\n className=\"flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:gap-5\"\n >\n <ProgressRing value={progressPct} size={64} className=\"shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={enrollment.status} />\n <span className=\"text-sm text-text-secondary tabular-nums\">\n {tr(\n 'buildmyiq.lessonPlayer.progressSummary',\n '{{completed}} of {{total}} lessons complete',\n { completed: completedCount, total: orderedLessons.length }\n )}\n </span>\n </div>\n <MiniBar value={progressPct} className=\"mt-3\" />\n </div>\n {allComplete && !alreadyFinished && (\n <Button\n className=\"shrink-0 max-sm:w-full\"\n onClick={handleFinishCourse}\n disabled={completeEnrollment.isPending}\n >\n {completeEnrollment.isPending ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <Trophy className=\"mr-1 h-4 w-4\" />\n )}\n {tr('buildmyiq.lessonPlayer.finishCourse', 'Finish course')}\n </Button>\n )}\n </GlassCard>\n\n {/* Mobile tree toggle — collapses the outline into a top sheet */}\n <Button\n variant=\"secondary\"\n size=\"sm\"\n className=\"w-full lg:hidden\"\n onClick={() => setShowTree((v) => !v)}\n >\n <ListChecks className=\"mr-1 h-4 w-4\" />\n {showTree\n ? tr('buildmyiq.lessonPlayer.hideLessons', 'Hide lessons')\n : tr('buildmyiq.lessonPlayer.showLessons', 'Lessons ({{completed}}/{{total}})', {\n completed: completedCount,\n total: orderedLessons.length,\n })}\n </Button>\n\n <div className=\"flex flex-col gap-5 lg:flex-row\">\n {/* Left: module / lesson completion timeline */}\n <aside className={cn('lg:block lg:w-80 lg:shrink-0', showTree ? 'block' : 'hidden')}>\n <GlassCard treatment=\"plain\" className=\"overflow-hidden p-0 lg:sticky lg:top-4\">\n {orderedLessons.length === 0 ? (\n <div className=\"p-4 text-sm text-text-secondary\">\n {tr('buildmyiq.lessonPlayer.noLessons', 'This course has no lessons yet.')}\n </div>\n ) : (\n <>\n <nav className=\"max-h-[68vh] overflow-auto p-3\">\n {groups.map((group) => {\n const groupDone = group.lessons.filter(\n (l) => progressByLesson.get(l.id) === 'COMPLETED'\n ).length;\n return (\n <div key={group.id} className=\"mb-4 last:mb-0\">\n <div className=\"mb-2 flex items-center justify-between gap-2 px-1\">\n <p className=\"min-w-0 truncate text-sm font-semibold text-text-primary\">\n {group.title}\n </p>\n <span className=\"shrink-0 text-xs font-medium tabular-nums text-text-secondary\">\n {groupDone} / {group.lessons.length}\n </span>\n </div>\n <ul className=\"flex flex-col\">\n {group.lessons.map((lesson, i) => {\n const status = progressByLesson.get(lesson.id);\n const done = status === 'COMPLETED';\n const active = lesson.id === selectedLessonId;\n const isLast = i === group.lessons.length - 1;\n const Icon = LESSON_ICON[lesson.type];\n return (\n <li key={lesson.id} className=\"flex gap-3\">\n {/* Timeline node + connector */}\n <div className=\"flex flex-col items-center pt-1.5\">\n {done ? (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-status-success-text)]\">\n <Check className=\"h-3.5 w-3.5 text-status-success-bg-subtle\" />\n </span>\n ) : active ? (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] ring-2 ring-[var(--color-accent-teal)]\">\n <Icon className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n </span>\n ) : (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border-seam bg-bg-sunken\">\n <Icon className=\"h-3.5 w-3.5 text-text-secondary opacity-70\" />\n </span>\n )}\n {!isLast && (\n <span\n className={cn(\n 'my-1 w-px flex-1',\n done\n ? 'bg-[color-mix(in_srgb,var(--color-status-success-text)_45%,transparent)]'\n : 'bg-border-seam'\n )}\n />\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => handleSelect(lesson.id)}\n className={cn(\n 'mb-1 min-w-0 flex-1 rounded-lg px-3 py-2 text-left transition-colors',\n active\n ? 'bg-[var(--color-accent-soft)]'\n : 'hover:bg-bg-sunken'\n )}\n aria-current={active ? 'true' : undefined}\n >\n <span\n className={cn(\n 'block truncate text-sm',\n active\n ? 'font-medium text-text-primary'\n : done\n ? 'text-text-secondary'\n : 'text-text-primary'\n )}\n >\n {lesson.title}\n </span>\n <span className=\"mt-0.5 block truncate text-xs text-text-secondary\">\n {formatEnumLabel(lesson.type)}\n {lesson.durationMin > 0\n ? ` · ${formatDuration(lesson.durationMin)}`\n : ''}\n </span>\n </button>\n </li>\n );\n })}\n </ul>\n </div>\n );\n })}\n </nav>\n {courseId && (\n <div className=\"border-t border-border-seam p-3\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => navigateTo(`catalog/${courseId}`)}\n >\n <BookOpen className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('buildmyiq.lessonPlayer.courseOverview', 'Course overview')}\n </Button>\n </div>\n )}\n </>\n )}\n </GlassCard>\n </aside>\n\n {/* Right: selected lesson */}\n <main className=\"min-w-0 flex-1\">\n {!selectedLesson ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.noLessonTitle', 'No lesson to show')}\n description={tr(\n 'buildmyiq.lessonPlayer.noLessonDescription',\n \"This course doesn't have any lessons yet. Check back once the author adds content.\"\n )}\n />\n ) : (\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-5 sm:p-7\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-[var(--color-accent-soft)] px-2.5 py-1 text-xs font-medium text-text-primary\">\n <SelectedIcon className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n {formatEnumLabel(selectedLesson.type)}\n </span>\n {selectedLesson.durationMin > 0 && (\n <span className=\"text-xs text-text-secondary\">\n {formatDuration(selectedLesson.durationMin)}\n </span>\n )}\n {isSelectedComplete && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" />{' '}\n {tr('buildmyiq.lessonPlayer.completedLabel', 'Completed')}\n </span>\n )}\n <OfflineLessonControl key={selectedLesson.id} lesson={selectedLesson} />\n <span className=\"ml-auto text-xs tabular-nums text-text-secondary\">\n {tr('buildmyiq.lessonPlayer.lessonPosition', 'Lesson {{n}} of {{total}}', {\n n: selectedIndex + 1,\n total: orderedLessons.length,\n })}\n </span>\n </div>\n\n <h2 className=\"text-2xl font-semibold tracking-tight text-text-primary\">\n {selectedLesson.title}\n </h2>\n\n {selectedLesson.media ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken p-3\">\n <p className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n {tr('buildmyiq.lessonPlayer.resources', 'Resources')}\n </p>\n <FileLink fileId={selectedLesson.media.id} />\n </div>\n ) : null}\n\n {selectedLesson.content ? (\n <p className=\"max-w-prose whitespace-pre-wrap text-[15px] leading-7 text-text-primary\">\n {selectedLesson.content}\n </p>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {selectedLesson.media\n ? tr(\n 'buildmyiq.lessonPlayer.noContentWithMedia',\n 'This lesson has no written content beyond the attached media.'\n )\n : tr(\n 'buildmyiq.lessonPlayer.noContent',\n 'This lesson has no written content'\n )}\n </p>\n )}\n\n {/* Actions — Prev · Mark complete · Next */}\n <div className=\"mt-2 flex flex-col gap-3 border-t border-border-seam pt-5 sm:flex-row sm:items-center sm:justify-between\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={() => goTo(-1)}\n disabled={selectedIndex <= 0}\n >\n <ChevronLeft className=\"mr-1 h-4 w-4\" />{' '}\n {tr('buildmyiq.lessonPlayer.previous', 'Previous')}\n </Button>\n\n {isSelectedComplete ? (\n <span className=\"inline-flex items-center justify-center gap-2 rounded-lg bg-status-success-bg-subtle px-4 py-2 text-sm font-medium text-status-success-text max-sm:w-full\">\n <CheckCircle2 className=\"h-4 w-4\" />{' '}\n {tr('buildmyiq.lessonPlayer.lessonComplete', 'Lesson complete')}\n </span>\n ) : (\n <Button\n size=\"lg\"\n className=\"max-sm:w-full\"\n onClick={handleMarkComplete}\n disabled={recordProgress.isPending}\n >\n {recordProgress.isPending ? (\n <Loader2 className=\"mr-1.5 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"mr-1.5 h-4 w-4\" />\n )}\n {recordProgress.isPending\n ? tr('buildmyiq.lessonPlayer.markingComplete', 'Marking complete…')\n : tr('buildmyiq.lessonPlayer.markComplete', 'Mark complete')}\n </Button>\n )}\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={() => goTo(1)}\n disabled={selectedIndex >= orderedLessons.length - 1}\n >\n {tr('buildmyiq.lessonPlayer.next', 'Next')}{' '}\n <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </GlassCard>\n )}\n\n {selectedLesson && courseId && enrollmentId && (\n <div className=\"mt-5\">\n {/* Course changes reset local state; lesson navigation keeps the course conversation. */}\n <TutorPanel\n key={courseId}\n courseId={courseId}\n enrollmentId={enrollmentId}\n lessonId={selectedLesson.id}\n lessonTitle={selectedLesson.title}\n lessonTitlesById={lessonTitlesById}\n />\n </div>\n )}\n </main>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"LessonPlayerPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"sourcesContent":["/**\n * LessonPlayerPage — the two-pane lesson player for a single enrollment.\n * @module buildmyiq/pages/learning\n *\n * Left pane: the course's module/lesson tree (from `buiCourse`) rendered as a\n * completion timeline driven by the enrollment's `lessonProgress`. Right pane:\n * the selected lesson (title, type, content text rendered safely, and a\n * `FileLink` for any attached media) with Prev / Mark-complete / Next controls.\n * Opening a lesson records IN_PROGRESS once; \"Mark complete\" records COMPLETED;\n * finishing every lesson transitions the enrollment to COMPLETED. All data is\n * live — no fabrication.\n */\n\nimport type { FC } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useParams, useSearchParams } from 'react-router';\nimport type { LucideIcon } from 'lucide-react';\nimport {\n ArrowLeft,\n BookOpen,\n Check,\n CheckCircle2,\n ChevronLeft,\n ChevronRight,\n FileText,\n HelpCircle,\n Loader2,\n ListChecks,\n MousePointerClick,\n PenLine,\n PlayCircle,\n Radio,\n Trophy,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n MiniBar,\n ProgressRing,\n LoadingView,\n ErrorView,\n EmptyState,\n FileLink,\n OfflineLessonControl,\n TutorPanel,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiMyEnrollment,\n useBuiCourse,\n useBuiRecordMyLessonProgress,\n useBuiCompleteMyEnrollment,\n} from '../../hooks';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { cn } from '../../utils/cn';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiLesson, BuiLessonType, BuiModule, BuiProgressStatus } from '../../types';\n\ninterface LessonGroup {\n id: string;\n title: string;\n lessons: BuiLesson[];\n}\n\nconst LESSON_ICON: Record<BuiLessonType, LucideIcon> = {\n ARTICLE: FileText,\n VIDEO: PlayCircle,\n QUIZ: HelpCircle,\n ASSIGNMENT: PenLine,\n INTERACTIVE: MousePointerClick,\n LIVE_SESSION: Radio,\n};\n\nconst sortByPosition = <T extends { position: number }>(items: T[]): T[] =>\n [...items].sort((a, b) => a.position - b.position);\n\nexport const LessonPlayerPage: FC = () => {\n const { enrollmentId } = useParams<{ enrollmentId: string }>();\n // `?lesson=<id>` deep-links straight to one lesson (used by the \"what's next\"\n // suggestion on My Learning). Purely an INITIAL anchor — once the learner\n // picks a lesson themselves, their pick wins.\n const [searchParams] = useSearchParams();\n const requestedLessonId = searchParams.get('lesson');\n const { navigateTo } = useBuildmyiq();\n const tr = useTr();\n\n const enrollmentQuery = useBuiMyEnrollment(enrollmentId);\n const enrollment = enrollmentQuery.data ?? null;\n const courseId = enrollment?.courseId;\n const courseQuery = useBuiCourse(courseId);\n const course = courseQuery.data ?? null;\n\n const recordProgress = useBuiRecordMyLessonProgress();\n const completeEnrollment = useBuiCompleteMyEnrollment();\n\n // react-query's mutate is referentially stable across renders, so it can be\n // used directly in effects without a render-time ref write.\n const recordMutate = recordProgress.mutate;\n const autoRecordedRef = useRef<Set<string>>(new Set());\n\n const [userSelectedId, setUserSelectedId] = useState<string | null>(null);\n const [showTree, setShowTree] = useState(false);\n // Error from the last \"Mark complete\" / \"Finish course\" attempt. Without it a\n // rejected mutation surfaced only as an unhandled promise rejection and the\n // button silently did nothing.\n const [actionError, setActionError] = useState<string | null>(null);\n\n // Grouped tree (modules → lessons) plus any standalone lessons, de-duplicated.\n const groups = useMemo<LessonGroup[]>(() => {\n if (!course) return [];\n const seen = new Set<string>();\n const modules: BuiModule[] = sortByPosition(course.modules ?? []);\n const result: LessonGroup[] = [];\n for (const m of modules) {\n const lessons = sortByPosition(m.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (lessons.length > 0) result.push({ id: m.id, title: m.title, lessons });\n }\n const standalone = sortByPosition(course.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (standalone.length > 0) {\n result.push({\n id: '__standalone',\n title: tr('common.lessons', 'Lessons'),\n lessons: standalone,\n });\n }\n return result;\n }, [course, tr]);\n\n // Flat, ordered lesson list for prev/next + completion checks.\n const orderedLessons = useMemo<BuiLesson[]>(() => groups.flatMap((g) => g.lessons), [groups]);\n // Lesson id → title for this course, so the tutor panel can NAME the lessons\n // its answers were grounded in instead of showing opaque ids.\n const lessonTitlesById = useMemo<ReadonlyMap<string, string>>(\n () => new Map(orderedLessons.map((lesson) => [lesson.id, lesson.title])),\n [orderedLessons]\n );\n\n const progressByLesson = useMemo(() => {\n const map = new Map<string, BuiProgressStatus>();\n for (const p of enrollment?.lessonProgress ?? []) map.set(p.lessonId, p.status);\n return map;\n }, [enrollment]);\n\n // Resolve the active lesson as DERIVED state: the user's pick if it still\n // exists in the course, otherwise the first lesson. Avoids a setState-in-\n // effect re-anchor loop.\n const selectedLessonId = useMemo<string | null>(() => {\n if (orderedLessons.length === 0) return null;\n if (userSelectedId && orderedLessons.some((l) => l.id === userSelectedId)) {\n return userSelectedId;\n }\n if (requestedLessonId && orderedLessons.some((l) => l.id === requestedLessonId)) {\n return requestedLessonId;\n }\n return orderedLessons[0].id;\n }, [orderedLessons, userSelectedId, requestedLessonId]);\n\n // Record IN_PROGRESS once when a not-yet-started lesson is opened.\n useEffect(() => {\n if (!enrollmentId || !selectedLessonId) return;\n const status = progressByLesson.get(selectedLessonId);\n if (status === 'COMPLETED' || status === 'IN_PROGRESS') return;\n if (autoRecordedRef.current.has(selectedLessonId)) return;\n autoRecordedRef.current.add(selectedLessonId);\n recordMutate({\n enrollmentId,\n lessonId: selectedLessonId,\n status: 'IN_PROGRESS',\n });\n }, [enrollmentId, selectedLessonId, progressByLesson, recordMutate]);\n\n const selectedIndex = orderedLessons.findIndex((l) => l.id === selectedLessonId);\n const selectedLesson = selectedIndex >= 0 ? orderedLessons[selectedIndex] : null;\n const selectedStatus = selectedLesson ? progressByLesson.get(selectedLesson.id) : undefined;\n const isSelectedComplete = selectedStatus === 'COMPLETED';\n\n const completedCount = orderedLessons.filter(\n (l) => progressByLesson.get(l.id) === 'COMPLETED'\n ).length;\n const allComplete = orderedLessons.length > 0 && completedCount === orderedLessons.length;\n const alreadyFinished = enrollment?.status === 'COMPLETED';\n\n const handleSelect = (lessonId: string) => {\n setUserSelectedId(lessonId);\n setShowTree(false);\n setActionError(null);\n };\n\n const goTo = (offset: number) => {\n if (selectedIndex < 0) return;\n const next = orderedLessons[selectedIndex + offset];\n if (next) handleSelect(next.id);\n };\n\n const handleMarkComplete = async () => {\n if (!enrollmentId || !selectedLesson) return;\n setActionError(null);\n try {\n await recordProgress.mutateAsync({\n enrollmentId,\n lessonId: selectedLesson.id,\n status: 'COMPLETED',\n });\n } catch (err) {\n setActionError(\n err instanceof Error && err.message\n ? err.message\n : tr('buildmyiq.lessonPlayer.markCompleteError', 'Could not mark this lesson complete.')\n );\n return;\n }\n // Advance to the next incomplete lesson for a natural flow.\n const next = orderedLessons[selectedIndex + 1];\n if (next) setUserSelectedId(next.id);\n };\n\n const handleFinishCourse = async () => {\n if (!enrollmentId) return;\n setActionError(null);\n try {\n await completeEnrollment.mutateAsync({ id: enrollmentId });\n } catch (err) {\n setActionError(\n err instanceof Error && err.message\n ? err.message\n : tr('buildmyiq.lessonPlayer.finishCourseError', 'Could not finish this course.')\n );\n }\n };\n\n // ---- top-level states ----------------------------------------------------\n if (!enrollmentId) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.noEnrollmentTitle', 'No enrollment selected')}\n description={tr(\n 'buildmyiq.lessonPlayer.noEnrollmentDescription',\n 'Open a course from My Learning to start the lesson player.'\n )}\n action={\n <Button onClick={() => navigateTo('learning')}>\n {tr('buildmyiq.lessonPlayer.backToLearning', 'Back to My Learning')}\n </Button>\n }\n />\n </PageLayout>\n );\n }\n\n const isLoading = enrollmentQuery.isLoading || (Boolean(courseId) && courseQuery.isLoading);\n\n const backAction = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('learning')}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" /> {tr('buildmyiq.nav.learning', 'My Learning')}\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <LoadingView label={tr('buildmyiq.lessonPlayer.loading', 'Loading your lesson…')} />\n </PageLayout>\n );\n }\n\n if (enrollmentQuery.isError) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <ErrorView\n title={tr('buildmyiq.lessonPlayer.enrollmentErrorTitle', \"Couldn't load this enrollment\")}\n message={\n enrollmentQuery.error instanceof Error ? enrollmentQuery.error.message : undefined\n }\n onRetry={() => enrollmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!enrollment) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.enrollmentNotFoundTitle', 'Enrollment not found')}\n description={tr(\n 'buildmyiq.lessonPlayer.enrollmentNotFoundDescription',\n 'This enrollment may have been removed or is not available to you.'\n )}\n action={\n <Button onClick={() => navigateTo('learning')}>\n {tr('buildmyiq.lessonPlayer.backToLearning', 'Back to My Learning')}\n </Button>\n }\n />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <ErrorView\n title={tr('buildmyiq.lessonPlayer.courseErrorTitle', \"Couldn't load the course\")}\n message={courseQuery.error instanceof Error ? courseQuery.error.message : undefined}\n onRetry={() => courseQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n const progressPct = Math.round(enrollment.progressPct ?? 0);\n const SelectedIcon = selectedLesson ? LESSON_ICON[selectedLesson.type] : FileText;\n\n return (\n <PageLayout\n title={course?.title ?? tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')}\n actions={backAction}\n >\n <div className=\"flex flex-col gap-5\">\n {/* Header progress — ProgressRing hero + completion bar */}\n <GlassCard\n treatment=\"glass\"\n glow={progressPct > 0}\n className=\"flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:gap-5\"\n >\n <ProgressRing value={progressPct} size={64} className=\"shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={enrollment.status} />\n <span className=\"text-sm text-text-secondary tabular-nums\">\n {tr(\n 'buildmyiq.lessonPlayer.progressSummary',\n '{{completed}} of {{total}} lessons complete',\n { completed: completedCount, total: orderedLessons.length }\n )}\n </span>\n </div>\n <MiniBar value={progressPct} className=\"mt-3\" />\n </div>\n {allComplete && !alreadyFinished && (\n <Button\n className=\"shrink-0 max-sm:w-full\"\n onClick={handleFinishCourse}\n disabled={completeEnrollment.isPending}\n >\n {completeEnrollment.isPending ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <Trophy className=\"mr-1 h-4 w-4\" />\n )}\n {tr('buildmyiq.lessonPlayer.finishCourse', 'Finish course')}\n </Button>\n )}\n </GlassCard>\n\n {actionError && (\n <p role=\"alert\" className=\"text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n {/* Mobile tree toggle — collapses the outline into a top sheet */}\n <Button\n variant=\"secondary\"\n size=\"sm\"\n className=\"w-full lg:hidden\"\n onClick={() => setShowTree((v) => !v)}\n >\n <ListChecks className=\"mr-1 h-4 w-4\" />\n {showTree\n ? tr('buildmyiq.lessonPlayer.hideLessons', 'Hide lessons')\n : tr('buildmyiq.lessonPlayer.showLessons', 'Lessons ({{completed}}/{{total}})', {\n completed: completedCount,\n total: orderedLessons.length,\n })}\n </Button>\n\n <div className=\"flex flex-col gap-5 lg:flex-row\">\n {/* Left: module / lesson completion timeline */}\n <aside className={cn('lg:block lg:w-80 lg:shrink-0', showTree ? 'block' : 'hidden')}>\n <GlassCard treatment=\"plain\" className=\"overflow-hidden p-0 lg:sticky lg:top-4\">\n {orderedLessons.length === 0 ? (\n <div className=\"p-4 text-sm text-text-secondary\">\n {tr('buildmyiq.lessonPlayer.noLessons', 'This course has no lessons yet.')}\n </div>\n ) : (\n <>\n <nav className=\"max-h-[68vh] overflow-auto p-3\">\n {groups.map((group) => {\n const groupDone = group.lessons.filter(\n (l) => progressByLesson.get(l.id) === 'COMPLETED'\n ).length;\n return (\n <div key={group.id} className=\"mb-4 last:mb-0\">\n <div className=\"mb-2 flex items-center justify-between gap-2 px-1\">\n <p className=\"min-w-0 truncate text-sm font-semibold text-text-primary\">\n {group.title}\n </p>\n <span className=\"shrink-0 text-xs font-medium tabular-nums text-text-secondary\">\n {groupDone} / {group.lessons.length}\n </span>\n </div>\n <ul className=\"flex flex-col\">\n {group.lessons.map((lesson, i) => {\n const status = progressByLesson.get(lesson.id);\n const done = status === 'COMPLETED';\n const active = lesson.id === selectedLessonId;\n const isLast = i === group.lessons.length - 1;\n const Icon = LESSON_ICON[lesson.type];\n return (\n <li key={lesson.id} className=\"flex gap-3\">\n {/* Timeline node + connector */}\n <div className=\"flex flex-col items-center pt-1.5\">\n {done ? (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-status-success-text)]\">\n <Check className=\"h-3.5 w-3.5 text-status-success-bg-subtle\" />\n </span>\n ) : active ? (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] ring-2 ring-[var(--color-accent-teal)]\">\n <Icon className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n </span>\n ) : (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border-seam bg-bg-sunken\">\n <Icon className=\"h-3.5 w-3.5 text-text-secondary opacity-70\" />\n </span>\n )}\n {!isLast && (\n <span\n className={cn(\n 'my-1 w-px flex-1',\n done\n ? 'bg-[color-mix(in_srgb,var(--color-status-success-text)_45%,transparent)]'\n : 'bg-border-seam'\n )}\n />\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => handleSelect(lesson.id)}\n className={cn(\n 'mb-1 min-w-0 flex-1 rounded-lg px-3 py-2 text-left transition-colors',\n active\n ? 'bg-[var(--color-accent-soft)]'\n : 'hover:bg-bg-sunken'\n )}\n aria-current={active ? 'true' : undefined}\n >\n <span\n className={cn(\n 'block truncate text-sm',\n active\n ? 'font-medium text-text-primary'\n : done\n ? 'text-text-secondary'\n : 'text-text-primary'\n )}\n >\n {lesson.title}\n </span>\n <span className=\"mt-0.5 block truncate text-xs text-text-secondary\">\n {formatEnumLabel(lesson.type)}\n {lesson.durationMin > 0\n ? ` · ${formatDuration(lesson.durationMin)}`\n : ''}\n </span>\n </button>\n </li>\n );\n })}\n </ul>\n </div>\n );\n })}\n </nav>\n {courseId && (\n <div className=\"border-t border-border-seam p-3\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => navigateTo(`catalog/${courseId}`)}\n >\n <BookOpen className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('buildmyiq.lessonPlayer.courseOverview', 'Course overview')}\n </Button>\n </div>\n )}\n </>\n )}\n </GlassCard>\n </aside>\n\n {/* Right: selected lesson */}\n <main className=\"min-w-0 flex-1\">\n {!selectedLesson ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.noLessonTitle', 'No lesson to show')}\n description={tr(\n 'buildmyiq.lessonPlayer.noLessonDescription',\n \"This course doesn't have any lessons yet. Check back once the author adds content.\"\n )}\n />\n ) : (\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-5 sm:p-7\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-[var(--color-accent-soft)] px-2.5 py-1 text-xs font-medium text-text-primary\">\n <SelectedIcon className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n {formatEnumLabel(selectedLesson.type)}\n </span>\n {selectedLesson.durationMin > 0 && (\n <span className=\"text-xs text-text-secondary\">\n {formatDuration(selectedLesson.durationMin)}\n </span>\n )}\n {isSelectedComplete && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" />{' '}\n {tr('buildmyiq.lessonPlayer.completedLabel', 'Completed')}\n </span>\n )}\n <OfflineLessonControl key={selectedLesson.id} lesson={selectedLesson} />\n <span className=\"ml-auto text-xs tabular-nums text-text-secondary\">\n {tr('buildmyiq.lessonPlayer.lessonPosition', 'Lesson {{n}} of {{total}}', {\n n: selectedIndex + 1,\n total: orderedLessons.length,\n })}\n </span>\n </div>\n\n <h2 className=\"text-2xl font-semibold tracking-tight text-text-primary\">\n {selectedLesson.title}\n </h2>\n\n {selectedLesson.media ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken p-3\">\n <p className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n {tr('buildmyiq.lessonPlayer.resources', 'Resources')}\n </p>\n <FileLink fileId={selectedLesson.media.id} />\n </div>\n ) : null}\n\n {selectedLesson.content ? (\n <p className=\"max-w-prose whitespace-pre-wrap text-[15px] leading-7 text-text-primary\">\n {selectedLesson.content}\n </p>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {selectedLesson.media\n ? tr(\n 'buildmyiq.lessonPlayer.noContentWithMedia',\n 'This lesson has no written content beyond the attached media.'\n )\n : tr(\n 'buildmyiq.lessonPlayer.noContent',\n 'This lesson has no written content'\n )}\n </p>\n )}\n\n {/* Actions — Prev · Mark complete · Next */}\n <div className=\"mt-2 flex flex-col gap-3 border-t border-border-seam pt-5 sm:flex-row sm:items-center sm:justify-between\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={() => goTo(-1)}\n disabled={selectedIndex <= 0}\n >\n <ChevronLeft className=\"mr-1 h-4 w-4\" />{' '}\n {tr('buildmyiq.lessonPlayer.previous', 'Previous')}\n </Button>\n\n {isSelectedComplete ? (\n <span className=\"inline-flex items-center justify-center gap-2 rounded-lg bg-status-success-bg-subtle px-4 py-2 text-sm font-medium text-status-success-text max-sm:w-full\">\n <CheckCircle2 className=\"h-4 w-4\" />{' '}\n {tr('buildmyiq.lessonPlayer.lessonComplete', 'Lesson complete')}\n </span>\n ) : (\n <Button\n size=\"lg\"\n className=\"max-sm:w-full\"\n onClick={handleMarkComplete}\n disabled={recordProgress.isPending}\n >\n {recordProgress.isPending ? (\n <Loader2 className=\"mr-1.5 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"mr-1.5 h-4 w-4\" />\n )}\n {recordProgress.isPending\n ? tr('buildmyiq.lessonPlayer.markingComplete', 'Marking complete…')\n : tr('buildmyiq.lessonPlayer.markComplete', 'Mark complete')}\n </Button>\n )}\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={() => goTo(1)}\n disabled={selectedIndex >= orderedLessons.length - 1}\n >\n {tr('buildmyiq.lessonPlayer.next', 'Next')}{' '}\n <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </GlassCard>\n )}\n\n {selectedLesson && courseId && enrollmentId && (\n <div className=\"mt-5\">\n {/* Course changes reset local state; lesson navigation keeps the course conversation. */}\n <TutorPanel\n key={courseId}\n courseId={courseId}\n enrollmentId={enrollmentId}\n lessonId={selectedLesson.id}\n lessonTitle={selectedLesson.title}\n lessonTitlesById={lessonTitlesById}\n />\n </div>\n )}\n </main>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { CourseCard } from '../buildmyiq/pages/_shared/CourseCard';
3
+ declare const meta: Meta<typeof CourseCard>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof meta>;
6
+ export declare const Catalog: Story;
7
+ export declare const Authoring: Story;
8
+ export declare const SparseCourse: Story;
9
+ //# sourceMappingURL=CourseCard.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CourseCard.stories.d.ts","sourceRoot":"","sources":["../../src/stories/CourseCard.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAC;AAGnE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,UAAU,CAajC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAmBrB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAUvB,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,KAa1B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"EmptyState.stories.d.ts","sourceRoot":"","sources":["../../src/stories/EmptyState.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAEhE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,UAAU,CAMjC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,QAAQ,EAAE,KAMtB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KAexB,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAMzB,CAAC"}
1
+ {"version":3,"file":"EmptyState.stories.d.ts","sourceRoot":"","sources":["../../src/stories/EmptyState.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAEhE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,UAAU,CAMjC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,QAAQ,EAAE,KActB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KAoBxB,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAazB,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ type EnrollmentFilter = 'ALL' | 'IN_PROGRESS' | 'COMPLETED';
3
+ interface StatefulFilterTabsProps {
4
+ initial: EnrollmentFilter;
5
+ onChange: (value: EnrollmentFilter) => void;
6
+ }
7
+ /** FilterTabs is controlled; this wrapper owns the state like a real page does. */
8
+ declare function StatefulFilterTabs({ initial, onChange }: StatefulFilterTabsProps): import("react").JSX.Element;
9
+ declare const meta: Meta<typeof StatefulFilterTabs>;
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+ export declare const SwitchFilter: Story;
13
+ //# sourceMappingURL=FilterTabs.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FilterTabs.stories.d.ts","sourceRoot":"","sources":["../../src/stories/FilterTabs.stories.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAIvD,KAAK,gBAAgB,GAAG,KAAK,GAAG,aAAa,GAAG,WAAW,CAAC;AAQ5D,UAAU,uBAAuB;IAC/B,OAAO,EAAE,gBAAgB,CAAC;IAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAC7C;AAED,mFAAmF;AACnF,iBAAS,kBAAkB,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,uBAAuB,+BAYzE;AAED,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,kBAAkB,CAMzC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,YAAY,EAAE,KAmB1B,CAAC"}
@@ -6,4 +6,5 @@ type Story = StoryObj<typeof meta>;
6
6
  export declare const Default: Story;
7
7
  export declare const WithTrend: Story;
8
8
  export declare const Featured: Story;
9
+ export declare const WithPill: Story;
9
10
  //# sourceMappingURL=MetricsCard.stories.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MetricsCard.stories.d.ts","sourceRoot":"","sources":["../../src/stories/MetricsCard.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAElE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,WAAW,CAMlC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAMrB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAQvB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAUtB,CAAC"}
1
+ {"version":3,"file":"MetricsCard.stories.d.ts","sourceRoot":"","sources":["../../src/stories/MetricsCard.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAElE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,WAAW,CAMlC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAWrB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAavB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAetB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAgBtB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { Pagination } from '../buildmyiq/components/Pagination';
3
+ declare const meta: Meta<typeof Pagination>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof meta>;
6
+ export declare const FirstPage: Story;
7
+ export declare const LastPage: Story;
8
+ export declare const SinglePageHidden: Story;
9
+ //# sourceMappingURL=Pagination.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Pagination.stories.d.ts","sourceRoot":"","sources":["../../src/stories/Pagination.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAEhE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,UAAU,CAUjC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,SAAS,EAAE,KAevB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KActB,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,KAW9B,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { ErrorView } from '../buildmyiq/components/StateViews';
3
+ declare const meta: Meta<typeof ErrorView>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof meta>;
6
+ export declare const WithRetry: Story;
7
+ export declare const DefaultsWithoutRetry: Story;
8
+ //# sourceMappingURL=StateViews.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StateViews.stories.d.ts","sourceRoot":"","sources":["../../src/stories/StateViews.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;AAE/D,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,SAAS,CAMhC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,SAAS,EAAE,KAcvB,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,KAUlC,CAAC"}
@@ -8,4 +8,5 @@ export declare const InProgress: Story;
8
8
  export declare const Completed: Story;
9
9
  export declare const Advanced: Story;
10
10
  export declare const Technical: Story;
11
+ export declare const UnknownStatusFallsBack: Story;
11
12
  //# sourceMappingURL=StatusBadge.stories.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"StatusBadge.stories.d.ts","sourceRoot":"","sources":["../../src/stories/StatusBadge.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAElE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,WAAW,CAYlC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,SAAS,EAAE,KAIvB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KAIxB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAIvB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAItB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAIvB,CAAC"}
1
+ {"version":3,"file":"StatusBadge.stories.d.ts","sourceRoot":"","sources":["../../src/stories/StatusBadge.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAElE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,WAAW,CAYlC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,SAAS,EAAE,KAUvB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KAUxB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAOvB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAOtB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAOvB,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,KAQpC,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ type CohortSection = 'members' | 'schedule';
3
+ interface StatefulTabsProps {
4
+ onChange: (value: CohortSection) => void;
5
+ }
6
+ /** Tabs is presentational; the parent page owns the active section. */
7
+ declare function StatefulTabs({ onChange }: StatefulTabsProps): import("react").JSX.Element;
8
+ declare const meta: Meta<typeof StatefulTabs>;
9
+ export default meta;
10
+ type Story = StoryObj<typeof meta>;
11
+ export declare const SwitchSection: Story;
12
+ //# sourceMappingURL=Tabs.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tabs.stories.d.ts","sourceRoot":"","sources":["../../src/stories/Tabs.stories.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAKvD,KAAK,aAAa,GAAG,SAAS,GAAG,UAAU,CAAC;AAO5C,UAAU,iBAAiB;IACzB,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;CAC1C;AAED,uEAAuE;AACvE,iBAAS,YAAY,CAAC,EAAE,QAAQ,EAAE,EAAE,iBAAiB,+BAepD;AAED,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,YAAY,CAMnC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,aAAa,EAAE,KAgB3B,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { BuiCourse, BuiSkill } from '../../buildmyiq/types';
2
+ export declare const skillFixture: BuiSkill;
3
+ export declare const courseFixture: BuiCourse;
4
+ /** A course with no modules, lessons, skill or duration — the sparse edge case. */
5
+ export declare const sparseCourseFixture: BuiCourse;
6
+ //# sourceMappingURL=courses.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"courses.d.ts","sourceRoot":"","sources":["../../../src/stories/fixtures/courses.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,SAAS,EAAwB,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAMvF,eAAO,MAAM,YAAY,EAAE,QAc1B,CAAC;AAuDF,eAAO,MAAM,aAAa,EAAE,SAsB3B,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,mBAAmB,EAAE,SAajC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-buildmyiq",
3
- "version": "2026.1029.6",
3
+ "version": "2026.1030.1",
4
4
  "description": "Burdenoff BuildMyIQ Microfrontend — education, learning & development",
5
5
  "type": "module",
6
6
  "files": [
@@ -40,6 +40,7 @@
40
40
  "build-storybook": "storybook build",
41
41
  "test": "bun test",
42
42
  "test:storybook": "test-storybook",
43
+ "test:storybook:ci": "bun scripts/test-storybook-static.ts",
43
44
  "sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run lint && bun run format && bun run type:check && bun run build && bun run build-storybook'",
44
45
  "schema:pull": "graphql-codegen --project schema-wspace && graphql-codegen --project schema-global",
45
46
  "codegen": "graphql-codegen --project wspace && graphql-codegen --project global",