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,727 @@
1
+ // NOTE: Both SCORM 1.2 and SCORM 2004 drivers use pipwerks wrapper (dynamically imported)
2
+ // cmi5 uses @xapi/cmi5. No script loading needed here.
3
+
4
+ // Import core framework modules
5
+ import { eventBus } from './core/event-bus.js';
6
+
7
+ import * as CourseHelpers from './utilities/course-helpers.js';
8
+ import { createViewManager } from './utilities/view-manager.js';
9
+ import { courseConfig } from '../../course/course-config.js';
10
+ import { customIcons } from '../../course/icons.js';
11
+
12
+ // Import the central interaction type catalog (auto-discovers built-in + custom interactions)
13
+ import { getCreator, getRegisteredTypes } from './core/interaction-catalog.js';
14
+
15
+ // Import managers
16
+ import stateManager from './state/index.js';
17
+
18
+ import objectiveManager from './managers/objective-manager.js';
19
+ import interactionManager from './managers/interaction-manager.js';
20
+ import interactionRegistry from './managers/interaction-registry.js';
21
+ import * as AssessmentManager from './managers/assessment-manager.js';
22
+ import flagManager from './managers/flag-manager.js';
23
+ import accessibilityManager from './managers/accessibility-manager.js';
24
+ import engagementManager from './engagement/engagement-manager.js';
25
+
26
+ import commentManager from './managers/comment-manager.js';
27
+ import audioManager from './managers/audio-manager.js';
28
+ import videoManager from './managers/video-manager.js';
29
+ import * as NavigationActions from './navigation/NavigationActions.js';
30
+ import * as DocumentGallery from './navigation/document-gallery.js';
31
+ import * as AppState from './app/AppState.js';
32
+ import * as AppUI from './app/AppUI.js';
33
+ import * as AppActions from './app/AppActions.js';
34
+
35
+ // Interaction creators are auto-discovered via interaction-catalog.js
36
+ // No explicit imports needed - use getCreator('type') or window.CourseCode.createTypeQuestion
37
+
38
+ // Import UI components (programmatic APIs only - initialization handled by component-catalog)
39
+ import * as Modal from './components/ui-components/modal.js';
40
+ import * as AudioPlayer from './components/ui-components/audio-player.js';
41
+ import { announceToScreenReader } from './components/ui-components/index.js';
42
+ import { showNotification } from './components/ui-components/notifications.js';
43
+ import { updateProgress } from './components/ui-components/progress.js';
44
+
45
+ // Import utilities
46
+ import { ScrollTracker } from './utilities/scroll-tracker.js';
47
+ import { logger } from './utilities/logger.js';
48
+ import { iconManager } from './utilities/icons.js';
49
+ import { breakpointManager } from './utilities/breakpoint-manager.js';
50
+ import { initErrorReporter } from './utilities/error-reporter.js';
51
+ import { initDataReporter } from './utilities/data-reporter.js';
52
+ import { initCourseChannel } from './utilities/course-channel.js';
53
+ import { canvasSlide } from './utilities/canvas-slide.js';
54
+
55
+ // Expose framework modules globally IMMEDIATELY for bundled course slides
56
+ // This MUST happen before any slide code executes (which happens during glob import)
57
+ window.logger = logger;
58
+ window.CourseCode = {
59
+ // Managers
60
+ stateManager,
61
+ objectiveManager,
62
+ interactionManager,
63
+ interactionRegistry,
64
+ AssessmentManager,
65
+ flagManager,
66
+ accessibilityManager,
67
+ commentManager,
68
+ audioManager,
69
+ videoManager,
70
+ scoreManager: null, // Will be set during initialization if scoring is configured
71
+
72
+ // Actions
73
+ NavigationActions,
74
+ AppActions,
75
+ AppState,
76
+
77
+ // Interaction creators (dynamically from catalog - includes built-in + custom)
78
+ ...Object.fromEntries(
79
+ getRegisteredTypes()
80
+ .filter(type => type !== 'multiple-choice-single') // Skip alias
81
+ .map(type => {
82
+ const pascalName = type.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
83
+ .replace(/^[a-z]/, c => c.toUpperCase());
84
+ return [`create${pascalName}Question`, getCreator(type)];
85
+ })
86
+ ),
87
+
88
+ // UI components (programmatic APIs)
89
+ Modal,
90
+ announceToScreenReader,
91
+ showNotification,
92
+ updateProgress,
93
+
94
+ // Utilities
95
+ iconManager,
96
+ breakpointManager,
97
+ canvasSlide,
98
+
99
+ // Core
100
+ eventBus,
101
+ courseConfig
102
+ };
103
+
104
+ // --- Conditional Automation Module Loading ---
105
+ // The automation API is ONLY loaded when explicitly enabled via course config.
106
+ // During production builds (vite build), Vite replaces import.meta.env.MODE with 'production'
107
+ // and tree-shaking removes this entire block if the condition is always false.
108
+ //
109
+ // Safety: When import.meta.env is undefined (non-Vite environments), we allow automation
110
+ // only if explicitly enabled in config. This supports SCORM desktop testing apps.
111
+ const buildMode = import.meta?.env?.MODE;
112
+ const isProductionBuild = buildMode === 'production';
113
+ const automationEnabled = courseConfig.environment?.automation?.enabled === true;
114
+
115
+ // Store automation initialization promise for coordination
116
+ let automationInitPromise = null;
117
+
118
+ // Only load automation if:
119
+ // 1. NOT a production build AND
120
+ // 2. Explicitly enabled in course config
121
+ if (!isProductionBuild && automationEnabled) {
122
+ logger.debug('[Framework] Automation mode enabled (MODE:', buildMode || 'undefined', ')');
123
+
124
+ // Dynamic import ensures the automation code is loaded on-demand
125
+ // Store the promise so initializeCourseApplication can wait for it
126
+ automationInitPromise = import('./automation/index.js').then(({ initializeAutomation }) => {
127
+ initializeAutomation();
128
+ logger.debug('[Framework] Automation initialization complete');
129
+ }).catch(error => {
130
+ logger.error('[Framework] Failed to load automation module:', error);
131
+ });
132
+ }
133
+
134
+ // --- Global Form Submission Guard ---
135
+ // This listener prevents accidental form submissions, which cause a full page reload
136
+ // and lead to SCORM re-initialization errors (error 103). This is a critical
137
+ // safeguard for the single-page application architecture of the course.
138
+ window.addEventListener('submit', (event) => {
139
+ // Prevent the default submission behavior that reloads the page.
140
+ event.preventDefault();
141
+
142
+ const form = event.target;
143
+ const submitter = event.submitter;
144
+
145
+ // Construct submitter context for developer debugging.
146
+ let submitterInfo = 'N/A (Submission not triggered by a button)';
147
+ if (submitter) {
148
+ const type = submitter.getAttribute('type');
149
+ const tag = submitter.tagName.toLowerCase();
150
+ submitterInfo = `Tag: <${tag}>, Type: "${type || 'submit (default)'}", Text: "${submitter.textContent.trim()}"`;
151
+ }
152
+
153
+ // preventDefault() already blocks the dangerous action above.
154
+ // logger.fatal throws in DEV for visibility, logs warning in PROD to avoid crashing.
155
+ logger.fatal('Form submission blocked to prevent SCORM data corruption.', {
156
+ domain: 'framework',
157
+ operation: 'formSubmissionGuard',
158
+ form: form.id || '(no id)',
159
+ submitter: submitterInfo,
160
+ fix: 'Ensure all <button> elements inside <form> have type="button". Use data-action pattern instead of form submissions.'
161
+ });
162
+ });
163
+
164
+ // --- Global Anchor Tag Click Guard ---
165
+ // This listener prevents accidental navigation from <a> tags, which would also
166
+ // cause a page reload and corrupt the SCORM session.
167
+ window.addEventListener('click', (event) => {
168
+ const anchor = event.target.closest('a');
169
+
170
+ // If the click was not on an anchor tag, do nothing.
171
+ if (!anchor) {
172
+ return;
173
+ }
174
+
175
+ // Allow links designed to open in a new tab.
176
+ if (anchor.target === '_blank') {
177
+ return;
178
+ }
179
+
180
+ // Allow lightbox triggers - they handle their own click behavior
181
+ if (anchor.dataset.component === 'lightbox') {
182
+ return;
183
+ }
184
+
185
+ // Get the href to check its value.
186
+ const href = anchor.getAttribute('href');
187
+
188
+ // Allow hash-only anchors (in-page scrolling, e.g., #features)
189
+ if (href && href.startsWith('#')) {
190
+ return;
191
+ }
192
+
193
+ // Allow links inside lightbox containers (e.g., markdown content links)
194
+ if (anchor.closest('.lightbox-markdown') || anchor.closest('.lightbox-content')) {
195
+ // For external links, force open in new tab for safety
196
+ if (href && (href.startsWith('http://') || href.startsWith('https://'))) {
197
+ event.preventDefault();
198
+ window.open(href, '_blank', 'noopener,noreferrer');
199
+ }
200
+ return;
201
+ }
202
+
203
+ // If the href is empty, '#' or a real URL, and it's not a new tab, it's a problem.
204
+ // We prevent the default action to stop the navigation/reload.
205
+ event.preventDefault();
206
+
207
+ // preventDefault() already blocks the dangerous action above.
208
+ // logger.fatal throws in DEV for visibility, logs warning in PROD to avoid crashing.
209
+ logger.fatal('Anchor tag navigation blocked to prevent SCORM data corruption.', {
210
+ domain: 'framework',
211
+ operation: 'anchorClickGuard',
212
+ href: href || '(not set)',
213
+ text: anchor.textContent.trim(),
214
+ fix: 'Use NavigationActions.goToSlide(slideId) for internal navigation, or <button type="button"> with data-action pattern.'
215
+ });
216
+ });
217
+
218
+ function reportInitializationError(error) {
219
+ // Store error in AppState if initialized (defensive - AppState might not be initialized yet)
220
+ try {
221
+ if (AppState.isInitialized()) {
222
+ AppState.setInitializationError(error);
223
+ }
224
+ } catch (e) {
225
+ // AppState not initialized, that's ok - initialization failed early
226
+ if (import.meta.env.DEV) {
227
+ logger.debug('AppState not initialized during error reporting:', e.message);
228
+ }
229
+ }
230
+
231
+ // Check if this is a user-facing error (e.g., expired session)
232
+ // These should be shown to the user but NOT reported to error tracking
233
+ const isUserFacing = error.userFacing === true;
234
+
235
+ // Report via unified logger (unless user-facing)
236
+ if (!isUserFacing) {
237
+ logger.error(error.message, { domain: 'initialization', operation: 'initializeCourseApplication', stack: error.stack });
238
+ }
239
+
240
+ // Determine error type for appropriate messaging
241
+ const isSessionExpired = error.isSessionExpired === true;
242
+ const errorTitle = isSessionExpired ? 'Session Expired' : 'Initialization Error';
243
+ const errorMessage = isSessionExpired
244
+ ? error.message // Already user-friendly
245
+ : `Failed to initialize course: ${error.message}`;
246
+ const actionMessage = isSessionExpired
247
+ ? 'Close this window and launch the course again from your learning portal.'
248
+ : 'Please refresh the page.';
249
+
250
+ // Try to use the error modal if AppUI is initialized
251
+ // Otherwise fall back to inline HTML for early initialization errors
252
+ try {
253
+ if (AppUI.showErrorModal) {
254
+ AppUI.showErrorModal({
255
+ title: errorTitle,
256
+ message: errorMessage,
257
+ details: import.meta.env.DEV && !isSessionExpired ? error.stack : null,
258
+ showRefresh: !isSessionExpired, // Don't show refresh for expired sessions
259
+ showClose: false
260
+ });
261
+ return;
262
+ }
263
+ } catch (_e) {
264
+ // Modal system not available, fall back to inline HTML
265
+ }
266
+
267
+ // Fallback: Inline HTML for early errors before modal system is ready
268
+ const supportEmail = courseConfig.support?.email;
269
+ const supportHtml = !isSessionExpired && supportEmail
270
+ ? `<p>If the problem persists, contact support at <a href="mailto:${supportEmail}">${supportEmail}</a>.</p>`
271
+ : !isSessionExpired ? '<p>If the problem persists, contact support.</p>' : '';
272
+
273
+ // Use info styling for session expired (expected behavior), error for real errors
274
+ const calloutClass = isSessionExpired ? 'callout-info' : 'callout-danger';
275
+
276
+ const content = document.getElementById('content');
277
+ if (content) {
278
+ content.innerHTML = `
279
+ <div class="p-6 callout ${calloutClass}" role="alert" aria-live="assertive">
280
+ <h2>${errorTitle}</h2>
281
+ <p>${errorMessage}</p>
282
+ <p>${actionMessage}</p>
283
+ ${supportHtml}
284
+ </div>
285
+ `;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Attempt to resize the browser window to configured dimensions.
291
+ * Only works for popup windows - browsers block resize for main windows.
292
+ * Disabled when environment.autoResizeWindow is false.
293
+ */
294
+ function resizeWindowToConfig() {
295
+ const autoResize = courseConfig.environment?.autoResizeWindow;
296
+
297
+ // Skip if explicitly disabled
298
+ if (autoResize === false) {
299
+ logger.debug('[CourseInit] Window auto-resize disabled by config');
300
+ return;
301
+ }
302
+
303
+ // Get dimensions from config object or use defaults
304
+ const width = typeof autoResize === 'object' ? autoResize.width : 1024;
305
+ const height = typeof autoResize === 'object' ? autoResize.height : 768;
306
+
307
+ if (!width || !height) return;
308
+
309
+ try {
310
+ // Check if we're in a popup window (opener exists) or undersized window
311
+ const isPopup = window.opener !== null ||
312
+ (window.outerWidth < width || window.outerHeight < height);
313
+
314
+ if (isPopup) {
315
+ window.resizeTo(width, height);
316
+ // Center the window on screen after resize
317
+ const left = Math.max(0, (screen.width - width) / 2);
318
+ const top = Math.max(0, (screen.height - height) / 2);
319
+ window.moveTo(left, top);
320
+ logger.debug(`[CourseInit] Resized window to ${width}x${height}`);
321
+ }
322
+ } catch (_e) {
323
+ // Browser may block resize - this is expected for security
324
+ logger.debug('[CourseInit] Window resize blocked by browser (expected for non-popup windows)');
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Apply theme variant tokens as data attributes on <html>
330
+ *
331
+ * This bridges CSS custom properties to data attributes, enabling themes to configure
332
+ * global component styles (tabs, accordions, cards, etc.) via CSS tokens.
333
+ *
334
+ * Themes set tokens like: --tab-style: pills;
335
+ * This function reads them and applies: data-tab-style="pills" on <html>
336
+ *
337
+ * HTML-level overrides take precedence (existing data attributes are preserved).
338
+ */
339
+ function applyThemeVariants() {
340
+ const html = document.documentElement;
341
+ const styles = getComputedStyle(html);
342
+
343
+ // Apply course layout from config (before theme variants)
344
+ // Layouts: 'article' (default), 'traditional', 'focused', 'presentation', 'canvas'
345
+ const layout = courseConfig.layout || 'article';
346
+ if (!html.hasAttribute('data-layout')) {
347
+ html.setAttribute('data-layout', layout);
348
+ logger.debug(`[Layout] Applied data-layout="${layout}" from course config`);
349
+ }
350
+
351
+ // Apply sidebar enabled state from config
352
+ // For 'traditional' layout, sidebar is always enabled
353
+ // For other layouts, it's controlled by navigation.sidebar.enabled
354
+ const sidebarEnabled = layout === 'traditional'
355
+ ? true
356
+ : (courseConfig.navigation?.sidebar?.enabled ?? false);
357
+ html.setAttribute('data-sidebar-enabled', sidebarEnabled ? 'true' : 'false');
358
+ logger.debug(`[Layout] Applied data-sidebar-enabled="${sidebarEnabled}" from course config`);
359
+
360
+ // Nav button visibility — traditional layout always shows buttons
361
+ const showNavButtons = layout === 'traditional'
362
+ ? true
363
+ : (courseConfig.navigation?.footer?.showButtons ?? true);
364
+ html.setAttribute('data-nav-buttons', showNavButtons ? 'true' : 'false');
365
+ logger.debug(`[Layout] Applied data-nav-buttons="${showNavButtons}" from course config`);
366
+
367
+ // Header visibility — canvas layout always hides header
368
+ const headerEnabled = layout === 'canvas'
369
+ ? false
370
+ : (courseConfig.navigation?.header?.enabled ?? true);
371
+ html.setAttribute('data-header-enabled', headerEnabled ? 'true' : 'false');
372
+ logger.debug(`[Layout] Applied data-header-enabled="${headerEnabled}" from course config`);
373
+
374
+ // Map of CSS token names to their corresponding data attribute names
375
+ const variantTokens = [
376
+ { token: '--tab-style', attr: 'data-tab-style' },
377
+ { token: '--accordion-style', attr: 'data-accordion-style' },
378
+ { token: '--button-shape', attr: 'data-button-shape' },
379
+ { token: '--card-style', attr: 'data-card-style' },
380
+ { token: '--callout-style', attr: 'data-callout-style' },
381
+ { token: '--header-style', attr: 'data-header-style' },
382
+ { token: '--sidebar-style', attr: 'data-sidebar-style' },
383
+ { token: '--footer-style', attr: 'data-footer-style' }
384
+ ];
385
+
386
+ for (const { token, attr } of variantTokens) {
387
+ // Only apply if not already set in HTML (HTML overrides theme)
388
+ if (!html.hasAttribute(attr)) {
389
+ const value = styles.getPropertyValue(token).trim();
390
+ if (value) {
391
+ html.setAttribute(attr, value);
392
+ logger.debug(`[ThemeVariants] Applied ${attr}="${value}" from theme token`);
393
+ }
394
+ }
395
+ }
396
+ }
397
+
398
+ async function initializeCourseApplication() {
399
+ logger.debug('[CourseInit] Initializing course modules...');
400
+
401
+ try {
402
+ // 0. Set document title and description from course config
403
+ if (courseConfig.metadata?.title) {
404
+ document.title = courseConfig.metadata.title;
405
+ const titleElement = document.getElementById('page-title');
406
+ if (titleElement) titleElement.textContent = courseConfig.metadata.title;
407
+ }
408
+ if (courseConfig.metadata?.description) {
409
+ const descElement = document.getElementById('page-description');
410
+ if (descElement) descElement.setAttribute('content', courseConfig.metadata.description);
411
+ }
412
+
413
+ // 0a. Initialize error reporter (if configured) - must be early to catch init errors
414
+ initErrorReporter(courseConfig);
415
+
416
+ // 0b. Initialize data reporter (if configured) - must be early to capture all events
417
+ initDataReporter(courseConfig);
418
+
419
+ // 0c. Initialize course channel (if configured) - pub/sub transport for course-to-course comms
420
+ initCourseChannel(courseConfig);
421
+
422
+ // 0d. Validate access control (for external hosting / multi-tenant CDN)
423
+ // This MUST run early before any LMS initialization to reject unauthorized clients
424
+ if (courseConfig.accessControl?.clients) {
425
+ const { validateAccess, showUnauthorizedScreen } = await import('./utilities/access-control.js');
426
+ const accessResult = validateAccess();
427
+ if (!accessResult.valid) {
428
+ logger.warn('[AccessControl] Access denied:', accessResult.error);
429
+ showUnauthorizedScreen(accessResult.error);
430
+ return; // Halt initialization
431
+ }
432
+ if (accessResult.clientId) {
433
+ logger.debug(`[AccessControl] Client authorized: ${accessResult.clientId}`);
434
+ }
435
+ }
436
+
437
+ // 0e. Initialize breakpoint manager (must be early - before components render)
438
+ // Applies responsive .bp-* classes to <html> based on viewport width
439
+ breakpointManager.init();
440
+
441
+ // 0f. Attempt to resize window to configured dimensions (LMS popup windows)
442
+ resizeWindowToConfig();
443
+
444
+ // 0g. Apply theme variant tokens as data attributes (before components render)
445
+ applyThemeVariants();
446
+
447
+ // 0h. Register custom icons
448
+ if (customIcons) {
449
+ iconManager.registerAll(customIcons);
450
+ logger.debug('[IconManager] Registered custom icons');
451
+ }
452
+
453
+ // 0i. Run static validation in development mode BEFORE any initialization
454
+ // Uses __DEV__ (replaced at build time) instead of runtime check to enable
455
+ // tree-shaking - Rollup sees `if (false)` in prod and eliminates the entire block
456
+ // The typeof check prevents ReferenceError if __DEV__ wasn't defined at build time
457
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
458
+ try {
459
+ const { lintCourse } = await import('./dev/runtime-linter.js');
460
+ await lintCourse(courseConfig);
461
+ } catch (error) {
462
+ // Linter throws with formatted error message - show it and halt
463
+ reportInitializationError(error);
464
+ throw error;
465
+ }
466
+ }
467
+
468
+ // 0j. Wait for automation to initialize (if enabled) before proceeding
469
+ // This ensures interactions can register when they're created
470
+ if (automationInitPromise) {
471
+ logger.debug('[CourseInit] Waiting for automation initialization...');
472
+ await automationInitPromise;
473
+ }
474
+
475
+ // 1. LMS connection (required - no fallback)
476
+ // Handles driver init, lifecycle handlers, and xAPI service setup
477
+ stateManager.setCompatibilityMode(courseConfig.environment?.lmsCompatibilityMode || 'auto');
478
+ await stateManager.initializeConnection();
479
+
480
+ // 2. Set up state validation config BEFORE initializing stateManager
481
+ // This enables validation of stored LMS data against current course structure.
482
+ // In dev: throws on mismatch to catch stale data issues
483
+ // In prod: gracefully recovers to handle course updates
484
+ stateManager.setCourseValidationConfig({
485
+ structure: courseConfig.structure,
486
+ objectives: courseConfig.objectives,
487
+ version: courseConfig.metadata?.version
488
+ });
489
+
490
+ // 3. Initialize state manager (hydrates from LMS with validation)
491
+ stateManager.initialize();
492
+
493
+ // 4. Initialize all managers with their configurations
494
+ objectiveManager.initialize(courseConfig.objectives);
495
+ interactionManager.initialize();
496
+ accessibilityManager.initialize();
497
+ flagManager.initialize();
498
+ commentManager.initialize();
499
+ engagementManager.initialize(courseConfig); // Pass courseConfig for requirement lookups
500
+ audioManager.initialize(); // Initialize audio manager for narration support
501
+ videoManager.initialize(); // Initialize video manager for embedded video support
502
+
503
+ // Initialize score manager (if course-level scoring is configured)
504
+ // Must happen AFTER objectiveManager to allow loading existing scores
505
+ if (courseConfig.scoring) {
506
+ const scoreManagerModule = await import('./managers/score-manager.js');
507
+ const scoreManager = scoreManagerModule.default;
508
+ scoreManager.initialize(courseConfig.scoring);
509
+ // Expose scoreManager globally for course authors
510
+ window.CourseCode.scoreManager = scoreManager;
511
+ }
512
+
513
+ // 5. Initialize course helpers with the course configuration
514
+ CourseHelpers.init(courseConfig);
515
+
516
+ // 6. Load course structure
517
+ const slides = await CourseHelpers.getFlattenedSlides();
518
+ const menuTree = await CourseHelpers.getMenuTree();
519
+ const assessmentConfigs = await CourseHelpers.getAssessmentConfigs();
520
+
521
+ // 7. Initialize View Manager
522
+ const slideContainer = document.getElementById('slide-container');
523
+ if (!slideContainer) {
524
+ throw new Error('Framework error: #slide-container not found.');
525
+ }
526
+ const viewManager = createViewManager(slideContainer, 'main');
527
+
528
+ // Register all slides as views
529
+ slides.forEach(slide => {
530
+ const component = slide.component;
531
+
532
+ // REQUIRED: Validate engagement config exists in structure
533
+ if (!slide.engagement) {
534
+ throw new Error(
535
+ `Slide "${slide.id}" missing required 'engagement' configuration in course-config.js structure. ` +
536
+ 'Add "engagement: { required: false }" to the slide definition in courseConfig.structure.'
537
+ );
538
+ }
539
+
540
+ viewManager.registerView(slide.id, {
541
+ render: async (options) => {
542
+ // 1. Clear registry and initialize engagement for the new slide
543
+ interactionRegistry.clear();
544
+ engagementManager.initSlide(slide.id, slide.engagement);
545
+
546
+ const renderContext = {
547
+ ...options,
548
+ slideId: slide.id,
549
+ title: slide.title
550
+ };
551
+
552
+ // 2. The slide's render function is now responsible for creating and returning its own element
553
+ let slideElement;
554
+ try {
555
+ slideElement = await component.render(null, renderContext);
556
+ } catch (err) {
557
+ // Add slide context to error message
558
+ throw new Error(`Slide "${slide.id}" render() failed: ${err.message}`);
559
+ }
560
+
561
+ if (!slideElement) {
562
+ throw new Error(`Slide "${slide.id}" render() returned null/undefined. Must return a DOM element.`);
563
+ }
564
+
565
+ // 3. Declarative UI components will be initialized by ViewManager after render
566
+ // (Removed duplicate call here - ViewManager handles it in view-manager.js:84)
567
+
568
+ // 4. Finalize the interaction registry now that rendering is complete
569
+ interactionRegistry.setReady();
570
+
571
+ // 5. Return the element created by the slide
572
+ return slideElement;
573
+ },
574
+ onShow: (element, options) => {
575
+ // Original onShow logic
576
+ const tracker = new ScrollTracker('main#content', slide.id);
577
+ element._scrollTracker = tracker;
578
+ if (component.onShow) component.onShow(element, options);
579
+ },
580
+ onHide: (element) => {
581
+ // Cleanup scroll tracker
582
+ if (element._scrollTracker) {
583
+ element._scrollTracker.destroy();
584
+ element._scrollTracker = null;
585
+ }
586
+
587
+ // Cleanup engagement before hiding
588
+ engagementManager.cleanupSlide(slide.id);
589
+ if (component.onHide) component.onHide(element);
590
+ },
591
+ });
592
+ });
593
+
594
+ // 8. Initialize the main app controller and UI
595
+ AppState.initAppState();
596
+ AppUI.initAppUI();
597
+ AppActions.initAppActions();
598
+ AudioPlayer.setup(); // Initialize audio player UI in footer
599
+
600
+ // Listen for view changes to log them
601
+ eventBus.on('view:change', ({ view, context }) => {
602
+ logger.debug(`[ViewManager] View changed to '${view}'`, context);
603
+ });
604
+
605
+ // Listen for navigation changes to load/unload slide audio
606
+ // IMPORTANT: Must be registered BEFORE NavigationActions.init() to catch first slide
607
+ let slideAudioCompletedHandler = null;
608
+
609
+ eventBus.on('navigation:changed', async ({ toSlideId }) => {
610
+ // Clean up previous slide's audio completion listener
611
+ if (slideAudioCompletedHandler) {
612
+ eventBus.off('audio:completed', slideAudioCompletedHandler);
613
+ slideAudioCompletedHandler = null;
614
+ }
615
+
616
+ // Find the slide configuration
617
+ const slide = slides.find(s => s.id === toSlideId);
618
+ if (!slide) return;
619
+
620
+ // Check if slide has audio configuration
621
+ if (slide.audio && slide.audio.src) {
622
+ try {
623
+ await audioManager.load(slide.audio, toSlideId, 'slide');
624
+ logger.debug(`[AudioManager] Loaded audio for slide: ${toSlideId}`);
625
+ } catch (error) {
626
+ logger.error(`[AudioManager] Failed to load audio for slide ${toSlideId}:`, error);
627
+ // Continue - don't let audio errors break navigation
628
+ }
629
+
630
+ // Check if slideAudioComplete is required in engagement config
631
+ // This is the new pattern - audio gating is configured via engagement requirements
632
+ const hasSlideAudioRequirement = slide.engagement?.requirements?.some(
633
+ req => req.type === 'slideAudioComplete'
634
+ );
635
+
636
+ if (hasSlideAudioRequirement) {
637
+ slideAudioCompletedHandler = ({ contextId }) => {
638
+ if (contextId === toSlideId) {
639
+ engagementManager.trackSlideAudioComplete(toSlideId);
640
+ }
641
+ };
642
+ eventBus.on('audio:completed', slideAudioCompletedHandler);
643
+ }
644
+ } else {
645
+ // No audio for this slide - unload current audio
646
+ audioManager.unload();
647
+ }
648
+ });
649
+
650
+ // Unload audio before navigating away from a slide
651
+ eventBus.on('navigation:beforeChange', () => {
652
+ // Save position and pause (don't fully unload yet - navigation:changed will handle)
653
+ if (audioManager.hasAudio()) {
654
+ audioManager.pause();
655
+ }
656
+ });
657
+
658
+ // 9. Initialize Navigation (this will trigger the first slide load)
659
+ await NavigationActions.init(slides, viewManager, menuTree, assessmentConfigs);
660
+
661
+ // 10. Initialize Document Gallery (after navigation renders the menu)
662
+ await DocumentGallery.init(courseConfig);
663
+
664
+ // 11. Initialize Breadcrumbs (after navigation to catch first slide)
665
+ const { init: initBreadcrumbs } = await import('./navigation/Breadcrumbs.js');
666
+ initBreadcrumbs();
667
+
668
+ logger.debug('[CourseInit] Initialization complete');
669
+
670
+ // Signal to automation consumers (headless browser) that the framework is fully ready
671
+ if (window.CourseCodeAutomation) {
672
+ window.CourseCodeAutomation.ready = true;
673
+ }
674
+
675
+ // Hide loading indicator
676
+ AppUI.hideLoadingIndicator();
677
+
678
+ // --- Layer 3: SCORM Best Practice - Passive Beforeunload Warning ---
679
+ // This listener provides a warning but does not block reloads.
680
+ // It is enabled by default in production and disabled by default in development.
681
+ window.addEventListener('beforeunload', (event) => {
682
+ const isProduction = import.meta?.env?.MODE === 'production';
683
+ let guardEnabled = isProduction; // ON in production, OFF in development by default
684
+
685
+ // Allow course config to explicitly override the default
686
+ if (courseConfig.environment?.disableBeforeUnloadGuard === true) {
687
+ guardEnabled = false;
688
+ } else if (courseConfig.environment?.disableBeforeUnloadGuard === false) {
689
+ guardEnabled = true; // Explicitly enable it even in dev
690
+ }
691
+
692
+ // Automation config can also disable it
693
+ if (courseConfig.environment?.automation?.enabled &&
694
+ courseConfig.environment?.automation?.disableBeforeUnloadGuard) {
695
+ guardEnabled = false;
696
+ }
697
+
698
+ // If the guard is disabled for any reason, do nothing.
699
+ if (!guardEnabled) {
700
+ logger.debug('[Framework] Beforeunload warning is disabled.');
701
+ return;
702
+ }
703
+
704
+ // If the exit is intentional (via Exit button), allow it silently.
705
+ if (AppState.isExitIntentional()) {
706
+ logger.debug('[Framework] Intentional exit detected, allowing page unload.');
707
+ return;
708
+ }
709
+
710
+ // For unintentional exits (F5, browser close, etc.), trigger the browser's native confirmation dialog.
711
+ event.preventDefault();
712
+ event.returnValue = ''; // Required by modern browsers to trigger the dialog.
713
+ logger.warn('[Framework] Unintentional page unload detected. Showing browser confirmation.');
714
+ });
715
+
716
+ } catch (error) {
717
+ reportInitializationError(error);
718
+ throw error;
719
+ }
720
+ }
721
+
722
+ // Start initialization when DOM is ready
723
+ if (document.readyState === 'loading') {
724
+ document.addEventListener('DOMContentLoaded', initializeCourseApplication);
725
+ } else {
726
+ initializeCourseApplication();
727
+ }