coursecode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/bin/cli.js +331 -0
  5. package/framework/assets/logo-coursecode-black.svg +14 -0
  6. package/framework/assets/logo-coursecode-white.svg +14 -0
  7. package/framework/assets/logo-coursecode.svg +14 -0
  8. package/framework/css/01-base.css +160 -0
  9. package/framework/css/02-layout.css +499 -0
  10. package/framework/css/accessibility.css +834 -0
  11. package/framework/css/components/accordions.css +710 -0
  12. package/framework/css/components/assessments.css +520 -0
  13. package/framework/css/components/audio-player.css +570 -0
  14. package/framework/css/components/badges.css +80 -0
  15. package/framework/css/components/breadcrumbs.css +87 -0
  16. package/framework/css/components/buttons.css +707 -0
  17. package/framework/css/components/callouts.css +1280 -0
  18. package/framework/css/components/cards.css +475 -0
  19. package/framework/css/components/carousel.css +193 -0
  20. package/framework/css/components/checkbox-group.css +123 -0
  21. package/framework/css/components/checklist.css +203 -0
  22. package/framework/css/components/collapse.css +96 -0
  23. package/framework/css/components/comparison.css +33 -0
  24. package/framework/css/components/content-image.css +36 -0
  25. package/framework/css/components/document-gallery.css +425 -0
  26. package/framework/css/components/dropdown.css +115 -0
  27. package/framework/css/components/embed-frame.css +142 -0
  28. package/framework/css/components/engagement.css +412 -0
  29. package/framework/css/components/features.css +35 -0
  30. package/framework/css/components/flip-cards.css +253 -0
  31. package/framework/css/components/footer.css +353 -0
  32. package/framework/css/components/forms.css +294 -0
  33. package/framework/css/components/hero.css +216 -0
  34. package/framework/css/components/images.css +528 -0
  35. package/framework/css/components/interactive-timeline.css +274 -0
  36. package/framework/css/components/intro-cards.css +30 -0
  37. package/framework/css/components/lightbox.css +666 -0
  38. package/framework/css/components/loading.css +65 -0
  39. package/framework/css/components/modals.css +235 -0
  40. package/framework/css/components/notifications.css +107 -0
  41. package/framework/css/components/quote.css +150 -0
  42. package/framework/css/components/sidebar.css +684 -0
  43. package/framework/css/components/slide-header.css +52 -0
  44. package/framework/css/components/spinner.css +62 -0
  45. package/framework/css/components/stats.css +44 -0
  46. package/framework/css/components/steps.css +232 -0
  47. package/framework/css/components/tables.css +90 -0
  48. package/framework/css/components/tabs.css +347 -0
  49. package/framework/css/components/timeline.css +154 -0
  50. package/framework/css/components/toggle.css +95 -0
  51. package/framework/css/components/tooltip.css +226 -0
  52. package/framework/css/components/video-player.css +438 -0
  53. package/framework/css/design-tokens.css +707 -0
  54. package/framework/css/framework.css +86 -0
  55. package/framework/css/interactions/accessibility.css +75 -0
  56. package/framework/css/interactions/base.css +92 -0
  57. package/framework/css/interactions/drag-drop.css +295 -0
  58. package/framework/css/interactions/fill-in-the-blank.css +236 -0
  59. package/framework/css/interactions/hotspots.css +69 -0
  60. package/framework/css/interactions/index.css +45 -0
  61. package/framework/css/interactions/interactive-image.css +359 -0
  62. package/framework/css/interactions/likert.css +126 -0
  63. package/framework/css/interactions/matching.css +354 -0
  64. package/framework/css/interactions/numeric-input.css +78 -0
  65. package/framework/css/interactions/sequencing.css +378 -0
  66. package/framework/css/interactions/true-false.css +177 -0
  67. package/framework/css/layouts/article.css +258 -0
  68. package/framework/css/layouts/base.css +30 -0
  69. package/framework/css/layouts/canvas.css +38 -0
  70. package/framework/css/layouts/focused.css +236 -0
  71. package/framework/css/layouts/index.css +29 -0
  72. package/framework/css/layouts/presentation.css +191 -0
  73. package/framework/css/layouts/traditional.css +52 -0
  74. package/framework/css/responsive.css +439 -0
  75. package/framework/css/utilities/accessibility-utils.css +59 -0
  76. package/framework/css/utilities/animations.css +419 -0
  77. package/framework/css/utilities/borders.css +72 -0
  78. package/framework/css/utilities/colors.css +76 -0
  79. package/framework/css/utilities/container.css +46 -0
  80. package/framework/css/utilities/decorative.css +442 -0
  81. package/framework/css/utilities/display.css +257 -0
  82. package/framework/css/utilities/flexbox.css +80 -0
  83. package/framework/css/utilities/grid.css +69 -0
  84. package/framework/css/utilities/icons.css +534 -0
  85. package/framework/css/utilities/lists.css +190 -0
  86. package/framework/css/utilities/spacing.css +167 -0
  87. package/framework/css/utilities/tables.css +81 -0
  88. package/framework/css/utilities/typography.css +159 -0
  89. package/framework/css/utilities/visibility.css +117 -0
  90. package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
  91. package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
  92. package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
  93. package/framework/docs/DATA_MODEL.md +409 -0
  94. package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
  95. package/framework/docs/USER_GUIDE.md +583 -0
  96. package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
  97. package/framework/docs/examples/cloudflare-data-worker.js +102 -0
  98. package/framework/docs/examples/cloudflare-error-worker.js +228 -0
  99. package/framework/index.html +175 -0
  100. package/framework/js/app/AppActions.js +410 -0
  101. package/framework/js/app/AppState.js +225 -0
  102. package/framework/js/app/AppUI.js +616 -0
  103. package/framework/js/assessment/AssessmentActions.js +615 -0
  104. package/framework/js/assessment/AssessmentFactory.js +471 -0
  105. package/framework/js/assessment/AssessmentState.js +322 -0
  106. package/framework/js/assessment/AssessmentUI.js +451 -0
  107. package/framework/js/automation/api-engagement.js +196 -0
  108. package/framework/js/automation/api-interactions.js +167 -0
  109. package/framework/js/automation/api.js +242 -0
  110. package/framework/js/automation/index.js +41 -0
  111. package/framework/js/components/interactions/drag-drop.js +884 -0
  112. package/framework/js/components/interactions/fill-in.js +535 -0
  113. package/framework/js/components/interactions/hotspot.js +702 -0
  114. package/framework/js/components/interactions/interaction-base.js +511 -0
  115. package/framework/js/components/interactions/likert.js +301 -0
  116. package/framework/js/components/interactions/matching.js +699 -0
  117. package/framework/js/components/interactions/multiple-choice.js +377 -0
  118. package/framework/js/components/interactions/numeric.js +271 -0
  119. package/framework/js/components/interactions/sequencing.js +423 -0
  120. package/framework/js/components/interactions/true-false.js +241 -0
  121. package/framework/js/components/ui-components/accordion.js +442 -0
  122. package/framework/js/components/ui-components/alert.js +88 -0
  123. package/framework/js/components/ui-components/audio-player.js +1193 -0
  124. package/framework/js/components/ui-components/callout.js +121 -0
  125. package/framework/js/components/ui-components/carousel.js +145 -0
  126. package/framework/js/components/ui-components/checkbox-group.js +87 -0
  127. package/framework/js/components/ui-components/checklist.js +40 -0
  128. package/framework/js/components/ui-components/collapse.js +114 -0
  129. package/framework/js/components/ui-components/comparison.js +30 -0
  130. package/framework/js/components/ui-components/conditional-display.js +150 -0
  131. package/framework/js/components/ui-components/content-image.js +41 -0
  132. package/framework/js/components/ui-components/dropdown.js +262 -0
  133. package/framework/js/components/ui-components/embed-frame.js +274 -0
  134. package/framework/js/components/ui-components/features.js +33 -0
  135. package/framework/js/components/ui-components/flip-card.js +230 -0
  136. package/framework/js/components/ui-components/form-validator.js +76 -0
  137. package/framework/js/components/ui-components/hero.js +49 -0
  138. package/framework/js/components/ui-components/index.js +12 -0
  139. package/framework/js/components/ui-components/interactive-image.js +235 -0
  140. package/framework/js/components/ui-components/interactive-timeline.js +285 -0
  141. package/framework/js/components/ui-components/intro-cards.js +35 -0
  142. package/framework/js/components/ui-components/lightbox.js +652 -0
  143. package/framework/js/components/ui-components/modal.js +386 -0
  144. package/framework/js/components/ui-components/notifications.js +145 -0
  145. package/framework/js/components/ui-components/progress.js +88 -0
  146. package/framework/js/components/ui-components/quote.js +41 -0
  147. package/framework/js/components/ui-components/stats.js +33 -0
  148. package/framework/js/components/ui-components/steps.js +41 -0
  149. package/framework/js/components/ui-components/tabs.js +255 -0
  150. package/framework/js/components/ui-components/timeline.js +42 -0
  151. package/framework/js/components/ui-components/toggle-group.js +73 -0
  152. package/framework/js/components/ui-components/tooltip.js +458 -0
  153. package/framework/js/components/ui-components/value-display.js +133 -0
  154. package/framework/js/components/ui-components/video-player.js +686 -0
  155. package/framework/js/core/component-catalog.js +121 -0
  156. package/framework/js/core/event-bus.js +178 -0
  157. package/framework/js/core/interaction-catalog.js +149 -0
  158. package/framework/js/dev/runtime-linter.js +1725 -0
  159. package/framework/js/drivers/cmi5-driver.js +768 -0
  160. package/framework/js/drivers/driver-factory.js +77 -0
  161. package/framework/js/drivers/driver-interface.js +110 -0
  162. package/framework/js/drivers/http-driver-base.js +241 -0
  163. package/framework/js/drivers/lti-driver.js +508 -0
  164. package/framework/js/drivers/proxy-driver.js +444 -0
  165. package/framework/js/drivers/scorm-12-driver.js +560 -0
  166. package/framework/js/drivers/scorm-2004-driver.js +775 -0
  167. package/framework/js/drivers/scorm-driver-base.js +112 -0
  168. package/framework/js/engagement/engagement-manager.js +404 -0
  169. package/framework/js/engagement/engagement-progress.js +191 -0
  170. package/framework/js/engagement/engagement-trackers.js +215 -0
  171. package/framework/js/engagement/requirement-strategies.js +268 -0
  172. package/framework/js/main.js +727 -0
  173. package/framework/js/managers/accessibility-manager.js +499 -0
  174. package/framework/js/managers/assessment-manager.js +230 -0
  175. package/framework/js/managers/audio-manager.js +944 -0
  176. package/framework/js/managers/comment-manager.js +88 -0
  177. package/framework/js/managers/flag-manager.js +86 -0
  178. package/framework/js/managers/interaction-manager.js +254 -0
  179. package/framework/js/managers/interaction-registry.js +96 -0
  180. package/framework/js/managers/objective-manager.js +423 -0
  181. package/framework/js/managers/score-manager.js +441 -0
  182. package/framework/js/managers/video-manager.js +536 -0
  183. package/framework/js/navigation/Breadcrumbs.js +234 -0
  184. package/framework/js/navigation/NavigationActions.js +1132 -0
  185. package/framework/js/navigation/NavigationState.js +276 -0
  186. package/framework/js/navigation/NavigationUI.js +574 -0
  187. package/framework/js/navigation/document-gallery.js +357 -0
  188. package/framework/js/navigation/navigation-helpers.js +175 -0
  189. package/framework/js/navigation/navigation-validators.js +174 -0
  190. package/framework/js/state/index.js +8 -0
  191. package/framework/js/state/lms-connection.js +482 -0
  192. package/framework/js/state/lms-error-utils.js +58 -0
  193. package/framework/js/state/state-commits.js +200 -0
  194. package/framework/js/state/state-domains.js +86 -0
  195. package/framework/js/state/state-manager.js +502 -0
  196. package/framework/js/state/state-validation.js +311 -0
  197. package/framework/js/state/transaction-log.js +41 -0
  198. package/framework/js/state/xapi-statement-service.js +325 -0
  199. package/framework/js/utilities/access-control.js +99 -0
  200. package/framework/js/utilities/breakpoint-manager.js +315 -0
  201. package/framework/js/utilities/canvas-slide.js +35 -0
  202. package/framework/js/utilities/conditional-display.js +388 -0
  203. package/framework/js/utilities/course-channel.js +214 -0
  204. package/framework/js/utilities/course-helpers.js +420 -0
  205. package/framework/js/utilities/data-reporter.js +273 -0
  206. package/framework/js/utilities/error-reporter.js +313 -0
  207. package/framework/js/utilities/hotspot-helper.js +341 -0
  208. package/framework/js/utilities/icons.js +348 -0
  209. package/framework/js/utilities/logger.js +92 -0
  210. package/framework/js/utilities/markdown-renderer.js +45 -0
  211. package/framework/js/utilities/scroll-tracker.js +68 -0
  212. package/framework/js/utilities/ui-initializer.js +146 -0
  213. package/framework/js/utilities/utilities.js +293 -0
  214. package/framework/js/utilities/view-manager.js +227 -0
  215. package/framework/js/validation/html-validators.js +422 -0
  216. package/framework/js/validation/scorm-validators.js +438 -0
  217. package/framework/js/vendor/pipwerks.js +931 -0
  218. package/framework/scripts/generate-narration.js +629 -0
  219. package/framework/scripts/tts-providers/azure-provider.js +178 -0
  220. package/framework/scripts/tts-providers/base-provider.js +81 -0
  221. package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
  222. package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
  223. package/framework/scripts/tts-providers/google-provider.js +272 -0
  224. package/framework/scripts/tts-providers/index.js +158 -0
  225. package/framework/scripts/tts-providers/openai-provider.js +143 -0
  226. package/framework/version.json +63 -0
  227. package/lib/authoring-api.js +919 -0
  228. package/lib/build-linter.js +450 -0
  229. package/lib/build-packaging.js +186 -0
  230. package/lib/build.js +88 -0
  231. package/lib/cloud.js +691 -0
  232. package/lib/convert.js +341 -0
  233. package/lib/course-parser.js +936 -0
  234. package/lib/course-writer.js +258 -0
  235. package/lib/create.js +248 -0
  236. package/lib/css-index.js +237 -0
  237. package/lib/dev.js +51 -0
  238. package/lib/export-content.js +1246 -0
  239. package/lib/headless-browser.js +413 -0
  240. package/lib/import.js +377 -0
  241. package/lib/index.js +80 -0
  242. package/lib/info.js +79 -0
  243. package/lib/interaction-formatters.js +568 -0
  244. package/lib/manifest/cmi5-manifest.js +63 -0
  245. package/lib/manifest/lti-tool-config.js +53 -0
  246. package/lib/manifest/manifest-factory.js +99 -0
  247. package/lib/manifest/scorm-12-manifest.js +61 -0
  248. package/lib/manifest/scorm-2004-manifest.js +94 -0
  249. package/lib/manifest/scorm-proxy-manifest.js +104 -0
  250. package/lib/manifest-parser.js +96 -0
  251. package/lib/mcp-prompts.js +753 -0
  252. package/lib/mcp-server.js +316 -0
  253. package/lib/narration.js +53 -0
  254. package/lib/pdf-structure.js +142 -0
  255. package/lib/preview-export.js +231 -0
  256. package/lib/preview-routes-api.js +662 -0
  257. package/lib/preview-routes-editing.js +159 -0
  258. package/lib/preview-routes-lms.js +230 -0
  259. package/lib/preview-server.js +564 -0
  260. package/lib/project-utils.js +269 -0
  261. package/lib/proxy-templates/proxy.html +68 -0
  262. package/lib/proxy-templates/scorm-bridge.js +112 -0
  263. package/lib/scaffold.js +193 -0
  264. package/lib/schema-extractor.js +361 -0
  265. package/lib/slide-source-editor.js +586 -0
  266. package/lib/stub-player/app-viewer.js +195 -0
  267. package/lib/stub-player/app.js +370 -0
  268. package/lib/stub-player/catalog-panel.js +312 -0
  269. package/lib/stub-player/config-panel.js +1303 -0
  270. package/lib/stub-player/content-generator.js +586 -0
  271. package/lib/stub-player/content-viewer.js +173 -0
  272. package/lib/stub-player/debug-panel.js +420 -0
  273. package/lib/stub-player/edit-mode.js +922 -0
  274. package/lib/stub-player/edit-utils.js +400 -0
  275. package/lib/stub-player/header-bar.js +354 -0
  276. package/lib/stub-player/interaction-editor.js +210 -0
  277. package/lib/stub-player/interactions-panel.js +565 -0
  278. package/lib/stub-player/lms-api.js +1094 -0
  279. package/lib/stub-player/login-screen.js +74 -0
  280. package/lib/stub-player/outline-mode.js +689 -0
  281. package/lib/stub-player/styles/_assessments-panel.css +245 -0
  282. package/lib/stub-player/styles/_base.css +89 -0
  283. package/lib/stub-player/styles/_catalog-icons.css +96 -0
  284. package/lib/stub-player/styles/_catalog-panel.css +291 -0
  285. package/lib/stub-player/styles/_config-panel.css +636 -0
  286. package/lib/stub-player/styles/_content-viewer.css +834 -0
  287. package/lib/stub-player/styles/_debug-panel.css +576 -0
  288. package/lib/stub-player/styles/_edit-mode.css +128 -0
  289. package/lib/stub-player/styles/_header-bar.css +343 -0
  290. package/lib/stub-player/styles/_interaction-editor.css +140 -0
  291. package/lib/stub-player/styles/_interactions-panel.css +1038 -0
  292. package/lib/stub-player/styles/_login-screen.css +102 -0
  293. package/lib/stub-player/styles/_outline-mode.css +752 -0
  294. package/lib/stub-player/styles.css +15 -0
  295. package/lib/stub-player.js +160 -0
  296. package/lib/test-data-reporting.js +176 -0
  297. package/lib/test-error-reporting.js +146 -0
  298. package/lib/token.js +86 -0
  299. package/lib/upgrade.js +257 -0
  300. package/lib/validation-rules.js +517 -0
  301. package/lib/vite-plugin-content-discovery.js +296 -0
  302. package/package.json +108 -0
  303. package/schemas/XMLSchema.dtd +402 -0
  304. package/schemas/adlcp_v1p3.xsd +111 -0
  305. package/schemas/adlnav_v1p3.xsd +61 -0
  306. package/schemas/adlseq_v1p3.xsd +93 -0
  307. package/schemas/common/anyElement.xsd +27 -0
  308. package/schemas/common/dataTypes.xsd +138 -0
  309. package/schemas/common/elementNames.xsd +767 -0
  310. package/schemas/common/elementTypes.xsd +786 -0
  311. package/schemas/common/rootElement.xsd +31 -0
  312. package/schemas/common/vocabTypes.xsd +345 -0
  313. package/schemas/common/vocabValues.xsd +257 -0
  314. package/schemas/datatypes.dtd +203 -0
  315. package/schemas/ims_xml.xsd +35 -0
  316. package/schemas/imscp_v1p1.xsd +368 -0
  317. package/schemas/imsss_v1p0.xsd +67 -0
  318. package/schemas/imsss_v1p0auxresource.xsd +19 -0
  319. package/schemas/imsss_v1p0control.xsd +20 -0
  320. package/schemas/imsss_v1p0delivery.xsd +17 -0
  321. package/schemas/imsss_v1p0limit.xsd +47 -0
  322. package/schemas/imsss_v1p0objective.xsd +67 -0
  323. package/schemas/imsss_v1p0random.xsd +16 -0
  324. package/schemas/imsss_v1p0rollup.xsd +46 -0
  325. package/schemas/imsss_v1p0seqrule.xsd +108 -0
  326. package/schemas/imsss_v1p0util.xsd +94 -0
  327. package/schemas/license.txt +17 -0
  328. package/schemas/lom.xsd +102 -0
  329. package/schemas/lomCustom.xsd +62 -0
  330. package/schemas/lomLoose.xsd +62 -0
  331. package/schemas/lomStrict.xsd +62 -0
  332. package/schemas/xml.xsd +81 -0
  333. package/template/.env.example +92 -0
  334. package/template/course/assets/audio/example-intro.mp3 +0 -0
  335. package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
  336. package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
  337. package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
  338. package/template/course/assets/docs/example_md_1.md +39 -0
  339. package/template/course/assets/docs/example_md_2.md +41 -0
  340. package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
  341. package/template/course/assets/docs/example_pdf_2.pdf +0 -0
  342. package/template/course/assets/images/course-architecture.svg +36 -0
  343. package/template/course/assets/images/logo.svg +14 -0
  344. package/template/course/assets/widgets/counter-demo.html +190 -0
  345. package/template/course/assets/widgets/gravity-painter.html +384 -0
  346. package/template/course/course-config.js +539 -0
  347. package/template/course/icons.js +19 -0
  348. package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
  349. package/template/course/slides/example-course-structure.js +138 -0
  350. package/template/course/slides/example-final-exam.js +144 -0
  351. package/template/course/slides/example-finishing.js +127 -0
  352. package/template/course/slides/example-interactions-showcase.js +615 -0
  353. package/template/course/slides/example-preview-tour.js +129 -0
  354. package/template/course/slides/example-remedial.js +143 -0
  355. package/template/course/slides/example-summary.js +103 -0
  356. package/template/course/slides/example-ui-showcase.js +1805 -0
  357. package/template/course/slides/example-welcome.js +123 -0
  358. package/template/course/slides/example-workflow.js +140 -0
  359. package/template/course/theme.css +165 -0
  360. package/template/eslint.config.js +47 -0
  361. package/template/package.json +28 -0
  362. package/template/vite.config.js +339 -0
@@ -0,0 +1,616 @@
1
+ import * as Modal from '../components/ui-components/modal.js';
2
+
3
+ import { setup as initNotifications, showNotification as showNotificationComponent } from '../components/ui-components/notifications.js';
4
+ import { eventBus } from '../core/event-bus.js';
5
+ import { courseConfig } from '../../../course/course-config.js';
6
+ import { logger } from '../utilities/logger.js';
7
+ import * as AppState from './AppState.js';
8
+ import { createLikertQuestion } from '../components/interactions/likert.js';
9
+ import { iconManager } from '../utilities/icons.js';
10
+ import { shouldBypassGating } from '../navigation/navigation-helpers.js';
11
+
12
+ // The HTML for the completion modal's feedback section is complex, so it's defined here as a template.
13
+ const completionFeedbackTemplate = `
14
+ <div class="completion-feedback-container">
15
+ <div id="completion-rating-section" class="feedback-section" hidden>
16
+ <div id="completion-rating-container"></div>
17
+ </div>
18
+ <div id="completion-comments-section" class="feedback-section" hidden>
19
+ <label for="completion-comments-textarea">Leave a comment (optional):</label>
20
+ <textarea id="completion-comments-textarea" class="feedback-textarea" rows="4" data-testid="completion-comments"></textarea>
21
+ </div>
22
+ </div>
23
+ `;
24
+
25
+ // Store the active rating interaction instance
26
+ let activeRatingInteraction = null;
27
+
28
+ const MODAL_DEFINITIONS = {
29
+ exit: {
30
+ title: 'Exit Course',
31
+ body: `
32
+ <p>Are you sure you want to exit the course?</p>
33
+ <p><strong>Your progress will be saved automatically.</strong> You can resume exactly where you left off when you return.</p>
34
+ `,
35
+ footer: `
36
+ <button class="btn btn-secondary" data-action="close-modal" data-testid="modal-exit-cancel">Cancel</button>
37
+ <button class="btn btn-primary" data-action="confirm-exit" data-testid="modal-exit-confirm">Exit Course</button>
38
+ `,
39
+ config: {
40
+ closeOnBackdrop: true,
41
+ closeOnEscape: true,
42
+ }
43
+ },
44
+ complete: {
45
+ title: 'Course Complete!',
46
+ body: `
47
+ <p><strong>Congratulations!</strong> You have successfully completed this course.</p>
48
+ <p>Your completion status and final score have been recorded in the Learning Management System.</p>
49
+ <p>When you click "Complete & Exit" below, this window will close and you will be returned to the LMS.</p>
50
+ ${completionFeedbackTemplate}
51
+ `,
52
+ footer: `
53
+ <button class="btn btn-secondary" data-action="close-modal" data-testid="modal-complete-cancel">Cancel</button>
54
+ <button class="btn btn-primary" data-action="confirm-complete" data-testid="modal-complete-confirm">Complete & Exit</button>
55
+ `,
56
+ config: {
57
+ closeOnBackdrop: true,
58
+ closeOnEscape: true,
59
+ }
60
+ },
61
+ postExit: {
62
+ title: 'Session Closed',
63
+ body: `
64
+ <p>Your progress has been saved in the LMS. It is now safe to close this window.</p>
65
+ <p>If this window does not close automatically, please close it manually and return to the LMS.</p>
66
+ `,
67
+ footer: '',
68
+ config: {
69
+ closeOnBackdrop: false,
70
+ closeOnEscape: false,
71
+ hideCloseButton: true,
72
+ }
73
+ },
74
+ restart: {
75
+ title: 'Restart Course',
76
+ body: `
77
+ <p class="font-bold text-error">Are you sure you want to restart the entire course?</p>
78
+ <p>This will permanently erase ALL of your progress, including assessment scores and engagement history. This action cannot be undone.</p>
79
+ `,
80
+ footer: `
81
+ <button class="btn btn-secondary" data-action="close-modal" data-testid="modal-restart-cancel">Cancel</button>
82
+ <button class="btn btn-primary" data-action="confirm-restart" data-testid="modal-restart-confirm">Restart Course</button>
83
+ `,
84
+ config: {
85
+ closeOnBackdrop: true,
86
+ closeOnEscape: true,
87
+ }
88
+ }
89
+ };
90
+
91
+ const appContainer = document.getElementById('app');
92
+ const loadingIndicator = document.getElementById('loading');
93
+ const footer = document.querySelector('.app-footer');
94
+ const exitButton = document.getElementById('exitBtn');
95
+ const prevButton = document.getElementById('prevBtn');
96
+ const nextButton = document.getElementById('nextBtn');
97
+
98
+
99
+
100
+ // Sidebar elements - cached after initialization
101
+ let sidebarToggle = null;
102
+ let sidebar = null;
103
+ let sidebarBackdrop = null;
104
+
105
+ // Footer display state - used to restore original display value
106
+ let originalFooterDisplay = null;
107
+
108
+ /**
109
+ * Initializes the AppUI module. This should be called once the DOM is ready.
110
+ * It prepares the modal and notification systems.
111
+ */
112
+ export function initAppUI() {
113
+ Modal.setup();
114
+
115
+ initNotifications();
116
+
117
+ logger.debug('AppUI initialized: Dynamic Modal and Notification systems are ready.');
118
+
119
+ _initSidebarToggle();
120
+ logger.debug('Sidebar toggle initialized.');
121
+
122
+ _initBranding();
123
+ logger.debug('Branding initialized.');
124
+
125
+ // Initialize footer button icons
126
+ _initFooterButtonIcons();
127
+
128
+ // Tooltips auto-initialize via event delegation - no manual init needed
129
+
130
+ // Listen for requests to prepare and show the completion modal
131
+ eventBus.on('ui:prepareCompletionModal', ({ promptForComments: _promptForComments, promptForRating: _promptForRating }) => {
132
+ // This event is now handled dynamically when the 'complete' modal is shown.
133
+ // The content is already part of the modal definition. We just need to show/hide sections.
134
+ // This logic will be triggered within the onOpen callback for the completion modal.
135
+ });
136
+
137
+ eventBus.on('ui:showModal', showModal);
138
+ eventBus.on('ui:hideModal', hideModal);
139
+
140
+
141
+
142
+ eventBus.on('ui:lockCourseForExit', _lockApplicationForExit);
143
+
144
+ // Listen for course status changes to update exit button appearance
145
+ eventBus.on('course:statusChanged', _handleCourseStatusChanged);
146
+
147
+ // Global Error Listener for SCORM Connection Issues
148
+ // This bridges the gap between low-level connection errors and user awareness.
149
+ eventBus.on('scorm:error', (errorData) => {
150
+ // Only notify for critical connection/save errors that affect data persistence
151
+ // We filter out minor warnings or handled recoveries to avoid noise.
152
+ const criticalOperations = ['Commit', 'SetValue', 'Initialize', 'Terminate'];
153
+
154
+ if (criticalOperations.includes(errorData.operation)) {
155
+ logger.error('[AppUI] Critical SCORM Error detected:', JSON.stringify(errorData, null, 2));
156
+
157
+ // Format a user-friendly message
158
+ let userMessage = 'Connection error: Your progress may not be saved.';
159
+
160
+ if (errorData.operation === 'Commit' || errorData.operation === 'SetValue') {
161
+ userMessage = 'Failed to save progress. Please check your internet connection.';
162
+ } else if (errorData.operation === 'Initialize') {
163
+ userMessage = 'Course failed to connect to the LMS. Progress will not be tracked.';
164
+ }
165
+
166
+ showNotificationComponent(userMessage, 'error', 5000);
167
+ }
168
+ });
169
+
170
+
171
+ }
172
+
173
+ /**
174
+ * Hides the loading indicator after the course has finished initializing.
175
+ */
176
+ export function hideLoadingIndicator() {
177
+ if (loadingIndicator) {
178
+ loadingIndicator.style.display = 'none';
179
+ AppState.setLoadingVisible(false);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Displays a modal by its key from the modal definitions.
185
+ * @param {string} modalKey - The key of the modal in MODAL_DEFINITIONS (e.g., 'exit', 'complete').
186
+ */
187
+ export function showModal(modalKey) {
188
+ const definition = MODAL_DEFINITIONS[modalKey];
189
+ if (!definition) {
190
+ throw new Error(`[AppUI] Modal definition not found for key: ${modalKey}`);
191
+ }
192
+
193
+ // Special handling for completion modal to show/hide feedback sections
194
+ if (modalKey === 'complete') {
195
+ definition.onOpen = () => {
196
+ const completionFeatures = courseConfig.completion || {};
197
+ const ratingSection = document.getElementById('completion-rating-section');
198
+ const commentsSection = document.getElementById('completion-comments-section');
199
+
200
+ if (ratingSection && completionFeatures.promptForRating) {
201
+ ratingSection.hidden = false;
202
+ _initCompletionModal();
203
+ }
204
+ if (commentsSection && completionFeatures.promptForComments) {
205
+ commentsSection.hidden = false;
206
+ }
207
+ };
208
+ }
209
+
210
+ AppState.setCurrentModal(modalKey);
211
+ Modal.show(definition);
212
+ }
213
+
214
+ /**
215
+ * Closes the currently active modal.
216
+ */
217
+ export function hideModal() {
218
+ AppState.clearCurrentModal();
219
+ Modal.hide();
220
+ }
221
+
222
+ /**
223
+ * Displays a notification message.
224
+ * @param {string} message - The message to display.
225
+ * @param {string} [type='info'] - The type of notification ('info', 'success', 'warning', 'error').
226
+ * @param {number} [duration=5000] - How long the notification should be visible (in ms).
227
+ */
228
+ export function showNotification(message, type = 'info', duration = 5000) {
229
+ showNotificationComponent(message, type, duration);
230
+ }
231
+
232
+ /**
233
+ * Displays a user-friendly error modal with support contact information.
234
+ * This is designed for production use when users encounter errors that may affect their progress.
235
+ *
236
+ * @param {object} options - Error modal configuration
237
+ * @param {string} [options.title='Something Went Wrong'] - Modal title
238
+ * @param {string} options.message - User-friendly error message
239
+ * @param {string} [options.details] - Technical details (shown in collapsible section in dev mode)
240
+ * @param {boolean} [options.showRefresh=true] - Whether to show refresh button
241
+ * @param {boolean} [options.showClose=false] - Whether to show close button (allows dismissing)
242
+ */
243
+ export function showErrorModal(options = {}) {
244
+ const {
245
+ title = 'Something Went Wrong',
246
+ message = 'An unexpected error occurred.',
247
+ details = null,
248
+ showRefresh = true,
249
+ showClose = false
250
+ } = options;
251
+
252
+ // Get support email from course config
253
+ const supportEmail = courseConfig.support?.email || null;
254
+ const supportPhone = courseConfig.support?.phone || null;
255
+
256
+ // Build contact section
257
+ let contactHtml = '';
258
+ if (supportEmail || supportPhone) {
259
+ const contactItems = [];
260
+ if (supportEmail) {
261
+ contactItems.push(`<a href="mailto:${supportEmail}">${supportEmail}</a>`);
262
+ }
263
+ if (supportPhone) {
264
+ contactItems.push(`<a href="tel:${supportPhone}">${supportPhone}</a>`);
265
+ }
266
+ contactHtml = `
267
+ <p class="mt-4"><strong>Need help?</strong> Contact support: ${contactItems.join(' or ')}</p>
268
+ `;
269
+ }
270
+
271
+ // Build details section (collapsible, only in dev mode or if explicitly requested)
272
+ let detailsHtml = '';
273
+ if (details && import.meta.env.DEV) {
274
+ detailsHtml = `
275
+ <details class="mt-4">
276
+ <summary class="cursor-pointer text-sm text-gray-600">Technical Details</summary>
277
+ <pre class="mt-2 p-3 bg-gray-100 rounded text-xs overflow-auto max-h-40">${details}</pre>
278
+ </details>
279
+ `;
280
+ }
281
+
282
+ // Build footer buttons
283
+ const footerButtons = [];
284
+ if (showClose) {
285
+ footerButtons.push('<button class="btn btn-secondary" data-action="close-modal" data-testid="modal-error-close">Close</button>');
286
+ }
287
+ if (showRefresh) {
288
+ footerButtons.push('<button class="btn btn-primary" data-action="refresh-page" data-testid="modal-error-refresh">Refresh Page</button>');
289
+ }
290
+
291
+ // Show the modal directly (no need for a static definition)
292
+ AppState.setCurrentModal('error');
293
+ Modal.show({
294
+ title,
295
+ body: `
296
+ <div class="callout callout-danger mb-4" role="alert">
297
+ <p>${message}</p>
298
+ </div>
299
+ ${contactHtml}
300
+ ${detailsHtml}
301
+ `,
302
+ footer: footerButtons.join('\n'),
303
+ config: {
304
+ closeOnBackdrop: showClose,
305
+ closeOnEscape: showClose,
306
+ hideCloseButton: !showClose,
307
+ }
308
+ });
309
+ }
310
+
311
+ /**
312
+ * Retrieves the main application container element.
313
+ * @returns {HTMLElement} The application container element.
314
+ */
315
+ export function getAppContainer() {
316
+ return appContainer;
317
+ }
318
+
319
+ /**
320
+ * Retrieves completion modal data (rating and comments) entered by the user.
321
+ * @returns {{rating: string|null, comment: string|null}} Object containing rating and comment values.
322
+ */
323
+ export function getCompletionModalData() {
324
+ let rating = null;
325
+
326
+ if (activeRatingInteraction) {
327
+ const response = activeRatingInteraction.getResponse();
328
+ // Extract the value for the 'overall' question
329
+ if (response && response['overall']) {
330
+ rating = response['overall'];
331
+ }
332
+ }
333
+
334
+ const commentTextarea = document.getElementById('completion-comments-textarea');
335
+ const comment = (commentTextarea && commentTextarea.value.trim())
336
+ ? commentTextarea.value
337
+ : null;
338
+
339
+ return { rating, comment };
340
+ }
341
+
342
+ /**
343
+ * Initializes the sidebar toggle functionality.
344
+ * @private
345
+ */
346
+ function _initSidebarToggle() {
347
+ sidebarToggle = document.getElementById('sidebar-toggle');
348
+ sidebar = document.getElementById('sidebar');
349
+ sidebarBackdrop = document.getElementById('sidebar-backdrop');
350
+
351
+ if (!sidebarToggle || !sidebar) {
352
+ throw new Error('Sidebar toggle elements not found.');
353
+ }
354
+
355
+ // Inject menu icon
356
+ const toggleIcon = sidebarToggle.querySelector('.toggle-icon');
357
+ if (toggleIcon) {
358
+ toggleIcon.innerHTML = iconManager.getIcon('menu', { size: 'lg' });
359
+ }
360
+
361
+ sidebarToggle.addEventListener('click', toggleSidebar);
362
+ if (sidebarBackdrop) {
363
+ sidebarBackdrop.addEventListener('click', closeSidebar);
364
+ }
365
+ document.addEventListener('keydown', (e) => {
366
+ if (e.key === 'Escape' && !sidebar.classList.contains('collapsed')) {
367
+ closeSidebar();
368
+ }
369
+ });
370
+ }
371
+
372
+ /**
373
+ * Initializes the interactive elements within the completion modal.
374
+ * This needs to be called each time the completion modal is opened,
375
+ * as its content is now dynamic.
376
+ * @private
377
+ */
378
+ function _initCompletionModal() {
379
+ const container = document.getElementById('completion-rating-container');
380
+ if (!container) return;
381
+
382
+ // Clear previous content
383
+ container.innerHTML = '';
384
+
385
+ // Create the likert interaction
386
+ activeRatingInteraction = createLikertQuestion({
387
+ id: 'course-rating',
388
+ prompt: 'How would you rate this course?',
389
+ scale: [
390
+ { value: '1', text: '1 Star' },
391
+ { value: '2', text: '2 Stars' },
392
+ { value: '3', text: '3 Stars' },
393
+ { value: '4', text: '4 Stars' },
394
+ { value: '5', text: '5 Stars' }
395
+ ],
396
+ questions: [
397
+ { id: 'overall', text: 'Overall Rating' }
398
+ ],
399
+ // No correctAnswers means it's a survey (always correct)
400
+ feedback: {
401
+ correct: 'Thank you for your rating!'
402
+ }
403
+ });
404
+
405
+ activeRatingInteraction.render(container);
406
+ }
407
+
408
+ export function toggleSidebar() {
409
+ const isCollapsed = sidebar.classList.toggle('collapsed');
410
+ AppState.setSidebarCollapsed(isCollapsed);
411
+ sidebarToggle.setAttribute('aria-expanded', String(!isCollapsed));
412
+ if (sidebarBackdrop) {
413
+ sidebarBackdrop.classList.toggle('visible', !isCollapsed);
414
+ }
415
+ }
416
+
417
+ export function closeSidebar() {
418
+ sidebar.classList.add('collapsed');
419
+ AppState.setSidebarCollapsed(true);
420
+ sidebarToggle.setAttribute('aria-expanded', 'false');
421
+ if (sidebarBackdrop) {
422
+ sidebarBackdrop.classList.remove('visible');
423
+ }
424
+ }
425
+
426
+ export function openSidebar() {
427
+ sidebar.classList.remove('collapsed');
428
+ sidebarToggle.setAttribute('aria-expanded', 'true');
429
+ if (sidebarBackdrop) {
430
+ sidebarBackdrop.classList.add('visible');
431
+ }
432
+ }
433
+
434
+ export function showFooter() {
435
+ if (!footer) return;
436
+ if (originalFooterDisplay === undefined || originalFooterDisplay === null) {
437
+ originalFooterDisplay = '';
438
+ }
439
+ footer.style.display = originalFooterDisplay;
440
+
441
+ // Re-enable sidebar toggle when footer is shown
442
+ if (sidebarToggle) {
443
+ sidebarToggle.disabled = false;
444
+ sidebarToggle.setAttribute('aria-disabled', 'false');
445
+ }
446
+ }
447
+
448
+ export function hideFooter() {
449
+ if (!footer) return;
450
+ if (originalFooterDisplay === undefined || originalFooterDisplay === null) {
451
+ originalFooterDisplay = footer.style.display || '';
452
+ }
453
+ footer.style.display = 'none';
454
+
455
+ // Disable sidebar toggle and close sidebar when footer is hidden
456
+ // This prevents navigation during assessment question/review views
457
+ if (sidebarToggle) {
458
+ sidebarToggle.disabled = true;
459
+ sidebarToggle.setAttribute('aria-disabled', 'true');
460
+ }
461
+ closeSidebar();
462
+ }
463
+
464
+ async function _initBranding() {
465
+ const brandContainer = document.getElementById('brand');
466
+ if (!brandContainer) throw new Error('Brand container #brand not found.');
467
+
468
+ const { branding = {} } = courseConfig;
469
+ const { logo, logoAlt, courseTitle: brandTitle } = branding;
470
+ const courseTitle = brandTitle || courseConfig.metadata?.title || 'Course';
471
+
472
+ let brandHTML = '';
473
+
474
+ // For SVG logos, inline them so they can inherit color via currentColor
475
+ if (logo && logo.endsWith('.svg')) {
476
+ try {
477
+ const response = await fetch(logo);
478
+ if (response.ok) {
479
+ const svgText = await response.text();
480
+ // Wrap in a container for styling
481
+ brandHTML += `<span class="logo" role="img" aria-label="${logoAlt || branding.companyName || 'Logo'}">${svgText}</span>`;
482
+ } else {
483
+ // Fallback to img if fetch fails
484
+ brandHTML += `<img src="${logo}" alt="${logoAlt || branding.companyName || 'Logo'}" class="logo" />`;
485
+ }
486
+ } catch {
487
+ // Fallback to img if fetch fails
488
+ brandHTML += `<img src="${logo}" alt="${logoAlt || branding.companyName || 'Logo'}" class="logo" />`;
489
+ }
490
+ } else if (logo) {
491
+ brandHTML += `<img src="${logo}" alt="${logoAlt || branding.companyName || 'Logo'}" class="logo" />`;
492
+ }
493
+
494
+ if (courseTitle) {
495
+ brandHTML += `<span class="brand-title">${courseTitle}</span>`;
496
+ }
497
+ brandContainer.innerHTML = brandHTML;
498
+ }
499
+
500
+ /**
501
+ * Initializes icons for footer buttons using iconManager.
502
+ * @private
503
+ */
504
+ function _initFooterButtonIcons() {
505
+ // Exit button icon - insert at the start of button
506
+ if (exitButton) {
507
+ exitButton.insertAdjacentHTML('afterbegin', iconManager.getIcon('log-out'));
508
+ }
509
+ }
510
+
511
+
512
+
513
+
514
+ /**
515
+ * Handles course status changes to update the exit button appearance.
516
+ * When on the last slide with completion requirements met, shows "Complete Course" button.
517
+ * @private
518
+ * @param {object} data - Status change data
519
+ * @param {string} data.completionStatus - Current completion status
520
+ * @param {boolean} data.isOnLastSlide - Whether user is on the last slide
521
+ */
522
+ function _handleCourseStatusChanged({ completionStatus, isOnLastSlide }) {
523
+ // In dev mode with gating disabled, always show completion button on last slide
524
+ const bypassGating = shouldBypassGating();
525
+ const showCompletionButton = isOnLastSlide && (completionStatus === 'completed' || bypassGating);
526
+
527
+ if (showCompletionButton) {
528
+ _setExitButtonToCompletionMode();
529
+ } else {
530
+ _setExitButtonToNormalMode();
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Animates the exit button content change with a fade transition.
536
+ * @private
537
+ * @param {Function} updateFn - Function that performs the actual DOM updates
538
+ */
539
+ function _animateExitButtonChange(updateFn) {
540
+ if (!exitButton) return;
541
+
542
+ // Add transition style and fade out
543
+ exitButton.style.transition = 'opacity 150ms ease-out, background-color 200ms ease, border-color 200ms ease';
544
+ exitButton.style.opacity = '0';
545
+
546
+ setTimeout(() => {
547
+ // Perform the update while hidden
548
+ updateFn();
549
+
550
+ // Fade back in
551
+ exitButton.style.opacity = '1';
552
+
553
+ // Clean up inline transition after animation completes
554
+ setTimeout(() => {
555
+ exitButton.style.transition = '';
556
+ }, 200);
557
+ }, 150);
558
+ }
559
+
560
+ /**
561
+ * Updates exit button to completion mode (green success style with trophy icon).
562
+ * @private
563
+ */
564
+ function _setExitButtonToCompletionMode() {
565
+ if (!exitButton) return;
566
+
567
+ // Skip animation if already in completion mode
568
+ if (exitButton.classList.contains('btn-success')) return;
569
+
570
+ _animateExitButtonChange(() => {
571
+ exitButton.innerHTML = iconManager.getIcon('trophy') + 'Complete Course';
572
+ exitButton.classList.remove('btn-secondary');
573
+ exitButton.classList.add('btn-success');
574
+ exitButton.setAttribute('data-tooltip', 'Complete and exit the course');
575
+ exitButton.setAttribute('data-testid', 'nav-complete');
576
+ });
577
+ }
578
+
579
+ /**
580
+ * Reverts exit button to normal mode (secondary style with log-out icon).
581
+ * @private
582
+ */
583
+ function _setExitButtonToNormalMode() {
584
+ if (!exitButton) return;
585
+
586
+ // Skip animation if already in normal mode
587
+ if (exitButton.classList.contains('btn-secondary')) return;
588
+
589
+ _animateExitButtonChange(() => {
590
+ exitButton.innerHTML = iconManager.getIcon('log-out') + 'Exit Course';
591
+ exitButton.classList.remove('btn-success');
592
+ exitButton.classList.add('btn-secondary');
593
+ exitButton.setAttribute('data-tooltip', 'Save progress and exit');
594
+ exitButton.setAttribute('data-testid', 'nav-exit');
595
+ });
596
+ }
597
+
598
+ function _lockApplicationForExit() {
599
+ AppState.setCourseExitLocked(true);
600
+ AppState.setExitInProgress(true);
601
+
602
+ if (exitButton) {
603
+ exitButton.removeAttribute('data-action');
604
+ exitButton.innerHTML = iconManager.getIcon('check-circle') + 'Course Complete';
605
+ exitButton.disabled = true;
606
+ exitButton.classList.remove('btn-secondary');
607
+ exitButton.classList.add('btn-success');
608
+ }
609
+ if (prevButton) prevButton.disabled = true;
610
+ if (nextButton) nextButton.disabled = true;
611
+ if (sidebarToggle) sidebarToggle.disabled = true;
612
+
613
+ closeSidebar();
614
+ hideModal(); // Hide any active modal
615
+ showModal('postExit'); // Show the final "safe to close" modal
616
+ }