@burdenoff/microfe-technospam 2026.1102.2 → 2026.1102.4

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 (94) hide show
  1. package/dist/generated/global-types.d.ts +307 -41
  2. package/dist/generated/global-types.d.ts.map +1 -1
  3. package/dist/generated/wspace-types.d.ts +1730 -8
  4. package/dist/generated/wspace-types.d.ts.map +1 -1
  5. package/dist/stories/fixtures/technospamFixtures.d.ts.map +1 -1
  6. package/dist/technospam/TechnoSpamRoot.d.ts.map +1 -1
  7. package/dist/technospam/TechnoSpamRoot.js +187 -178
  8. package/dist/technospam/TechnoSpamRoot.js.map +1 -1
  9. package/dist/technospam/TechnoSpamRoutes.d.ts.map +1 -1
  10. package/dist/technospam/TechnoSpamRoutes.js +32 -28
  11. package/dist/technospam/TechnoSpamRoutes.js.map +1 -1
  12. package/dist/technospam/api/assessmentApi.d.ts +183 -0
  13. package/dist/technospam/api/assessmentApi.d.ts.map +1 -0
  14. package/dist/technospam/api/assessmentApi.js +278 -0
  15. package/dist/technospam/api/assessmentApi.js.map +1 -0
  16. package/dist/technospam/api/technospamApi.js +141 -141
  17. package/dist/technospam/api/technospamApi.js.map +1 -1
  18. package/dist/technospam/components/CourseAssessments.d.ts +9 -0
  19. package/dist/technospam/components/CourseAssessments.d.ts.map +1 -0
  20. package/dist/technospam/components/CourseAssessments.js +66 -0
  21. package/dist/technospam/components/CourseAssessments.js.map +1 -0
  22. package/dist/technospam/components/MediaCover.js +2 -2
  23. package/dist/technospam/components/MediaCover.js.map +1 -1
  24. package/dist/technospam/components/MobileSectionTabs.d.ts +9 -0
  25. package/dist/technospam/components/MobileSectionTabs.d.ts.map +1 -0
  26. package/dist/technospam/components/MobileSectionTabs.js +43 -0
  27. package/dist/technospam/components/MobileSectionTabs.js.map +1 -0
  28. package/dist/technospam/components/PageLayout.d.ts +7 -1
  29. package/dist/technospam/components/PageLayout.d.ts.map +1 -1
  30. package/dist/technospam/components/PageLayout.js +40 -28
  31. package/dist/technospam/components/PageLayout.js.map +1 -1
  32. package/dist/technospam/components/StatusBadge.d.ts.map +1 -1
  33. package/dist/technospam/components/StatusBadge.js +8 -1
  34. package/dist/technospam/components/StatusBadge.js.map +1 -1
  35. package/dist/technospam/components/admin/AssessmentsAdminTab.d.ts +7 -0
  36. package/dist/technospam/components/admin/AssessmentsAdminTab.d.ts.map +1 -0
  37. package/dist/technospam/components/admin/AssessmentsAdminTab.js +421 -0
  38. package/dist/technospam/components/admin/AssessmentsAdminTab.js.map +1 -0
  39. package/dist/technospam/components/lesson/LessonKindFields.d.ts +3 -1
  40. package/dist/technospam/components/lesson/LessonKindFields.d.ts.map +1 -1
  41. package/dist/technospam/components/lesson/LessonKindFields.js +2 -2
  42. package/dist/technospam/components/lesson/LessonKindFields.js.map +1 -1
  43. package/dist/technospam/components/lesson/SaveCourseOffline.js +6 -6
  44. package/dist/technospam/hooks/useAssessmentIntegrity.d.ts +29 -0
  45. package/dist/technospam/hooks/useAssessmentIntegrity.d.ts.map +1 -0
  46. package/dist/technospam/hooks/useAssessmentIntegrity.js +57 -0
  47. package/dist/technospam/hooks/useAssessmentIntegrity.js.map +1 -0
  48. package/dist/technospam/hooks/usePhoneLayout.d.ts +18 -0
  49. package/dist/technospam/hooks/usePhoneLayout.d.ts.map +1 -0
  50. package/dist/technospam/hooks/usePhoneLayout.js +14 -0
  51. package/dist/technospam/hooks/usePhoneLayout.js.map +1 -0
  52. package/dist/technospam/pages/AdminAdminDashboardPagePage.d.ts.map +1 -1
  53. package/dist/technospam/pages/AdminAdminDashboardPagePage.js +365 -360
  54. package/dist/technospam/pages/AdminAdminDashboardPagePage.js.map +1 -1
  55. package/dist/technospam/pages/ApplicationsProblemsPagePage.d.ts.map +1 -1
  56. package/dist/technospam/pages/ApplicationsProblemsPagePage.js +89 -87
  57. package/dist/technospam/pages/ApplicationsProblemsPagePage.js.map +1 -1
  58. package/dist/technospam/pages/CoursesAssessmentPagePage.d.ts +9 -0
  59. package/dist/technospam/pages/CoursesAssessmentPagePage.d.ts.map +1 -0
  60. package/dist/technospam/pages/CoursesAssessmentPagePage.js +464 -0
  61. package/dist/technospam/pages/CoursesAssessmentPagePage.js.map +1 -0
  62. package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
  63. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +213 -211
  64. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
  65. package/dist/technospam/pages/CoursesCoursesPagePage.d.ts.map +1 -1
  66. package/dist/technospam/pages/CoursesCoursesPagePage.js +117 -81
  67. package/dist/technospam/pages/CoursesCoursesPagePage.js.map +1 -1
  68. package/dist/technospam/pages/CoursesMyLearningPagePage.js +1 -1
  69. package/dist/technospam/pages/CoursesMyLearningPagePage.js.map +1 -1
  70. package/dist/technospam/pages/DashboardDashboardPagePage.d.ts.map +1 -1
  71. package/dist/technospam/pages/DashboardDashboardPagePage.js +21 -17
  72. package/dist/technospam/pages/DashboardDashboardPagePage.js.map +1 -1
  73. package/dist/technospam/pages/HackathonsHackathonDetailsPagePage.js +3 -3
  74. package/dist/technospam/pages/HackathonsHackathonDetailsPagePage.js.map +1 -1
  75. package/dist/technospam/pages/MentorshipMentorProfilePagePage.js +4 -4
  76. package/dist/technospam/pages/MentorshipMentorProfilePagePage.js.map +1 -1
  77. package/dist/technospam/pages/MentorshipMentorsPagePage.d.ts.map +1 -1
  78. package/dist/technospam/pages/MentorshipMentorsPagePage.js +6 -4
  79. package/dist/technospam/pages/MentorshipMentorsPagePage.js.map +1 -1
  80. package/dist/technospam/pages/MentorshipSessionsPagePage.js +1 -1
  81. package/dist/technospam/pages/MentorshipSessionsPagePage.js.map +1 -1
  82. package/dist/technospam/types/index.d.ts +7 -0
  83. package/dist/technospam/types/index.d.ts.map +1 -1
  84. package/dist/technospam/utils/assessment.d.ts +64 -0
  85. package/dist/technospam/utils/assessment.d.ts.map +1 -0
  86. package/dist/technospam/utils/assessment.js +122 -0
  87. package/dist/technospam/utils/assessment.js.map +1 -0
  88. package/dist/technospam/utils/mobileTabs.d.ts +45 -0
  89. package/dist/technospam/utils/mobileTabs.d.ts.map +1 -0
  90. package/dist/technospam/utils/mobileTabs.js +70 -0
  91. package/dist/technospam/utils/mobileTabs.js.map +1 -0
  92. package/dist/test/matchMedia.d.ts +6 -0
  93. package/dist/test/matchMedia.d.ts.map +1 -0
  94. package/package.json +3 -3
@@ -0,0 +1,122 @@
1
+ //#region src/technospam/utils/assessment.ts
2
+ function e(e) {
3
+ return `/courses/assessment-page?assessmentId=${encodeURIComponent(e)}`;
4
+ }
5
+ function t(e) {
6
+ let t = Math.max(0, Math.floor(e)), n = Math.floor(t / 3600), r = Math.floor(t % 3600 / 60), i = t % 60, a = String(r).padStart(2, "0"), o = String(i).padStart(2, "0");
7
+ return n > 0 ? `${n}:${a}:${o}` : `${a}:${o}`;
8
+ }
9
+ function n(e) {
10
+ switch (e) {
11
+ case "NONE": return "A timed exam with no integrity monitoring.";
12
+ case "INTEGRITY_MONITORING": return "Your browser reports integrity events (leaving fullscreen, switching tabs or windows, copy and paste). Within the allowed number of events your result is verified automatically; above it, a reviewer checks the attempt before any certificate is issued.";
13
+ case "REVIEW_REQUIRED": return "Your browser reports integrity events (leaving fullscreen, switching tabs or windows, copy and paste), and a reviewer checks every attempt before a certificate is issued.";
14
+ default: return "";
15
+ }
16
+ }
17
+ function r(e) {
18
+ switch (e) {
19
+ case "NOT_REQUIRED": return "This assessment is not monitored.";
20
+ case "MONITORING": return "Integrity monitoring is active.";
21
+ case "PENDING_REVIEW": return "A reviewer will check this attempt before a certificate is issued.";
22
+ case "VERIFIED": return "The integrity check passed.";
23
+ case "FLAGGED": return "Too many integrity events were recorded, so a reviewer will check this attempt.";
24
+ case "REJECTED": return "A reviewer rejected this attempt, so it does not count toward a certificate.";
25
+ default: return "";
26
+ }
27
+ }
28
+ function i(e, t) {
29
+ let n = e.proctoringStatus === "VERIFIED" || e.proctoringStatus === "NOT_REQUIRED";
30
+ return t.issuesCertificate && e.passed === !0 && n && !e.certificateId;
31
+ }
32
+ function a(e) {
33
+ return e.status === "IN_PROGRESS" ? !1 : e.passed === !0 && e.proctoringStatus !== "REJECTED";
34
+ }
35
+ function o(e, t) {
36
+ return t.some(a) ? 0 : Math.max(0, e.maxAttempts - t.length);
37
+ }
38
+ function s(e) {
39
+ return e.filter((e) => e >= 0).length;
40
+ }
41
+ var c = () => ({
42
+ title: "",
43
+ description: "",
44
+ timeLimitMinutes: "30",
45
+ passingScore: "70",
46
+ maxAttempts: "1",
47
+ maxIntegrityViolations: "3",
48
+ proctoringMode: "INTEGRITY_MONITORING",
49
+ requireFullscreen: !0,
50
+ blockClipboard: !0,
51
+ shuffleQuestions: !0,
52
+ shuffleOptions: !0,
53
+ issuesCertificate: !0
54
+ });
55
+ function l(e) {
56
+ return {
57
+ title: e.title,
58
+ description: e.description ?? "",
59
+ timeLimitMinutes: String(e.timeLimitMinutes),
60
+ passingScore: String(e.passingScore),
61
+ maxAttempts: String(e.maxAttempts),
62
+ maxIntegrityViolations: String(e.maxIntegrityViolations),
63
+ proctoringMode: e.proctoringMode,
64
+ requireFullscreen: e.requireFullscreen,
65
+ blockClipboard: e.blockClipboard,
66
+ shuffleQuestions: e.shuffleQuestions,
67
+ shuffleOptions: e.shuffleOptions,
68
+ issuesCertificate: e.issuesCertificate
69
+ };
70
+ }
71
+ function u(e, t, n) {
72
+ if (!/^\d+$/.test(e.trim())) return null;
73
+ let r = Number(e.trim());
74
+ return r >= t && r <= n ? r : null;
75
+ }
76
+ function d(e) {
77
+ let t = e.title.trim();
78
+ if (!t) return {
79
+ ok: !1,
80
+ error: "A title is required."
81
+ };
82
+ let n = u(e.timeLimitMinutes, 1, 480);
83
+ if (n === null) return {
84
+ ok: !1,
85
+ error: "Time limit must be 1 to 480 minutes."
86
+ };
87
+ let r = u(e.passingScore, 1, 100);
88
+ if (r === null) return {
89
+ ok: !1,
90
+ error: "Pass mark must be 1 to 100%."
91
+ };
92
+ let i = u(e.maxAttempts, 1, 10);
93
+ if (i === null) return {
94
+ ok: !1,
95
+ error: "Attempts must be 1 to 10."
96
+ };
97
+ let a = u(e.maxIntegrityViolations, 0, 50);
98
+ return a === null ? {
99
+ ok: !1,
100
+ error: "Allowed integrity events must be 0 to 50."
101
+ } : {
102
+ ok: !0,
103
+ input: {
104
+ title: t,
105
+ description: e.description.trim() || null,
106
+ timeLimitMinutes: n,
107
+ passingScore: r,
108
+ maxAttempts: i,
109
+ maxIntegrityViolations: a,
110
+ proctoringMode: e.proctoringMode,
111
+ requireFullscreen: e.requireFullscreen,
112
+ blockClipboard: e.blockClipboard,
113
+ shuffleQuestions: e.shuffleQuestions,
114
+ shuffleOptions: e.shuffleOptions,
115
+ issuesCertificate: e.issuesCertificate
116
+ }
117
+ };
118
+ }
119
+ //#endregion
120
+ export { s as answeredCount, e as assessmentPath, d as assessmentSettingsToInput, o as attemptsLeft, i as certificatePending, l as draftFromAssessment, c as emptyAssessmentDraft, t as formatClock, n as proctoringModeDescription, r as proctoringStatusDescription };
121
+
122
+ //# sourceMappingURL=assessment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assessment.js","names":[],"sources":["../../../src/technospam/utils/assessment.ts"],"sourcesContent":["import type {\n TecAssessment,\n TecAssessmentAttempt,\n TecProctoringMode,\n TecProctoringStatus,\n} from '../api/assessmentApi';\n\n/**\n * Pure helpers for the certification assessment pages (BOFF-4427). They only\n * describe what the server decided; the server owns timing, grading and the\n * proctoring status.\n */\n\n/** MFE-relative route of an assessment: `/courses/assessment-page?assessmentId=<id>`. */\nexport function assessmentPath(assessmentId: string): string {\n return `/courses/assessment-page?assessmentId=${encodeURIComponent(assessmentId)}`;\n}\n\n/** `mm:ss` (or `h:mm:ss`) countdown text for a number of seconds. */\nexport function formatClock(totalSeconds: number): string {\n const safe = Math.max(0, Math.floor(totalSeconds));\n const hours = Math.floor(safe / 3600);\n const minutes = Math.floor((safe % 3600) / 60);\n const seconds = safe % 60;\n const mm = String(minutes).padStart(2, '0');\n const ss = String(seconds).padStart(2, '0');\n return hours > 0 ? `${hours}:${mm}:${ss}` : `${mm}:${ss}`;\n}\n\n/** What the learner is told about the integrity rules before starting. */\nexport function proctoringModeDescription(mode: TecProctoringMode): string {\n switch (mode) {\n case 'NONE':\n return 'A timed exam with no integrity monitoring.';\n case 'INTEGRITY_MONITORING':\n return 'Your browser reports integrity events (leaving fullscreen, switching tabs or windows, copy and paste). Within the allowed number of events your result is verified automatically; above it, a reviewer checks the attempt before any certificate is issued.';\n case 'REVIEW_REQUIRED':\n return 'Your browser reports integrity events (leaving fullscreen, switching tabs or windows, copy and paste), and a reviewer checks every attempt before a certificate is issued.';\n default:\n return '';\n }\n}\n\n/** Plain-language meaning of an attempt's proctoring status. */\nexport function proctoringStatusDescription(status: TecProctoringStatus): string {\n switch (status) {\n case 'NOT_REQUIRED':\n return 'This assessment is not monitored.';\n case 'MONITORING':\n return 'Integrity monitoring is active.';\n case 'PENDING_REVIEW':\n return 'A reviewer will check this attempt before a certificate is issued.';\n case 'VERIFIED':\n return 'The integrity check passed.';\n case 'FLAGGED':\n return 'Too many integrity events were recorded, so a reviewer will check this attempt.';\n case 'REJECTED':\n return 'A reviewer rejected this attempt, so it does not count toward a certificate.';\n default:\n return '';\n }\n}\n\n/** Whether the attempt has earned a certificate that is not yet issued (claimable retry). */\nexport function certificatePending(\n attempt: Pick<TecAssessmentAttempt, 'passed' | 'proctoringStatus' | 'certificateId'>,\n assessment: Pick<TecAssessment, 'issuesCertificate'>\n): boolean {\n const cleared =\n attempt.proctoringStatus === 'VERIFIED' || attempt.proctoringStatus === 'NOT_REQUIRED';\n return (\n assessment.issuesCertificate && attempt.passed === true && cleared && !attempt.certificateId\n );\n}\n\n/** Whether an earlier attempt already settles the assessment (mirrors the server rule). */\nexport function blocksRetake(\n attempt: Pick<TecAssessmentAttempt, 'status' | 'passed' | 'proctoringStatus'>\n): boolean {\n if (attempt.status === 'IN_PROGRESS') return false;\n return attempt.passed === true && attempt.proctoringStatus !== 'REJECTED';\n}\n\n/** Attempts the learner may still start (0 when one is in progress or the exam is settled). */\nexport function attemptsLeft(\n assessment: Pick<TecAssessment, 'maxAttempts'>,\n attempts: readonly Pick<TecAssessmentAttempt, 'status' | 'passed' | 'proctoringStatus'>[]\n): number {\n if (attempts.some(blocksRetake)) return 0;\n return Math.max(0, assessment.maxAttempts - attempts.length);\n}\n\n/** Number of answered questions in a display-order answer list (-1 = unanswered). */\nexport function answeredCount(answers: readonly number[]): number {\n return answers.filter((answer) => answer >= 0).length;\n}\n\n/** Author form state for an assessment (numbers kept as text while editing). */\nexport interface AssessmentSettingsDraft {\n title: string;\n description: string;\n timeLimitMinutes: string;\n passingScore: string;\n maxAttempts: string;\n maxIntegrityViolations: string;\n proctoringMode: TecProctoringMode;\n requireFullscreen: boolean;\n blockClipboard: boolean;\n shuffleQuestions: boolean;\n shuffleOptions: boolean;\n issuesCertificate: boolean;\n}\n\nexport const emptyAssessmentDraft = (): AssessmentSettingsDraft => ({\n title: '',\n description: '',\n timeLimitMinutes: '30',\n passingScore: '70',\n maxAttempts: '1',\n maxIntegrityViolations: '3',\n proctoringMode: 'INTEGRITY_MONITORING',\n requireFullscreen: true,\n blockClipboard: true,\n shuffleQuestions: true,\n shuffleOptions: true,\n issuesCertificate: true,\n});\n\nexport function draftFromAssessment(assessment: TecAssessment): AssessmentSettingsDraft {\n return {\n title: assessment.title,\n description: assessment.description ?? '',\n timeLimitMinutes: String(assessment.timeLimitMinutes),\n passingScore: String(assessment.passingScore),\n maxAttempts: String(assessment.maxAttempts),\n maxIntegrityViolations: String(assessment.maxIntegrityViolations),\n proctoringMode: assessment.proctoringMode,\n requireFullscreen: assessment.requireFullscreen,\n blockClipboard: assessment.blockClipboard,\n shuffleQuestions: assessment.shuffleQuestions,\n shuffleOptions: assessment.shuffleOptions,\n issuesCertificate: assessment.issuesCertificate,\n };\n}\n\nexport type AssessmentSettingsResult =\n { ok: true; input: TecAssessmentInputShape } | { ok: false; error: string };\n\nexport interface TecAssessmentInputShape {\n title: string;\n description: string | null;\n timeLimitMinutes: number;\n passingScore: number;\n maxAttempts: number;\n maxIntegrityViolations: number;\n proctoringMode: TecProctoringMode;\n requireFullscreen: boolean;\n blockClipboard: boolean;\n shuffleQuestions: boolean;\n shuffleOptions: boolean;\n issuesCertificate: boolean;\n}\n\nfunction boundedInteger(text: string, min: number, max: number): number | null {\n if (!/^\\d+$/.test(text.trim())) return null;\n const value = Number(text.trim());\n return value >= min && value <= max ? value : null;\n}\n\n/** Validate the author form with the same bounds the server enforces. */\nexport function assessmentSettingsToInput(\n draft: AssessmentSettingsDraft\n): AssessmentSettingsResult {\n const title = draft.title.trim();\n if (!title) return { ok: false, error: 'A title is required.' };\n const timeLimitMinutes = boundedInteger(draft.timeLimitMinutes, 1, 480);\n if (timeLimitMinutes === null)\n return { ok: false, error: 'Time limit must be 1 to 480 minutes.' };\n const passingScore = boundedInteger(draft.passingScore, 1, 100);\n if (passingScore === null) return { ok: false, error: 'Pass mark must be 1 to 100%.' };\n const maxAttempts = boundedInteger(draft.maxAttempts, 1, 10);\n if (maxAttempts === null) return { ok: false, error: 'Attempts must be 1 to 10.' };\n const maxIntegrityViolations = boundedInteger(draft.maxIntegrityViolations, 0, 50);\n if (maxIntegrityViolations === null) {\n return { ok: false, error: 'Allowed integrity events must be 0 to 50.' };\n }\n return {\n ok: true,\n input: {\n title,\n description: draft.description.trim() || null,\n timeLimitMinutes,\n passingScore,\n maxAttempts,\n maxIntegrityViolations,\n proctoringMode: draft.proctoringMode,\n requireFullscreen: draft.requireFullscreen,\n blockClipboard: draft.blockClipboard,\n shuffleQuestions: draft.shuffleQuestions,\n shuffleOptions: draft.shuffleOptions,\n issuesCertificate: draft.issuesCertificate,\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The four learner journeys the phone layouts are built around (BOFF-4108,
3
+ * mockups technospam-18/19/22): Home, Courses, Practice and Mentors. Profile
4
+ * is the shell's own "More" tab, which already carries settings and the
5
+ * account menu, so it is not duplicated here.
6
+ */
7
+ export type MobileTabId = 'home' | 'courses' | 'practice' | 'mentors';
8
+ export interface MobileTabDef {
9
+ id: MobileTabId;
10
+ labelKey: string;
11
+ label: string;
12
+ /** Module-relative section prefix; every page under it lights the tab up. */
13
+ section: string;
14
+ /** Module-relative page the tab opens. */
15
+ home: string;
16
+ }
17
+ export declare const MOBILE_TABS: readonly MobileTabDef[];
18
+ /**
19
+ * Which phone tab owns `pathname`. The module index (`/technospam`) renders the
20
+ * dashboard, so it belongs to Home; anything outside the four sections
21
+ * (hackathons, analytics, study planner, admin) belongs to no tab.
22
+ */
23
+ export declare function activeMobileTab(pathname: string, basePath: string): MobileTabId | null;
24
+ /** Absolute path a tab navigates to, under the MFE's basePath. */
25
+ export declare function mobileTabTarget(tab: MobileTabDef, basePath: string): string;
26
+ /**
27
+ * Child ids of the "TechnoSpam" sidebar item that the bottom tabs replace in
28
+ * the installed app. Dropping them keeps the shell's "More" sheet from
29
+ * listing (and lighting up for) the same pages the tabs already cover.
30
+ */
31
+ export declare const NATIVE_TAB_COVERED_CHILD_IDS: ReadonlySet<string>;
32
+ /** Practice pages covered by the Practice tab; the rest of Applications stays in More. */
33
+ export declare const NATIVE_TAB_COVERED_GRANDCHILD_IDS: ReadonlySet<string>;
34
+ interface TreeItem {
35
+ id: string;
36
+ children?: TreeItem[];
37
+ }
38
+ /**
39
+ * Remove the tab-covered sections from the module's nav children. Sections
40
+ * left with no children (Applications without its practice pages keeps
41
+ * Portfolio, so it survives) are dropped too.
42
+ */
43
+ export declare function withoutTabCoveredItems<T extends TreeItem>(children: readonly T[]): T[];
44
+ export {};
45
+ //# sourceMappingURL=mobileTabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mobileTabs.d.ts","sourceRoot":"","sources":["../../../src/technospam/utils/mobileTabs.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAEtE,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,WAAW,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,WAAW,EAAE,SAAS,YAAY,EA6B9C,CAAC;AAOF;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAYtF;AAED,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,GAAG,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,EAAE,WAAW,CAAC,MAAM,CAK3D,CAAC;AAEH,0FAA0F;AAC1F,eAAO,MAAM,iCAAiC,EAAE,WAAW,CAAC,MAAM,CAIhE,CAAC;AAEH,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,EAAE,CAatF"}
@@ -0,0 +1,70 @@
1
+ //#region src/technospam/utils/mobileTabs.ts
2
+ var e = [
3
+ {
4
+ id: "home",
5
+ labelKey: "technospam.mobile.tabs.home",
6
+ label: "Home",
7
+ section: "dashboard",
8
+ home: "dashboard/dashboard-page"
9
+ },
10
+ {
11
+ id: "courses",
12
+ labelKey: "technospam.mobile.tabs.courses",
13
+ label: "Courses",
14
+ section: "courses",
15
+ home: "courses/courses-page"
16
+ },
17
+ {
18
+ id: "practice",
19
+ labelKey: "technospam.mobile.tabs.practice",
20
+ label: "Practice",
21
+ section: "applications",
22
+ home: "applications/problems-page"
23
+ },
24
+ {
25
+ id: "mentors",
26
+ labelKey: "technospam.mobile.tabs.mentors",
27
+ label: "Mentors",
28
+ section: "mentorship",
29
+ home: "mentorship/mentors-page"
30
+ }
31
+ ];
32
+ function t(e) {
33
+ return e.length > 1 && e.endsWith("/") ? e.slice(0, -1) : e;
34
+ }
35
+ function n(n, r) {
36
+ let i = t(r), a = (i && i !== "/" && (n === i || n.startsWith(`${i}/`)) ? n.slice(i.length) : n).replace(/^\/+/, "").replace(/\/+$/, "");
37
+ if (a === "") return "home";
38
+ for (let t of e) if (a === t.section || a.startsWith(`${t.section}/`)) return t.id;
39
+ return null;
40
+ }
41
+ var r = new Set([
42
+ "technospam-home",
43
+ "technospam-dashboard-section",
44
+ "technospam-courses",
45
+ "technospam-mentorship"
46
+ ]), i = new Set([
47
+ "technospam-applications-problems-page",
48
+ "technospam-applications-problem-details-page",
49
+ "technospam-applications-solutions-page"
50
+ ]);
51
+ function a(e) {
52
+ let t = [];
53
+ for (let n of e) {
54
+ if (r.has(n.id)) continue;
55
+ if (!n.children) {
56
+ t.push(n);
57
+ continue;
58
+ }
59
+ let e = n.children.filter((e) => !i.has(e.id));
60
+ e.length !== 0 && t.push({
61
+ ...n,
62
+ children: e
63
+ });
64
+ }
65
+ return t;
66
+ }
67
+ //#endregion
68
+ export { e as MOBILE_TABS, n as activeMobileTab, a as withoutTabCoveredItems };
69
+
70
+ //# sourceMappingURL=mobileTabs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mobileTabs.js","names":[],"sources":["../../../src/technospam/utils/mobileTabs.ts"],"sourcesContent":["import { joinPath } from './navigation';\n\n/**\n * The four learner journeys the phone layouts are built around (BOFF-4108,\n * mockups technospam-18/19/22): Home, Courses, Practice and Mentors. Profile\n * is the shell's own \"More\" tab, which already carries settings and the\n * account menu, so it is not duplicated here.\n */\nexport type MobileTabId = 'home' | 'courses' | 'practice' | 'mentors';\n\nexport interface MobileTabDef {\n id: MobileTabId;\n labelKey: string;\n label: string;\n /** Module-relative section prefix; every page under it lights the tab up. */\n section: string;\n /** Module-relative page the tab opens. */\n home: string;\n}\n\nexport const MOBILE_TABS: readonly MobileTabDef[] = [\n {\n id: 'home',\n labelKey: 'technospam.mobile.tabs.home',\n label: 'Home',\n section: 'dashboard',\n home: 'dashboard/dashboard-page',\n },\n {\n id: 'courses',\n labelKey: 'technospam.mobile.tabs.courses',\n label: 'Courses',\n section: 'courses',\n home: 'courses/courses-page',\n },\n {\n id: 'practice',\n labelKey: 'technospam.mobile.tabs.practice',\n label: 'Practice',\n section: 'applications',\n home: 'applications/problems-page',\n },\n {\n id: 'mentors',\n labelKey: 'technospam.mobile.tabs.mentors',\n label: 'Mentors',\n section: 'mentorship',\n home: 'mentorship/mentors-page',\n },\n];\n\n/** Strip a trailing slash so `/technospam/` and `/technospam` compare equal. */\nfunction trimBase(basePath: string): string {\n return basePath.length > 1 && basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;\n}\n\n/**\n * Which phone tab owns `pathname`. The module index (`/technospam`) renders the\n * dashboard, so it belongs to Home; anything outside the four sections\n * (hackathons, analytics, study planner, admin) belongs to no tab.\n */\nexport function activeMobileTab(pathname: string, basePath: string): MobileTabId | null {\n const base = trimBase(basePath);\n const rel =\n base && base !== '/' && (pathname === base || pathname.startsWith(`${base}/`))\n ? pathname.slice(base.length)\n : pathname;\n const clean = rel.replace(/^\\/+/, '').replace(/\\/+$/, '');\n if (clean === '') return 'home';\n for (const tab of MOBILE_TABS) {\n if (clean === tab.section || clean.startsWith(`${tab.section}/`)) return tab.id;\n }\n return null;\n}\n\n/** Absolute path a tab navigates to, under the MFE's basePath. */\nexport function mobileTabTarget(tab: MobileTabDef, basePath: string): string {\n return joinPath(basePath, tab.home);\n}\n\n/**\n * Child ids of the \"TechnoSpam\" sidebar item that the bottom tabs replace in\n * the installed app. Dropping them keeps the shell's \"More\" sheet from\n * listing (and lighting up for) the same pages the tabs already cover.\n */\nexport const NATIVE_TAB_COVERED_CHILD_IDS: ReadonlySet<string> = new Set([\n 'technospam-home',\n 'technospam-dashboard-section',\n 'technospam-courses',\n 'technospam-mentorship',\n]);\n\n/** Practice pages covered by the Practice tab; the rest of Applications stays in More. */\nexport const NATIVE_TAB_COVERED_GRANDCHILD_IDS: ReadonlySet<string> = new Set([\n 'technospam-applications-problems-page',\n 'technospam-applications-problem-details-page',\n 'technospam-applications-solutions-page',\n]);\n\ninterface TreeItem {\n id: string;\n children?: TreeItem[];\n}\n\n/**\n * Remove the tab-covered sections from the module's nav children. Sections\n * left with no children (Applications without its practice pages keeps\n * Portfolio, so it survives) are dropped too.\n */\nexport function withoutTabCoveredItems<T extends TreeItem>(children: readonly T[]): T[] {\n const out: T[] = [];\n for (const child of children) {\n if (NATIVE_TAB_COVERED_CHILD_IDS.has(child.id)) continue;\n if (!child.children) {\n out.push(child);\n continue;\n }\n const kept = child.children.filter((c) => !NATIVE_TAB_COVERED_GRANDCHILD_IDS.has(c.id));\n if (kept.length === 0) continue;\n out.push({ ...child, children: kept });\n }\n return out;\n}\n"],"mappings":";AAoBA,IAAa,IAAuC;CAClD;EACE,IAAI;EACJ,UAAU;EACV,OAAO;EACP,SAAS;EACT,MAAM;EACP;CACD;EACE,IAAI;EACJ,UAAU;EACV,OAAO;EACP,SAAS;EACT,MAAM;EACP;CACD;EACE,IAAI;EACJ,UAAU;EACV,OAAO;EACP,SAAS;EACT,MAAM;EACP;CACD;EACE,IAAI;EACJ,UAAU;EACV,OAAO;EACP,SAAS;EACT,MAAM;EACP;CACF;AAGD,SAAS,EAAS,GAA0B;AAC1C,QAAO,EAAS,SAAS,KAAK,EAAS,SAAS,IAAI,GAAG,EAAS,MAAM,GAAG,GAAG,GAAG;;AAQjF,SAAgB,EAAgB,GAAkB,GAAsC;CACtF,IAAM,IAAO,EAAS,EAAS,EAKzB,KAHJ,KAAQ,MAAS,QAAQ,MAAa,KAAQ,EAAS,WAAW,GAAG,EAAK,GAAG,IACzE,EAAS,MAAM,EAAK,OAAO,GAC3B,GACY,QAAQ,QAAQ,GAAG,CAAC,QAAQ,QAAQ,GAAG;AACzD,KAAI,MAAU,GAAI,QAAO;AACzB,MAAK,IAAM,KAAO,EAChB,KAAI,MAAU,EAAI,WAAW,EAAM,WAAW,GAAG,EAAI,QAAQ,GAAG,CAAE,QAAO,EAAI;AAE/E,QAAO;;AAaT,IAAa,IAAoD,IAAI,IAAI;CACvE;CACA;CACA;CACA;CACD,CAAC,EAGW,IAAyD,IAAI,IAAI;CAC5E;CACA;CACA;CACD,CAAC;AAYF,SAAgB,EAA2C,GAA6B;CACtF,IAAM,IAAW,EAAE;AACnB,MAAK,IAAM,KAAS,GAAU;AAC5B,MAAI,EAA6B,IAAI,EAAM,GAAG,CAAE;AAChD,MAAI,CAAC,EAAM,UAAU;AACnB,KAAI,KAAK,EAAM;AACf;;EAEF,IAAM,IAAO,EAAM,SAAS,QAAQ,MAAM,CAAC,EAAkC,IAAI,EAAE,GAAG,CAAC;AACnF,IAAK,WAAW,KACpB,EAAI,KAAK;GAAE,GAAG;GAAO,UAAU;GAAM,CAAC;;AAExC,QAAO"}
@@ -0,0 +1,6 @@
1
+ export declare function installMatchMedia(): void;
2
+ /** Set the width the next render's media queries are evaluated against. */
3
+ export declare function setTestViewportWidth(width: number): void;
4
+ /** Restore the default desktop width. */
5
+ export declare function resetTestViewport(): void;
6
+ //# sourceMappingURL=matchMedia.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matchMedia.d.ts","sourceRoot":"","sources":["../../src/test/matchMedia.ts"],"names":[],"mappings":"AAyBA,wBAAgB,iBAAiB,IAAI,IAAI,CAexC;AAED,2EAA2E;AAC3E,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAExD;AAED,yCAAyC;AACzC,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-technospam",
3
- "version": "2026.1102.2",
3
+ "version": "2026.1102.4",
4
4
  "description": "Burdenoff TechnoSpam Microfrontend",
5
5
  "type": "module",
6
6
  "files": [
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@apollo/client": "^4.0.11",
54
- "@burdenoff/fe-libs": "2026.801.1",
54
+ "@burdenoff/fe-libs": "2026.1029.2",
55
55
  "@codemirror/autocomplete": "6.20.3",
56
56
  "@codemirror/commands": "6.11.1",
57
57
  "@codemirror/lang-go": "6.0.1",
@@ -125,7 +125,7 @@
125
125
  "access": "public"
126
126
  },
127
127
  "overrides": {
128
- "@burdenoff/fe-libs": "2026.801.1",
128
+ "@burdenoff/fe-libs": "2026.1029.2",
129
129
  "lodash": ">=4.17.21",
130
130
  "lodash-es": ">=4.17.21",
131
131
  "dompurify": ">=3.3.4",