@burdenoff/microfe-buildmyiq 2026.1030.6 → 2026.1031.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.
Files changed (29) hide show
  1. package/dist/buildmyiq/components/DataTable.d.ts +22 -1
  2. package/dist/buildmyiq/components/DataTable.d.ts.map +1 -1
  3. package/dist/buildmyiq/components/DataTable.js +66 -42
  4. package/dist/buildmyiq/components/DataTable.js.map +1 -1
  5. package/dist/buildmyiq/components/MiniBar.d.ts +7 -1
  6. package/dist/buildmyiq/components/MiniBar.d.ts.map +1 -1
  7. package/dist/buildmyiq/components/MiniBar.js +8 -7
  8. package/dist/buildmyiq/components/MiniBar.js.map +1 -1
  9. package/dist/buildmyiq/pages/DashboardPage.js +1 -0
  10. package/dist/buildmyiq/pages/DashboardPage.js.map +1 -1
  11. package/dist/buildmyiq/pages/catalog/CourseDetailPage.d.ts.map +1 -1
  12. package/dist/buildmyiq/pages/catalog/CourseDetailPage.js +4 -1
  13. package/dist/buildmyiq/pages/catalog/CourseDetailPage.js.map +1 -1
  14. package/dist/buildmyiq/pages/cohorts/CohortsPage.d.ts.map +1 -1
  15. package/dist/buildmyiq/pages/cohorts/CohortsPage.js +2 -1
  16. package/dist/buildmyiq/pages/cohorts/CohortsPage.js.map +1 -1
  17. package/dist/buildmyiq/pages/insights/InsightsPage.d.ts.map +1 -1
  18. package/dist/buildmyiq/pages/insights/InsightsPage.js +3 -0
  19. package/dist/buildmyiq/pages/insights/InsightsPage.js.map +1 -1
  20. package/dist/buildmyiq/pages/learning/LessonPlayerPage.d.ts.map +1 -1
  21. package/dist/buildmyiq/pages/learning/LessonPlayerPage.js +3 -1
  22. package/dist/buildmyiq/pages/learning/LessonPlayerPage.js.map +1 -1
  23. package/dist/buildmyiq/pages/learning/MyLearningPage.d.ts.map +1 -1
  24. package/dist/buildmyiq/pages/learning/MyLearningPage.js +1 -0
  25. package/dist/buildmyiq/pages/learning/MyLearningPage.js.map +1 -1
  26. package/dist/buildmyiq/pages/onboarding/GettingStartedChecklist.d.ts.map +1 -1
  27. package/dist/buildmyiq/pages/onboarding/GettingStartedChecklist.js +1 -0
  28. package/dist/buildmyiq/pages/onboarding/GettingStartedChecklist.js.map +1 -1
  29. package/package.json +1 -1
@@ -21,7 +21,28 @@ interface DataTableProps<T> {
21
21
  key: string;
22
22
  dir: 'asc' | 'desc';
23
23
  };
24
+ /**
25
+ * Accessible name of the scroll region, announced when the table is wider
26
+ * than its container and keyboard users tab into it (e.g. "Cohorts").
27
+ */
28
+ label?: string;
24
29
  }
25
- export declare function DataTable<T>({ columns, rows, rowKey, onRowClick, className, defaultSort, }: DataTableProps<T>): import("react").JSX.Element;
30
+ /**
31
+ * Props for the table's horizontal scroll container. When the table is wider
32
+ * than the viewport (phones), the container becomes a named, focusable region
33
+ * so keyboard users can scroll it with the arrow keys (WCAG 2.1.1; axe
34
+ * `scrollable-region-focusable`). When nothing overflows it stays a plain div
35
+ * and adds no extra Tab stop.
36
+ */
37
+ export declare function scrollRegionProps(overflows: boolean, label: string): {
38
+ readonly role: "region";
39
+ readonly 'aria-label': string;
40
+ readonly tabIndex: 0;
41
+ } | {
42
+ readonly role?: undefined;
43
+ readonly 'aria-label'?: undefined;
44
+ readonly tabIndex?: undefined;
45
+ };
46
+ export declare function DataTable<T>({ columns, rows, rowKey, onRowClick, className, defaultSort, label, }: DataTableProps<T>): import("react").JSX.Element;
26
47
  export {};
27
48
  //# sourceMappingURL=DataTable.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/DataTable.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAK1D,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,SAAS,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,GAAG,MAAM,CAAC;CACzC;AAED,UAAU,cAAc,CAAC,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACrB,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,WAAW,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,CAAC;CACpD;AAQD,wBAAgB,SAAS,CAAC,CAAC,EAAE,EAC3B,OAAO,EACP,IAAI,EACJ,MAAM,EACN,UAAU,EACV,SAAS,EACT,WAAW,GACZ,EAAE,cAAc,CAAC,CAAC,CAAC,+BAqGnB"}
1
+ {"version":3,"file":"DataTable.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/DataTable.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAwC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAM7E,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,SAAS,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,GAAG,MAAM,CAAC;CACzC;AAED,UAAU,cAAc,CAAC,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACrB,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,WAAW,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,CAAC;IACnD;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuBD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM;;;;;;;;EAIlE;AAQD,wBAAgB,SAAS,CAAC,CAAC,EAAE,EAC3B,OAAO,EACP,IAAI,EACJ,MAAM,EACN,UAAU,EACV,SAAS,EACT,WAAW,EACX,KAAK,GACN,EAAE,cAAc,CAAC,CAAC,CAAC,+BA2GnB"}
@@ -1,30 +1,52 @@
1
- import { cn as e } from "../utils/cn.js";
2
- import { useMemo as t, useState as n } from "react";
3
- import { ChevronDown as r, ChevronUp as i, ChevronsUpDown as a } from "lucide-react";
4
- import { jsx as o, jsxs as s } from "react/jsx-runtime";
5
- import { GlassCard as c } from "@burdenoff/fe-libs/ui";
1
+ import { useTr as e } from "../shared/hooks/useTr.js";
2
+ import { cn as t } from "../utils/cn.js";
3
+ import { useEffect as n, useMemo as r, useRef as i, useState as a } from "react";
4
+ import { ChevronDown as o, ChevronUp as s, ChevronsUpDown as c } from "lucide-react";
5
+ import { jsx as l, jsxs as u } from "react/jsx-runtime";
6
+ import { GlassCard as d } from "@burdenoff/fe-libs/ui";
6
7
  //#region src/buildmyiq/components/DataTable.tsx
7
- var l = {
8
+ function f() {
9
+ let e = i(null), [t, r] = a(!1);
10
+ return n(() => {
11
+ let t = e.current;
12
+ if (!t) return;
13
+ let n = () => r(t.scrollWidth > t.clientWidth + 1);
14
+ if (n(), typeof ResizeObserver > "u") return;
15
+ let i = new ResizeObserver(n);
16
+ return i.observe(t), t.firstElementChild && i.observe(t.firstElementChild), () => i.disconnect();
17
+ }, []), {
18
+ ref: e,
19
+ overflows: t
20
+ };
21
+ }
22
+ function p(e, t) {
23
+ return e ? {
24
+ role: "region",
25
+ "aria-label": t,
26
+ tabIndex: 0
27
+ } : {};
28
+ }
29
+ var m = {
8
30
  left: "text-start",
9
31
  right: "text-end",
10
32
  center: "text-center"
11
33
  };
12
- function u({ columns: u, rows: d, rowKey: f, onRowClick: p, className: m, defaultSort: h }) {
13
- let [g, _] = n(h ?? null), v = t(() => {
14
- if (!g) return d;
15
- let e = u.find((e) => e.key === g.key);
16
- if (!e?.sortValue) return d;
17
- let t = g.dir === "asc" ? 1 : -1;
18
- return [...d].sort((n, r) => {
34
+ function h({ columns: n, rows: i, rowKey: h, onRowClick: g, className: _, defaultSort: v, label: y }) {
35
+ let b = e(), { ref: x, overflows: S } = f(), [C, w] = a(v ?? null), T = r(() => {
36
+ if (!C) return i;
37
+ let e = n.find((e) => e.key === C.key);
38
+ if (!e?.sortValue) return i;
39
+ let t = C.dir === "asc" ? 1 : -1;
40
+ return [...i].sort((n, r) => {
19
41
  let i = e.sortValue(n), a = e.sortValue(r);
20
42
  return i < a ? -1 * t : i > a ? 1 * t : 0;
21
43
  });
22
44
  }, [
23
- d,
24
- g,
25
- u
26
- ]), y = (e) => {
27
- _((t) => t?.key === e ? {
45
+ i,
46
+ C,
47
+ n
48
+ ]), E = (e) => {
49
+ w((t) => t?.key === e ? {
28
50
  key: e,
29
51
  dir: t.dir === "asc" ? "desc" : "asc"
30
52
  } : {
@@ -32,40 +54,42 @@ function u({ columns: u, rows: d, rowKey: f, onRowClick: p, className: m, defaul
32
54
  dir: "asc"
33
55
  });
34
56
  };
35
- return /* @__PURE__ */ o(c, {
57
+ return /* @__PURE__ */ l(d, {
36
58
  treatment: "plain",
37
- className: e("overflow-hidden", m),
38
- children: /* @__PURE__ */ o("div", {
39
- className: "overflow-x-auto",
40
- children: /* @__PURE__ */ s("table", {
59
+ className: t("overflow-hidden", _),
60
+ children: /* @__PURE__ */ l("div", {
61
+ ref: x,
62
+ className: "overflow-x-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-focus-ring)]",
63
+ ...p(S, y ?? b("buildmyiq.common.table", "Table")),
64
+ children: /* @__PURE__ */ u("table", {
41
65
  className: "w-full border-collapse text-sm",
42
- children: [/* @__PURE__ */ o("thead", { children: /* @__PURE__ */ o("tr", {
66
+ children: [/* @__PURE__ */ l("thead", { children: /* @__PURE__ */ l("tr", {
43
67
  className: "border-b border-border-seam",
44
- children: u.map((t) => {
45
- let n = !!t.sortValue, c = g?.key === t.key;
46
- return /* @__PURE__ */ o("th", {
47
- onClick: n ? () => y(t.key) : void 0,
48
- "aria-sort": c ? g?.dir === "asc" ? "ascending" : "descending" : void 0,
49
- className: e("px-4 py-3 font-medium text-text-secondary whitespace-nowrap", l[t.align ?? "left"], t.hideOnMobile && "hidden md:table-cell", n && "cursor-pointer select-none hover:text-text-primary"),
50
- children: /* @__PURE__ */ s("span", {
51
- className: e("inline-flex items-center gap-1", t.align === "right" && "flex-row-reverse"),
52
- children: [t.header, n && (c ? g?.dir === "asc" ? /* @__PURE__ */ o(i, { className: "h-3.5 w-3.5 text-[var(--color-accent-primary)]" }) : /* @__PURE__ */ o(r, { className: "h-3.5 w-3.5 text-[var(--color-accent-primary)]" }) : /* @__PURE__ */ o(a, { className: "h-3.5 w-3.5 opacity-40" }))]
68
+ children: n.map((e) => {
69
+ let n = !!e.sortValue, r = C?.key === e.key;
70
+ return /* @__PURE__ */ l("th", {
71
+ onClick: n ? () => E(e.key) : void 0,
72
+ "aria-sort": r ? C?.dir === "asc" ? "ascending" : "descending" : void 0,
73
+ className: t("px-4 py-3 font-medium text-text-secondary whitespace-nowrap", m[e.align ?? "left"], e.hideOnMobile && "hidden md:table-cell", n && "cursor-pointer select-none hover:text-text-primary"),
74
+ children: /* @__PURE__ */ u("span", {
75
+ className: t("inline-flex items-center gap-1", e.align === "right" && "flex-row-reverse"),
76
+ children: [e.header, n && (r ? C?.dir === "asc" ? /* @__PURE__ */ l(s, { className: "h-3.5 w-3.5 text-[var(--color-accent-primary)]" }) : /* @__PURE__ */ l(o, { className: "h-3.5 w-3.5 text-[var(--color-accent-primary)]" }) : /* @__PURE__ */ l(c, { className: "h-3.5 w-3.5 opacity-40" }))]
53
77
  })
54
- }, t.key);
78
+ }, e.key);
55
79
  })
56
- }) }), /* @__PURE__ */ o("tbody", { children: v.map((t) => /* @__PURE__ */ o("tr", {
57
- onClick: p ? () => p(t) : void 0,
58
- className: e("group border-b border-border-seam/60 last:border-0", p && "cursor-pointer transition-colors hover:bg-bg-sunken/60"),
59
- children: u.map((n) => /* @__PURE__ */ o("td", {
60
- className: e("px-4 py-3 text-text-primary align-middle", l[n.align ?? "left"], n.hideOnMobile && "hidden md:table-cell", n.className),
61
- children: n.render(t)
80
+ }) }), /* @__PURE__ */ l("tbody", { children: T.map((e) => /* @__PURE__ */ l("tr", {
81
+ onClick: g ? () => g(e) : void 0,
82
+ className: t("group border-b border-border-seam/60 last:border-0", g && "cursor-pointer transition-colors hover:bg-bg-sunken/60"),
83
+ children: n.map((n) => /* @__PURE__ */ l("td", {
84
+ className: t("px-4 py-3 text-text-primary align-middle", m[n.align ?? "left"], n.hideOnMobile && "hidden md:table-cell", n.className),
85
+ children: n.render(e)
62
86
  }, n.key))
63
- }, f(t))) })]
87
+ }, h(e))) })]
64
88
  })
65
89
  })
66
90
  });
67
91
  }
68
92
  //#endregion
69
- export { u as DataTable };
93
+ export { h as DataTable };
70
94
 
71
95
  //# sourceMappingURL=DataTable.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/buildmyiq/components/DataTable.tsx"],"sourcesContent":["/**\n * DataTable — a lightweight, semantic-token table for list pages.\n * @module buildmyiq/components\n *\n * Deliberately minimal (no external table lib). Renders a responsive,\n * horizontally-scrollable table wrapped in a GlassCard. Columns declare a\n * header, a render fn, optional alignment, and optionally a `sortValue` to make\n * the column click-sortable.\n */\n\nimport { useMemo, useState, type ReactNode } from 'react';\nimport { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\nexport interface Column<T> {\n key: string;\n header: ReactNode;\n render: (row: T) => ReactNode;\n align?: 'left' | 'right' | 'center';\n className?: string;\n /** Hide below the `md` breakpoint for a mobile-friendly layout. */\n hideOnMobile?: boolean;\n /** Provide to make this column click-sortable (returns a comparable value). */\n sortValue?: (row: T) => string | number;\n}\n\ninterface DataTableProps<T> {\n columns: Column<T>[];\n rows: T[];\n rowKey: (row: T) => string;\n onRowClick?: (row: T) => void;\n className?: string;\n /** Default sort: a column key + direction. */\n defaultSort?: { key: string; dir: 'asc' | 'desc' };\n}\n\nconst alignClass = {\n left: 'text-start',\n right: 'text-end',\n center: 'text-center',\n} as const;\n\nexport function DataTable<T>({\n columns,\n rows,\n rowKey,\n onRowClick,\n className,\n defaultSort,\n}: DataTableProps<T>) {\n const [sort, setSort] = useState<{ key: string; dir: 'asc' | 'desc' } | null>(\n defaultSort ?? null\n );\n\n const sortedRows = useMemo(() => {\n if (!sort) return rows;\n const col = columns.find((c) => c.key === sort.key);\n if (!col?.sortValue) return rows;\n const dir = sort.dir === 'asc' ? 1 : -1;\n return [...rows].sort((a, b) => {\n const va = col.sortValue!(a);\n const vb = col.sortValue!(b);\n if (va < vb) return -1 * dir;\n if (va > vb) return 1 * dir;\n return 0;\n });\n }, [rows, sort, columns]);\n\n const toggleSort = (key: string) => {\n setSort((prev) =>\n prev?.key === key ? { key, dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'asc' }\n );\n };\n\n return (\n <GlassCard treatment=\"plain\" className={cn('overflow-hidden', className)}>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full border-collapse text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam\">\n {columns.map((col) => {\n const sortable = Boolean(col.sortValue);\n const active = sort?.key === col.key;\n return (\n <th\n key={col.key}\n onClick={sortable ? () => toggleSort(col.key) : undefined}\n aria-sort={\n active ? (sort?.dir === 'asc' ? 'ascending' : 'descending') : undefined\n }\n className={cn(\n 'px-4 py-3 font-medium text-text-secondary whitespace-nowrap',\n alignClass[col.align ?? 'left'],\n col.hideOnMobile && 'hidden md:table-cell',\n sortable && 'cursor-pointer select-none hover:text-text-primary'\n )}\n >\n <span\n className={cn(\n 'inline-flex items-center gap-1',\n col.align === 'right' && 'flex-row-reverse'\n )}\n >\n {col.header}\n {sortable &&\n (active ? (\n sort?.dir === 'asc' ? (\n <ChevronUp className=\"h-3.5 w-3.5 text-[var(--color-accent-primary)]\" />\n ) : (\n <ChevronDown className=\"h-3.5 w-3.5 text-[var(--color-accent-primary)]\" />\n )\n ) : (\n <ChevronsUpDown className=\"h-3.5 w-3.5 opacity-40\" />\n ))}\n </span>\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sortedRows.map((row) => (\n <tr\n key={rowKey(row)}\n onClick={onRowClick ? () => onRowClick(row) : undefined}\n className={cn(\n 'group border-b border-border-seam/60 last:border-0',\n onRowClick && 'cursor-pointer transition-colors hover:bg-bg-sunken/60'\n )}\n >\n {columns.map((col) => (\n <td\n key={col.key}\n className={cn(\n 'px-4 py-3 text-text-primary align-middle',\n alignClass[col.align ?? 'left'],\n col.hideOnMobile && 'hidden md:table-cell',\n col.className\n )}\n >\n {col.render(row)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;AAqCA,IAAM,IAAa;CACjB,MAAM;CACN,OAAO;CACP,QAAQ;AACV;AAEA,SAAgB,EAAa,EAC3B,YACA,SACA,WACA,eACA,cACA,kBACoB;CACpB,IAAM,CAAC,GAAM,KAAW,EACtB,KAAe,IACjB,GAEM,IAAa,QAAc;EAC/B,IAAI,CAAC,GAAM,OAAO;EAClB,IAAM,IAAM,EAAQ,MAAM,MAAM,EAAE,QAAQ,EAAK,GAAG;EAClD,IAAI,CAAC,GAAK,WAAW,OAAO;EAC5B,IAAM,IAAM,EAAK,QAAQ,QAAQ,IAAI;EACrC,OAAO,CAAC,GAAG,CAAI,EAAE,MAAM,GAAG,MAAM;GAC9B,IAAM,IAAK,EAAI,UAAW,CAAC,GACrB,IAAK,EAAI,UAAW,CAAC;GAG3B,OAFI,IAAK,IAAW,KAAK,IACrB,IAAK,IAAW,IAAI,IACjB;EACT,CAAC;CACH,GAAG;EAAC;EAAM;EAAM;CAAO,CAAC,GAElB,KAAc,MAAgB;EAClC,GAAS,MACP,GAAM,QAAQ,IAAM;GAAE;GAAK,KAAK,EAAK,QAAQ,QAAQ,SAAS;EAAM,IAAI;GAAE;GAAK,KAAK;EAAM,CAC5F;CACF;CAEA,OACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAW,EAAG,mBAAmB,CAAS;YACrE,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,SAAD;IAAO,WAAU;cAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAQ,KAAK,MAAQ;MACpB,IAAM,IAAW,EAAQ,EAAI,WACvB,IAAS,GAAM,QAAQ,EAAI;MACjC,OACE,kBAAC,MAAD;OAEE,SAAS,UAAiB,EAAW,EAAI,GAAG,IAAI,KAAA;OAChD,aACE,IAAU,GAAM,QAAQ,QAAQ,cAAc,eAAgB,KAAA;OAEhE,WAAW,EACT,+DACA,EAAW,EAAI,SAAS,SACxB,EAAI,gBAAgB,wBACpB,KAAY,oDACd;iBAEA,kBAAC,QAAD;QACE,WAAW,EACT,kCACA,EAAI,UAAU,WAAW,kBAC3B;kBAJF,CAMG,EAAI,QACJ,MACE,IACC,GAAM,QAAQ,QACZ,kBAAC,GAAD,EAAW,WAAU,iDAAkD,CAAA,IAEvE,kBAAC,GAAD,EAAa,WAAU,iDAAkD,CAAA,IAG3E,kBAAC,GAAD,EAAgB,WAAU,yBAA0B,CAAA,EAEpD;;MACJ,GA9BG,EAAI,GA8BP;KAER,CAAC;IACC,CAAA,EACC,CAAA,GACP,kBAAC,SAAD,EAAA,UACG,EAAW,KAAK,MACf,kBAAC,MAAD;KAEE,SAAS,UAAmB,EAAW,CAAG,IAAI,KAAA;KAC9C,WAAW,EACT,sDACA,KAAc,wDAChB;eAEC,EAAQ,KAAK,MACZ,kBAAC,MAAD;MAEE,WAAW,EACT,4CACA,EAAW,EAAI,SAAS,SACxB,EAAI,gBAAgB,wBACpB,EAAI,SACN;gBAEC,EAAI,OAAO,CAAG;KACb,GATG,EAAI,GASP,CACL;IACC,GApBG,EAAO,CAAG,CAoBb,CACL,EACI,CAAA,CACF;;EACJ,CAAA;CACI,CAAA;AAEf"}
1
+ {"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/buildmyiq/components/DataTable.tsx"],"sourcesContent":["/**\n * DataTable — a lightweight, semantic-token table for list pages.\n * @module buildmyiq/components\n *\n * Deliberately minimal (no external table lib). Renders a responsive,\n * horizontally-scrollable table wrapped in a GlassCard. Columns declare a\n * header, a render fn, optional alignment, and optionally a `sortValue` to make\n * the column click-sortable.\n */\n\nimport { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { useTr } from '../shared/hooks/useTr';\n\nexport interface Column<T> {\n key: string;\n header: ReactNode;\n render: (row: T) => ReactNode;\n align?: 'left' | 'right' | 'center';\n className?: string;\n /** Hide below the `md` breakpoint for a mobile-friendly layout. */\n hideOnMobile?: boolean;\n /** Provide to make this column click-sortable (returns a comparable value). */\n sortValue?: (row: T) => string | number;\n}\n\ninterface DataTableProps<T> {\n columns: Column<T>[];\n rows: T[];\n rowKey: (row: T) => string;\n onRowClick?: (row: T) => void;\n className?: string;\n /** Default sort: a column key + direction. */\n defaultSort?: { key: string; dir: 'asc' | 'desc' };\n /**\n * Accessible name of the scroll region, announced when the table is wider\n * than its container and keyboard users tab into it (e.g. \"Cohorts\").\n */\n label?: string;\n}\n\n/**\n * Tracks whether an element scrolls horizontally, re-checking whenever it or\n * its content resizes.\n */\nfunction useHorizontalOverflow<E extends HTMLElement>() {\n const ref = useRef<E>(null);\n const [overflows, setOverflows] = useState(false);\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n const check = () => setOverflows(el.scrollWidth > el.clientWidth + 1);\n check();\n if (typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(check);\n observer.observe(el);\n if (el.firstElementChild) observer.observe(el.firstElementChild);\n return () => observer.disconnect();\n }, []);\n return { ref, overflows };\n}\n\n/**\n * Props for the table's horizontal scroll container. When the table is wider\n * than the viewport (phones), the container becomes a named, focusable region\n * so keyboard users can scroll it with the arrow keys (WCAG 2.1.1; axe\n * `scrollable-region-focusable`). When nothing overflows it stays a plain div\n * and adds no extra Tab stop.\n */\nexport function scrollRegionProps(overflows: boolean, label: string) {\n return overflows\n ? ({ role: 'region', 'aria-label': label, tabIndex: 0 } as const)\n : ({} as const);\n}\n\nconst alignClass = {\n left: 'text-start',\n right: 'text-end',\n center: 'text-center',\n} as const;\n\nexport function DataTable<T>({\n columns,\n rows,\n rowKey,\n onRowClick,\n className,\n defaultSort,\n label,\n}: DataTableProps<T>) {\n const tr = useTr();\n const { ref: scrollRef, overflows } = useHorizontalOverflow<HTMLDivElement>();\n const [sort, setSort] = useState<{ key: string; dir: 'asc' | 'desc' } | null>(\n defaultSort ?? null\n );\n\n const sortedRows = useMemo(() => {\n if (!sort) return rows;\n const col = columns.find((c) => c.key === sort.key);\n if (!col?.sortValue) return rows;\n const dir = sort.dir === 'asc' ? 1 : -1;\n return [...rows].sort((a, b) => {\n const va = col.sortValue!(a);\n const vb = col.sortValue!(b);\n if (va < vb) return -1 * dir;\n if (va > vb) return 1 * dir;\n return 0;\n });\n }, [rows, sort, columns]);\n\n const toggleSort = (key: string) => {\n setSort((prev) =>\n prev?.key === key ? { key, dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'asc' }\n );\n };\n\n return (\n <GlassCard treatment=\"plain\" className={cn('overflow-hidden', className)}>\n <div\n ref={scrollRef}\n className=\"overflow-x-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-focus-ring)]\"\n {...scrollRegionProps(overflows, label ?? tr('buildmyiq.common.table', 'Table'))}\n >\n <table className=\"w-full border-collapse text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam\">\n {columns.map((col) => {\n const sortable = Boolean(col.sortValue);\n const active = sort?.key === col.key;\n return (\n <th\n key={col.key}\n onClick={sortable ? () => toggleSort(col.key) : undefined}\n aria-sort={\n active ? (sort?.dir === 'asc' ? 'ascending' : 'descending') : undefined\n }\n className={cn(\n 'px-4 py-3 font-medium text-text-secondary whitespace-nowrap',\n alignClass[col.align ?? 'left'],\n col.hideOnMobile && 'hidden md:table-cell',\n sortable && 'cursor-pointer select-none hover:text-text-primary'\n )}\n >\n <span\n className={cn(\n 'inline-flex items-center gap-1',\n col.align === 'right' && 'flex-row-reverse'\n )}\n >\n {col.header}\n {sortable &&\n (active ? (\n sort?.dir === 'asc' ? (\n <ChevronUp className=\"h-3.5 w-3.5 text-[var(--color-accent-primary)]\" />\n ) : (\n <ChevronDown className=\"h-3.5 w-3.5 text-[var(--color-accent-primary)]\" />\n )\n ) : (\n <ChevronsUpDown className=\"h-3.5 w-3.5 opacity-40\" />\n ))}\n </span>\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sortedRows.map((row) => (\n <tr\n key={rowKey(row)}\n onClick={onRowClick ? () => onRowClick(row) : undefined}\n className={cn(\n 'group border-b border-border-seam/60 last:border-0',\n onRowClick && 'cursor-pointer transition-colors hover:bg-bg-sunken/60'\n )}\n >\n {columns.map((col) => (\n <td\n key={col.key}\n className={cn(\n 'px-4 py-3 text-text-primary align-middle',\n alignClass[col.align ?? 'left'],\n col.hideOnMobile && 'hidden md:table-cell',\n col.className\n )}\n >\n {col.render(row)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":"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"}
@@ -5,12 +5,18 @@
5
5
  interface MiniBarProps {
6
6
  /** 0–100. */
7
7
  value: number;
8
+ /**
9
+ * Accessible name of the bar, e.g. the course title or the metric it shows.
10
+ * Required: a `progressbar` without a name fails WCAG 4.1.2 (axe
11
+ * `aria-progressbar-name`), and screen readers would only say "progress".
12
+ */
13
+ label: string;
8
14
  className?: string;
9
15
  /** Bar fill token class (defaults to accent). */
10
16
  barClassName?: string;
11
17
  /** Height in px. */
12
18
  height?: number;
13
19
  }
14
- export declare function MiniBar({ value, className, barClassName, height, }: MiniBarProps): import("react").JSX.Element;
20
+ export declare function MiniBar({ value, label, className, barClassName, height, }: MiniBarProps): import("react").JSX.Element;
15
21
  export {};
16
22
  //# sourceMappingURL=MiniBar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MiniBar.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/MiniBar.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,UAAU,YAAY;IACpB,aAAa;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,SAAS,EACT,YAAiD,EACjD,MAAU,GACX,EAAE,YAAY,+BAiBd"}
1
+ {"version":3,"file":"MiniBar.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/MiniBar.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,UAAU,YAAY;IACpB,aAAa;IACb,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,KAAK,EACL,SAAS,EACT,YAAiD,EACjD,MAAU,GACX,EAAE,YAAY,+BAkBd"}
@@ -1,18 +1,19 @@
1
1
  import { cn as e } from "../utils/cn.js";
2
2
  import { jsx as t } from "react/jsx-runtime";
3
3
  //#region src/buildmyiq/components/MiniBar.tsx
4
- function n({ value: n, className: r, barClassName: i = "bg-[var(--color-accent-primary)]", height: a = 6 }) {
5
- let o = Math.max(0, Math.min(100, n));
4
+ function n({ value: n, label: r, className: i, barClassName: a = "bg-[var(--color-accent-primary)]", height: o = 6 }) {
5
+ let s = Math.max(0, Math.min(100, n));
6
6
  return /* @__PURE__ */ t("div", {
7
- className: e("w-full overflow-hidden rounded-full bg-bg-sunken", r),
8
- style: { height: a },
7
+ className: e("w-full overflow-hidden rounded-full bg-bg-sunken", i),
8
+ style: { height: o },
9
9
  role: "progressbar",
10
- "aria-valuenow": Math.round(o),
10
+ "aria-label": r,
11
+ "aria-valuenow": Math.round(s),
11
12
  "aria-valuemin": 0,
12
13
  "aria-valuemax": 100,
13
14
  children: /* @__PURE__ */ t("div", {
14
- className: e("h-full rounded-full transition-[width] duration-700 ease-out", i),
15
- style: { width: `${o}%` }
15
+ className: e("h-full rounded-full transition-[width] duration-700 ease-out", a),
16
+ style: { width: `${s}%` }
16
17
  })
17
18
  });
18
19
  }
@@ -1 +1 @@
1
- {"version":3,"file":"MiniBar.js","names":[],"sources":["../../../src/buildmyiq/components/MiniBar.tsx"],"sourcesContent":["/**\n * MiniBar — a thin inline progress bar (target progress, completion, breach %).\n * @module buildmyiq/components\n */\n\nimport { cn } from '../utils/cn';\n\ninterface MiniBarProps {\n /** 0–100. */\n value: number;\n className?: string;\n /** Bar fill token class (defaults to accent). */\n barClassName?: string;\n /** Height in px. */\n height?: number;\n}\n\nexport function MiniBar({\n value,\n className,\n barClassName = 'bg-[var(--color-accent-primary)]',\n height = 6,\n}: MiniBarProps) {\n const clamped = Math.max(0, Math.min(100, value));\n return (\n <div\n className={cn('w-full overflow-hidden rounded-full bg-bg-sunken', className)}\n style={{ height }}\n role=\"progressbar\"\n aria-valuenow={Math.round(clamped)}\n aria-valuemin={0}\n aria-valuemax={100}\n >\n <div\n className={cn('h-full rounded-full transition-[width] duration-700 ease-out', barClassName)}\n style={{ width: `${clamped}%` }}\n />\n </div>\n );\n}\n"],"mappings":";;;AAiBA,SAAgB,EAAQ,EACtB,UACA,cACA,kBAAe,oCACf,YAAS,KACM;CACf,IAAM,IAAU,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,CAAK,CAAC;CAChD,OACE,kBAAC,OAAD;EACE,WAAW,EAAG,oDAAoD,CAAS;EAC3E,OAAO,EAAE,UAAO;EAChB,MAAK;EACL,iBAAe,KAAK,MAAM,CAAO;EACjC,iBAAe;EACf,iBAAe;YAEf,kBAAC,OAAD;GACE,WAAW,EAAG,gEAAgE,CAAY;GAC1F,OAAO,EAAE,OAAO,GAAG,EAAQ,GAAG;EAC/B,CAAA;CACE,CAAA;AAET"}
1
+ {"version":3,"file":"MiniBar.js","names":[],"sources":["../../../src/buildmyiq/components/MiniBar.tsx"],"sourcesContent":["/**\n * MiniBar — a thin inline progress bar (target progress, completion, breach %).\n * @module buildmyiq/components\n */\n\nimport { cn } from '../utils/cn';\n\ninterface MiniBarProps {\n /** 0–100. */\n value: number;\n /**\n * Accessible name of the bar, e.g. the course title or the metric it shows.\n * Required: a `progressbar` without a name fails WCAG 4.1.2 (axe\n * `aria-progressbar-name`), and screen readers would only say \"progress\".\n */\n label: string;\n className?: string;\n /** Bar fill token class (defaults to accent). */\n barClassName?: string;\n /** Height in px. */\n height?: number;\n}\n\nexport function MiniBar({\n value,\n label,\n className,\n barClassName = 'bg-[var(--color-accent-primary)]',\n height = 6,\n}: MiniBarProps) {\n const clamped = Math.max(0, Math.min(100, value));\n return (\n <div\n className={cn('w-full overflow-hidden rounded-full bg-bg-sunken', className)}\n style={{ height }}\n role=\"progressbar\"\n aria-label={label}\n aria-valuenow={Math.round(clamped)}\n aria-valuemin={0}\n aria-valuemax={100}\n >\n <div\n className={cn('h-full rounded-full transition-[width] duration-700 ease-out', barClassName)}\n style={{ width: `${clamped}%` }}\n />\n </div>\n );\n}\n"],"mappings":";;;AAuBA,SAAgB,EAAQ,EACtB,UACA,UACA,cACA,kBAAe,oCACf,YAAS,KACM;CACf,IAAM,IAAU,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,CAAK,CAAC;CAChD,OACE,kBAAC,OAAD;EACE,WAAW,EAAG,oDAAoD,CAAS;EAC3E,OAAO,EAAE,UAAO;EAChB,MAAK;EACL,cAAY;EACZ,iBAAe,KAAK,MAAM,CAAO;EACjC,iBAAe;EACf,iBAAe;YAEf,kBAAC,OAAD;GACE,WAAW,EAAG,gEAAgE,CAAY;GAC1F,OAAO,EAAE,OAAO,GAAG,EAAQ,GAAG;EAC/B,CAAA;CACE,CAAA;AAET"}
@@ -210,6 +210,7 @@ var A = (e, t = 8) => {
210
210
  className: "flex items-center gap-2",
211
211
  children: [/* @__PURE__ */ T(u, {
212
212
  value: e.progressPct,
213
+ label: n,
213
214
  className: "flex-1"
214
215
  }), /* @__PURE__ */ E("span", {
215
216
  className: "shrink-0 text-xs font-semibold tabular-nums text-text-secondary",
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/buildmyiq/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * DashboardPage — the Buildmyiq landing surface.\n * @module buildmyiq/pages\n *\n * A learner + educator overview built entirely from real `bui*` data:\n * - KPI row: published courses, my active enrollments, completed, certificates\n * — each stat card carries a tinted icon tile and (where derivable) a\n * Sparkline trend built from the REAL timestamps of the underlying rows.\n * - Continue learning: the caller's in-progress enrollments as rich cards with\n * real cover art, summary, a progress bar and a Continue action.\n * - Explore: recently published catalog courses.\n * - Getting started (BOFF-3095): a four-step checklist for workspaces that\n * have not activated yet, with optional demo content to explore first.\n * Every number, row, avatar and trend is derived from live hooks; empty states\n * are honest and no value is fabricated.\n */\n\nimport type { FC, ReactNode, MouseEvent } from 'react';\nimport { useMemo } from 'react';\nimport { BookOpen, Clock, CheckCircle2, Award, ArrowRight, Play } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport {\n PageLayout,\n MetricsCard,\n EmptyState,\n LoadingView,\n Section,\n MiniBar,\n Avatar,\n Sparkline,\n} from '../components';\nimport { CourseCard } from './_shared';\nimport { GettingStartedChecklist } from './onboarding/GettingStartedChecklist';\nimport { cn } from '../utils/cn';\nimport { useBuildmyiq } from '../context';\nimport { useBuiCourses, useBuiMyEnrollments, useBuiMyCertificates } from '../hooks';\nimport { useTr } from '../shared/hooks/useTr';\nimport { formatRelativeTime, formatEnumLabel } from '../utils/formatters';\nimport type { BuiCourse, BuiEnrollment } from '../types';\n\n/**\n * Build a cumulative-count trend series from a set of REAL event timestamps\n * (e.g. `publishedAt`, `enrolledAt`, `issuedAt`). The span from the earliest\n * event to now is split into `buckets` slots and each slot holds the running\n * count of events up to its boundary — a genuine, non-fabricated growth line.\n * Returns fewer than two points when there is not enough history to plot; the\n * caller then omits the sparkline rather than drawing a meaningless flat line.\n */\nconst cumulativeSeries = (isoDates: Array<string | null | undefined>, buckets = 8): number[] => {\n const times = isoDates\n .map((d) => (d ? new Date(d).getTime() : Number.NaN))\n .filter((t) => !Number.isNaN(t))\n .sort((a, b) => a - b);\n if (times.length < 2) return times.length === 1 ? [1] : [];\n const start = times[0];\n const end = Date.now();\n const span = Math.max(1, end - start);\n const series: number[] = [];\n for (let i = 1; i <= buckets; i += 1) {\n const boundary = start + (span * i) / buckets;\n series.push(times.filter((t) => t <= boundary).length);\n }\n return series;\n};\n\n/** A tinted, rounded icon tile used in the KPI stat cards. */\nconst StatIcon: FC<{ tint: string; children: ReactNode }> = ({ tint, children }) => (\n <span\n className={cn('inline-flex h-10 w-10 items-center justify-center rounded-xl', tint)}\n aria-hidden=\"true\"\n >\n {children}\n </span>\n);\n\nexport const DashboardPage: FC = () => {\n const { currentUser, navigateTo, workspaceId } = useBuildmyiq();\n const tr = useTr();\n const bus = useEventBus();\n const learnerId = currentUser?.id;\n\n const coursesQuery = useBuiCourses({ status: 'PUBLISHED', limit: 50 });\n // Self-scoped reads of the signed-in learner's own records (FERPA/IDOR\n // hardening removed the learnerId arg from the workspace-wide lists).\n const enrollmentsQuery = useBuiMyEnrollments({ limit: 50, enabled: Boolean(learnerId) });\n const certsQuery = useBuiMyCertificates(undefined, Boolean(learnerId));\n\n const courses: BuiCourse[] = coursesQuery.data?.items ?? [];\n const enrollments: BuiEnrollment[] = enrollmentsQuery.data?.items ?? [];\n const certificates = certsQuery.data ?? [];\n\n const activeEnrollments = useMemo(\n () =>\n enrollments\n .filter((e) => e.status === 'ENROLLED' || e.status === 'IN_PROGRESS')\n .sort((a, b) =>\n (b.lastActiveAt ?? b.enrolledAt).localeCompare(a.lastActiveAt ?? a.enrolledAt)\n ),\n [enrollments]\n );\n const completedCount = enrollments.filter((e) => e.status === 'COMPLETED').length;\n\n const firstName = currentUser?.firstName || currentUser?.name?.split(' ')[0] || 'there';\n const fullName = [currentUser?.firstName, currentUser?.lastName].filter(Boolean).join(' ').trim();\n const displayName = currentUser?.name || fullName || (firstName === 'there' ? '' : firstName);\n const roleLabel = currentUser?.role ? formatEnumLabel(currentUser.role) : currentUser?.email;\n\n const isLoading = coursesQuery.isLoading || (Boolean(learnerId) && enrollmentsQuery.isLoading);\n\n const courseById = useMemo(() => {\n const map = new Map<string, BuiCourse>();\n for (const c of courses) map.set(c.id, c);\n return map;\n }, [courses]);\n\n // REAL trend lines — cumulative growth from each entity's own timestamps.\n const trends = useMemo(\n () => ({\n published: cumulativeSeries(courses.map((c) => c.publishedAt ?? c.createdAt)),\n inProgress: cumulativeSeries(enrollments.map((e) => e.enrolledAt)),\n completed: cumulativeSeries(\n enrollments.filter((e) => e.status === 'COMPLETED').map((e) => e.completedAt ?? e.updatedAt)\n ),\n certificates: cumulativeSeries(certificates.map((c) => c.issuedAt)),\n }),\n [courses, enrollments, certificates]\n );\n\n const kpis: Array<{\n key: string;\n icon: ReactNode;\n tint: string;\n value: string | number;\n label: string;\n featured?: boolean;\n series: number[];\n sparkColor: string;\n }> = [\n {\n key: 'published',\n icon: <BookOpen className=\"h-5 w-5\" />,\n tint: 'bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)]',\n value: `${courses.length}${coursesQuery.data?.pageInfo?.hasMore ? '+' : ''}`,\n label: tr('buildmyiq.dashboard.publishedCourses', 'Published courses'),\n featured: true,\n series: trends.published,\n sparkColor: 'var(--color-accent-primary)',\n },\n {\n key: 'in-progress',\n icon: <Clock className=\"h-5 w-5\" />,\n tint: 'bg-[color-mix(in_srgb,var(--color-accent-teal)_16%,transparent)] text-[var(--color-accent-teal)]',\n value: activeEnrollments.length,\n label: tr('buildmyiq.dashboard.coursesInProgress', 'Courses in progress'),\n series: trends.inProgress,\n sparkColor: 'var(--color-accent-teal)',\n },\n {\n key: 'completed',\n icon: <CheckCircle2 className=\"h-5 w-5\" />,\n tint: 'bg-status-success-bg-subtle text-status-success-text',\n value: completedCount,\n label: tr('buildmyiq.dashboard.completed', 'Completed'),\n series: trends.completed,\n sparkColor: 'var(--color-status-success-text)',\n },\n {\n key: 'certificates',\n icon: <Award className=\"h-5 w-5\" />,\n tint: 'bg-status-warning-bg-subtle text-status-warning-text',\n value: certificates.length,\n label: tr('buildmyiq.dashboard.certificatesEarned', 'Certificates earned'),\n series: trends.certificates,\n sparkColor: 'var(--color-status-warning-text)',\n },\n ];\n\n return (\n <PageLayout\n title={tr('buildmyiq.dashboard.welcomeBack', 'Welcome back, {{name}}', { name: firstName })}\n description={tr(\n 'buildmyiq.dashboard.subtitle',\n 'Your learning, teaching and credentialing — all in one workspace.'\n )}\n titleGradient\n aurora\n actions={\n currentUser ? (\n <div className=\"flex w-fit items-center gap-2.5 rounded-full border border-border-seam bg-bg-surface/60 py-1.5 ps-1.5 pe-3.5 backdrop-blur\">\n <Avatar\n name={displayName || null}\n seed={currentUser.id}\n src={currentUser.avatar}\n size=\"sm\"\n />\n {(displayName || roleLabel) && (\n <div className=\"min-w-0 leading-tight\">\n {displayName && (\n <p className=\"truncate text-sm font-medium text-text-primary\">{displayName}</p>\n )}\n {roleLabel && (\n <p className=\"max-w-[10rem] truncate text-xs text-text-secondary\">{roleLabel}</p>\n )}\n </div>\n )}\n </div>\n ) : undefined\n }\n >\n {isLoading ? (\n <LoadingView\n label={tr('buildmyiq.dashboard.loading', 'Loading your learning dashboard…')}\n />\n ) : (\n <div className=\"flex flex-col gap-8\" data-tour=\"buildmyiq-dashboard-content\">\n {/* KPI row */}\n <div\n className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\"\n data-tour=\"buildmyiq-dashboard-kpis\"\n >\n {kpis.map((k) => (\n <MetricsCard\n key={k.key}\n icon={<StatIcon tint={k.tint}>{k.icon}</StatIcon>}\n value={k.value}\n label={k.label}\n featured={k.featured}\n pill={\n k.series.length >= 2 ? (\n <Sparkline data={k.series} color={k.sparkColor} />\n ) : undefined\n }\n />\n ))}\n </div>\n\n {/* Getting-started checklist (BOFF-3095): hidden once the workspace activates */}\n <GettingStartedChecklist />\n\n {/* Continue learning */}\n <Section\n title={tr('buildmyiq.dashboard.continueLearning', 'Continue learning')}\n description={tr(\n 'buildmyiq.dashboard.continueLearningDescription',\n 'Pick up where you left off.'\n )}\n action={\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.my_learning_clicked', { workspaceId });\n navigateTo('learning');\n }}\n data-tour=\"buildmyiq-dashboard-my-learning\"\n >\n {tr('buildmyiq.dashboard.myLearning', 'My learning')}{' '}\n <ArrowRight className=\"ms-1 h-4 w-4 rtl:-scale-x-100\" />\n </Button>\n }\n >\n {activeEnrollments.length === 0 ? (\n <EmptyState\n illustration=\"onboarding\"\n title={tr('buildmyiq.dashboard.noActiveCoursesTitle', 'No active courses yet')}\n description={tr(\n 'buildmyiq.dashboard.noActiveCoursesDescription',\n 'Browse the catalog and enrol in a course to start learning.'\n )}\n action={\n <Button\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.explore_catalog_clicked', {\n workspaceId,\n });\n navigateTo('catalog');\n }}\n data-tour=\"buildmyiq-dashboard-explore-catalog\"\n >\n {tr('common.explore', 'Explore the catalog')}\n </Button>\n }\n />\n ) : (\n <div\n className=\"grid grid-cols-1 gap-4 md:grid-cols-2\"\n data-tour=\"buildmyiq-dashboard-continue-cards\"\n >\n {activeEnrollments.slice(0, 4).map((e) => {\n const course = courseById.get(e.courseId);\n const title = course?.title ?? tr('buildmyiq.course.pageTitle', 'Course');\n const goToCourse = (ev?: MouseEvent) => {\n ev?.stopPropagation();\n navigateTo(`learning/${e.id}`);\n };\n return (\n <GlassCard\n key={e.id}\n treatment=\"plain\"\n className=\"group flex cursor-pointer flex-col overflow-hidden transition-shadow hover:shadow-lg sm:flex-row\"\n onClick={() => goToCourse()}\n >\n {/* Cover / play thumbnail */}\n <div className=\"relative h-36 w-full shrink-0 overflow-hidden bg-bg-sunken sm:h-auto sm:w-40\">\n {course?.cover?.downloadUrl ? (\n <img\n src={course.cover.downloadUrl}\n alt=\"\"\n className=\"h-full w-full object-cover transition-transform duration-500 group-hover:scale-105\"\n />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center bg-gradient-to-br from-[var(--color-accent-soft)] to-bg-sunken\">\n <BookOpen className=\"h-8 w-8 text-text-secondary opacity-60\" />\n </div>\n )}\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <span className=\"inline-flex h-11 w-11 items-center justify-center rounded-full bg-bg-surface/85 shadow-lg backdrop-blur transition-transform duration-200 group-hover:scale-110\">\n <Play className=\"ms-0.5 h-5 w-5 fill-current text-[var(--color-accent-primary)]\" />\n </span>\n </div>\n </div>\n\n {/* Content */}\n <div className=\"flex min-w-0 flex-1 flex-col gap-2 p-4\">\n <p className=\"line-clamp-1 font-semibold text-text-primary\">{title}</p>\n {course?.summary && (\n <p className=\"line-clamp-2 text-sm text-text-secondary\">\n {course.summary}\n </p>\n )}\n <div className=\"mt-auto space-y-2.5 pt-2\">\n <div className=\"flex items-center gap-2\">\n <MiniBar value={e.progressPct} className=\"flex-1\" />\n <span className=\"shrink-0 text-xs font-semibold tabular-nums text-text-secondary\">\n {e.progressPct}%\n </span>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate text-xs text-text-secondary\">\n {tr('common.active', 'Active {{time}}', {\n time: formatRelativeTime(e.lastActiveAt ?? e.enrolledAt),\n })}\n </span>\n <Button\n size=\"sm\"\n className=\"min-h-[40px] shrink-0\"\n onClick={(ev: MouseEvent) => {\n bus.emitCustom('buildmyiq.dashboard.continue_course_clicked', {\n workspaceId,\n enrollmentId: e.id,\n courseId: e.courseId,\n });\n goToCourse(ev);\n }}\n data-tour=\"buildmyiq-dashboard-continue-button\"\n >\n <Play className=\"me-1 h-4 w-4 fill-current\" />{' '}\n {tr('common.continue', 'Continue')}\n </Button>\n </div>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n )}\n </Section>\n\n {/* Explore catalog */}\n <Section\n title={tr('buildmyiq.dashboard.exploreCatalog', 'Explore the catalog')}\n description={tr(\n 'buildmyiq.dashboard.exploreCatalogDescription',\n 'Newly published courses in your workspace.'\n )}\n action={\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.browse_all_catalog_clicked', { workspaceId });\n navigateTo('catalog');\n }}\n data-tour=\"buildmyiq-dashboard-browse-all\"\n >\n {tr('common.browseAll', 'Browse all')}{' '}\n <ArrowRight className=\"ms-1 h-4 w-4 rtl:-scale-x-100\" />\n </Button>\n }\n >\n {courses.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr(\n 'buildmyiq.dashboard.noPublishedCoursesTitle',\n 'No published courses yet'\n )}\n description={tr(\n 'buildmyiq.dashboard.noPublishedCoursesDescription',\n 'Create and publish a course in Authoring to populate the catalog.'\n )}\n action={\n <Button\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.go_to_authoring_clicked', {\n workspaceId,\n });\n navigateTo('authoring');\n }}\n data-tour=\"buildmyiq-dashboard-go-authoring\"\n >\n {tr('common.create', 'Go to Authoring')}\n </Button>\n }\n />\n ) : (\n <div\n className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\"\n data-tour=\"buildmyiq-dashboard-catalog-grid\"\n >\n {courses.slice(0, 8).map((course) => (\n <CourseCard\n key={course.id}\n course={course}\n onClick={() => navigateTo(`catalog/${course.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n </div>\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/buildmyiq/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * DashboardPage — the Buildmyiq landing surface.\n * @module buildmyiq/pages\n *\n * A learner + educator overview built entirely from real `bui*` data:\n * - KPI row: published courses, my active enrollments, completed, certificates\n * — each stat card carries a tinted icon tile and (where derivable) a\n * Sparkline trend built from the REAL timestamps of the underlying rows.\n * - Continue learning: the caller's in-progress enrollments as rich cards with\n * real cover art, summary, a progress bar and a Continue action.\n * - Explore: recently published catalog courses.\n * - Getting started (BOFF-3095): a four-step checklist for workspaces that\n * have not activated yet, with optional demo content to explore first.\n * Every number, row, avatar and trend is derived from live hooks; empty states\n * are honest and no value is fabricated.\n */\n\nimport type { FC, ReactNode, MouseEvent } from 'react';\nimport { useMemo } from 'react';\nimport { BookOpen, Clock, CheckCircle2, Award, ArrowRight, Play } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport {\n PageLayout,\n MetricsCard,\n EmptyState,\n LoadingView,\n Section,\n MiniBar,\n Avatar,\n Sparkline,\n} from '../components';\nimport { CourseCard } from './_shared';\nimport { GettingStartedChecklist } from './onboarding/GettingStartedChecklist';\nimport { cn } from '../utils/cn';\nimport { useBuildmyiq } from '../context';\nimport { useBuiCourses, useBuiMyEnrollments, useBuiMyCertificates } from '../hooks';\nimport { useTr } from '../shared/hooks/useTr';\nimport { formatRelativeTime, formatEnumLabel } from '../utils/formatters';\nimport type { BuiCourse, BuiEnrollment } from '../types';\n\n/**\n * Build a cumulative-count trend series from a set of REAL event timestamps\n * (e.g. `publishedAt`, `enrolledAt`, `issuedAt`). The span from the earliest\n * event to now is split into `buckets` slots and each slot holds the running\n * count of events up to its boundary — a genuine, non-fabricated growth line.\n * Returns fewer than two points when there is not enough history to plot; the\n * caller then omits the sparkline rather than drawing a meaningless flat line.\n */\nconst cumulativeSeries = (isoDates: Array<string | null | undefined>, buckets = 8): number[] => {\n const times = isoDates\n .map((d) => (d ? new Date(d).getTime() : Number.NaN))\n .filter((t) => !Number.isNaN(t))\n .sort((a, b) => a - b);\n if (times.length < 2) return times.length === 1 ? [1] : [];\n const start = times[0];\n const end = Date.now();\n const span = Math.max(1, end - start);\n const series: number[] = [];\n for (let i = 1; i <= buckets; i += 1) {\n const boundary = start + (span * i) / buckets;\n series.push(times.filter((t) => t <= boundary).length);\n }\n return series;\n};\n\n/** A tinted, rounded icon tile used in the KPI stat cards. */\nconst StatIcon: FC<{ tint: string; children: ReactNode }> = ({ tint, children }) => (\n <span\n className={cn('inline-flex h-10 w-10 items-center justify-center rounded-xl', tint)}\n aria-hidden=\"true\"\n >\n {children}\n </span>\n);\n\nexport const DashboardPage: FC = () => {\n const { currentUser, navigateTo, workspaceId } = useBuildmyiq();\n const tr = useTr();\n const bus = useEventBus();\n const learnerId = currentUser?.id;\n\n const coursesQuery = useBuiCourses({ status: 'PUBLISHED', limit: 50 });\n // Self-scoped reads of the signed-in learner's own records (FERPA/IDOR\n // hardening removed the learnerId arg from the workspace-wide lists).\n const enrollmentsQuery = useBuiMyEnrollments({ limit: 50, enabled: Boolean(learnerId) });\n const certsQuery = useBuiMyCertificates(undefined, Boolean(learnerId));\n\n const courses: BuiCourse[] = coursesQuery.data?.items ?? [];\n const enrollments: BuiEnrollment[] = enrollmentsQuery.data?.items ?? [];\n const certificates = certsQuery.data ?? [];\n\n const activeEnrollments = useMemo(\n () =>\n enrollments\n .filter((e) => e.status === 'ENROLLED' || e.status === 'IN_PROGRESS')\n .sort((a, b) =>\n (b.lastActiveAt ?? b.enrolledAt).localeCompare(a.lastActiveAt ?? a.enrolledAt)\n ),\n [enrollments]\n );\n const completedCount = enrollments.filter((e) => e.status === 'COMPLETED').length;\n\n const firstName = currentUser?.firstName || currentUser?.name?.split(' ')[0] || 'there';\n const fullName = [currentUser?.firstName, currentUser?.lastName].filter(Boolean).join(' ').trim();\n const displayName = currentUser?.name || fullName || (firstName === 'there' ? '' : firstName);\n const roleLabel = currentUser?.role ? formatEnumLabel(currentUser.role) : currentUser?.email;\n\n const isLoading = coursesQuery.isLoading || (Boolean(learnerId) && enrollmentsQuery.isLoading);\n\n const courseById = useMemo(() => {\n const map = new Map<string, BuiCourse>();\n for (const c of courses) map.set(c.id, c);\n return map;\n }, [courses]);\n\n // REAL trend lines — cumulative growth from each entity's own timestamps.\n const trends = useMemo(\n () => ({\n published: cumulativeSeries(courses.map((c) => c.publishedAt ?? c.createdAt)),\n inProgress: cumulativeSeries(enrollments.map((e) => e.enrolledAt)),\n completed: cumulativeSeries(\n enrollments.filter((e) => e.status === 'COMPLETED').map((e) => e.completedAt ?? e.updatedAt)\n ),\n certificates: cumulativeSeries(certificates.map((c) => c.issuedAt)),\n }),\n [courses, enrollments, certificates]\n );\n\n const kpis: Array<{\n key: string;\n icon: ReactNode;\n tint: string;\n value: string | number;\n label: string;\n featured?: boolean;\n series: number[];\n sparkColor: string;\n }> = [\n {\n key: 'published',\n icon: <BookOpen className=\"h-5 w-5\" />,\n tint: 'bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)]',\n value: `${courses.length}${coursesQuery.data?.pageInfo?.hasMore ? '+' : ''}`,\n label: tr('buildmyiq.dashboard.publishedCourses', 'Published courses'),\n featured: true,\n series: trends.published,\n sparkColor: 'var(--color-accent-primary)',\n },\n {\n key: 'in-progress',\n icon: <Clock className=\"h-5 w-5\" />,\n tint: 'bg-[color-mix(in_srgb,var(--color-accent-teal)_16%,transparent)] text-[var(--color-accent-teal)]',\n value: activeEnrollments.length,\n label: tr('buildmyiq.dashboard.coursesInProgress', 'Courses in progress'),\n series: trends.inProgress,\n sparkColor: 'var(--color-accent-teal)',\n },\n {\n key: 'completed',\n icon: <CheckCircle2 className=\"h-5 w-5\" />,\n tint: 'bg-status-success-bg-subtle text-status-success-text',\n value: completedCount,\n label: tr('buildmyiq.dashboard.completed', 'Completed'),\n series: trends.completed,\n sparkColor: 'var(--color-status-success-text)',\n },\n {\n key: 'certificates',\n icon: <Award className=\"h-5 w-5\" />,\n tint: 'bg-status-warning-bg-subtle text-status-warning-text',\n value: certificates.length,\n label: tr('buildmyiq.dashboard.certificatesEarned', 'Certificates earned'),\n series: trends.certificates,\n sparkColor: 'var(--color-status-warning-text)',\n },\n ];\n\n return (\n <PageLayout\n title={tr('buildmyiq.dashboard.welcomeBack', 'Welcome back, {{name}}', { name: firstName })}\n description={tr(\n 'buildmyiq.dashboard.subtitle',\n 'Your learning, teaching and credentialing — all in one workspace.'\n )}\n titleGradient\n aurora\n actions={\n currentUser ? (\n <div className=\"flex w-fit items-center gap-2.5 rounded-full border border-border-seam bg-bg-surface/60 py-1.5 ps-1.5 pe-3.5 backdrop-blur\">\n <Avatar\n name={displayName || null}\n seed={currentUser.id}\n src={currentUser.avatar}\n size=\"sm\"\n />\n {(displayName || roleLabel) && (\n <div className=\"min-w-0 leading-tight\">\n {displayName && (\n <p className=\"truncate text-sm font-medium text-text-primary\">{displayName}</p>\n )}\n {roleLabel && (\n <p className=\"max-w-[10rem] truncate text-xs text-text-secondary\">{roleLabel}</p>\n )}\n </div>\n )}\n </div>\n ) : undefined\n }\n >\n {isLoading ? (\n <LoadingView\n label={tr('buildmyiq.dashboard.loading', 'Loading your learning dashboard…')}\n />\n ) : (\n <div className=\"flex flex-col gap-8\" data-tour=\"buildmyiq-dashboard-content\">\n {/* KPI row */}\n <div\n className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\"\n data-tour=\"buildmyiq-dashboard-kpis\"\n >\n {kpis.map((k) => (\n <MetricsCard\n key={k.key}\n icon={<StatIcon tint={k.tint}>{k.icon}</StatIcon>}\n value={k.value}\n label={k.label}\n featured={k.featured}\n pill={\n k.series.length >= 2 ? (\n <Sparkline data={k.series} color={k.sparkColor} />\n ) : undefined\n }\n />\n ))}\n </div>\n\n {/* Getting-started checklist (BOFF-3095): hidden once the workspace activates */}\n <GettingStartedChecklist />\n\n {/* Continue learning */}\n <Section\n title={tr('buildmyiq.dashboard.continueLearning', 'Continue learning')}\n description={tr(\n 'buildmyiq.dashboard.continueLearningDescription',\n 'Pick up where you left off.'\n )}\n action={\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.my_learning_clicked', { workspaceId });\n navigateTo('learning');\n }}\n data-tour=\"buildmyiq-dashboard-my-learning\"\n >\n {tr('buildmyiq.dashboard.myLearning', 'My learning')}{' '}\n <ArrowRight className=\"ms-1 h-4 w-4 rtl:-scale-x-100\" />\n </Button>\n }\n >\n {activeEnrollments.length === 0 ? (\n <EmptyState\n illustration=\"onboarding\"\n title={tr('buildmyiq.dashboard.noActiveCoursesTitle', 'No active courses yet')}\n description={tr(\n 'buildmyiq.dashboard.noActiveCoursesDescription',\n 'Browse the catalog and enrol in a course to start learning.'\n )}\n action={\n <Button\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.explore_catalog_clicked', {\n workspaceId,\n });\n navigateTo('catalog');\n }}\n data-tour=\"buildmyiq-dashboard-explore-catalog\"\n >\n {tr('common.explore', 'Explore the catalog')}\n </Button>\n }\n />\n ) : (\n <div\n className=\"grid grid-cols-1 gap-4 md:grid-cols-2\"\n data-tour=\"buildmyiq-dashboard-continue-cards\"\n >\n {activeEnrollments.slice(0, 4).map((e) => {\n const course = courseById.get(e.courseId);\n const title = course?.title ?? tr('buildmyiq.course.pageTitle', 'Course');\n const goToCourse = (ev?: MouseEvent) => {\n ev?.stopPropagation();\n navigateTo(`learning/${e.id}`);\n };\n return (\n <GlassCard\n key={e.id}\n treatment=\"plain\"\n className=\"group flex cursor-pointer flex-col overflow-hidden transition-shadow hover:shadow-lg sm:flex-row\"\n onClick={() => goToCourse()}\n >\n {/* Cover / play thumbnail */}\n <div className=\"relative h-36 w-full shrink-0 overflow-hidden bg-bg-sunken sm:h-auto sm:w-40\">\n {course?.cover?.downloadUrl ? (\n <img\n src={course.cover.downloadUrl}\n alt=\"\"\n className=\"h-full w-full object-cover transition-transform duration-500 group-hover:scale-105\"\n />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center bg-gradient-to-br from-[var(--color-accent-soft)] to-bg-sunken\">\n <BookOpen className=\"h-8 w-8 text-text-secondary opacity-60\" />\n </div>\n )}\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <span className=\"inline-flex h-11 w-11 items-center justify-center rounded-full bg-bg-surface/85 shadow-lg backdrop-blur transition-transform duration-200 group-hover:scale-110\">\n <Play className=\"ms-0.5 h-5 w-5 fill-current text-[var(--color-accent-primary)]\" />\n </span>\n </div>\n </div>\n\n {/* Content */}\n <div className=\"flex min-w-0 flex-1 flex-col gap-2 p-4\">\n <p className=\"line-clamp-1 font-semibold text-text-primary\">{title}</p>\n {course?.summary && (\n <p className=\"line-clamp-2 text-sm text-text-secondary\">\n {course.summary}\n </p>\n )}\n <div className=\"mt-auto space-y-2.5 pt-2\">\n <div className=\"flex items-center gap-2\">\n <MiniBar value={e.progressPct} label={title} className=\"flex-1\" />\n <span className=\"shrink-0 text-xs font-semibold tabular-nums text-text-secondary\">\n {e.progressPct}%\n </span>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate text-xs text-text-secondary\">\n {tr('common.active', 'Active {{time}}', {\n time: formatRelativeTime(e.lastActiveAt ?? e.enrolledAt),\n })}\n </span>\n <Button\n size=\"sm\"\n className=\"min-h-[40px] shrink-0\"\n onClick={(ev: MouseEvent) => {\n bus.emitCustom('buildmyiq.dashboard.continue_course_clicked', {\n workspaceId,\n enrollmentId: e.id,\n courseId: e.courseId,\n });\n goToCourse(ev);\n }}\n data-tour=\"buildmyiq-dashboard-continue-button\"\n >\n <Play className=\"me-1 h-4 w-4 fill-current\" />{' '}\n {tr('common.continue', 'Continue')}\n </Button>\n </div>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n )}\n </Section>\n\n {/* Explore catalog */}\n <Section\n title={tr('buildmyiq.dashboard.exploreCatalog', 'Explore the catalog')}\n description={tr(\n 'buildmyiq.dashboard.exploreCatalogDescription',\n 'Newly published courses in your workspace.'\n )}\n action={\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.browse_all_catalog_clicked', { workspaceId });\n navigateTo('catalog');\n }}\n data-tour=\"buildmyiq-dashboard-browse-all\"\n >\n {tr('common.browseAll', 'Browse all')}{' '}\n <ArrowRight className=\"ms-1 h-4 w-4 rtl:-scale-x-100\" />\n </Button>\n }\n >\n {courses.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr(\n 'buildmyiq.dashboard.noPublishedCoursesTitle',\n 'No published courses yet'\n )}\n description={tr(\n 'buildmyiq.dashboard.noPublishedCoursesDescription',\n 'Create and publish a course in Authoring to populate the catalog.'\n )}\n action={\n <Button\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.go_to_authoring_clicked', {\n workspaceId,\n });\n navigateTo('authoring');\n }}\n data-tour=\"buildmyiq-dashboard-go-authoring\"\n >\n {tr('common.create', 'Go to Authoring')}\n </Button>\n }\n />\n ) : (\n <div\n className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\"\n data-tour=\"buildmyiq-dashboard-catalog-grid\"\n >\n {courses.slice(0, 8).map((course) => (\n <CourseCard\n key={course.id}\n course={course}\n onClick={() => navigateTo(`catalog/${course.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n </div>\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CourseDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/catalog/CourseDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAC;AAkJ3C,eAAO,MAAM,gBAAgB,EAAE,EA+oB9B,CAAC"}
1
+ {"version":3,"file":"CourseDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/catalog/CourseDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAC;AAkJ3C,eAAO,MAAM,gBAAgB,EAAE,EAkpB9B,CAAC"}
@@ -268,7 +268,10 @@ var M = "buildmyiq-course", _e = "__ungrouped__", N = {
268
268
  className: "font-semibold tabular-nums text-text-primary",
269
269
  children: [U.progressPct, "%"]
270
270
  })]
271
- }), /* @__PURE__ */ O(ee, { value: U.progressPct })]
271
+ }), /* @__PURE__ */ O(ee, {
272
+ value: U.progressPct,
273
+ label: _("buildmyiq.course.enrol.yourProgress", "Your progress")
274
+ })]
272
275
  }),
273
276
  /* @__PURE__ */ k("div", {
274
277
  className: "flex items-center justify-between border-t border-border-seam pt-3 text-xs",