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,574 @@
1
+ /**
2
+ * @file NavigationUI.js
3
+ * @description Renders and manages the UI components for course navigation.
4
+ * @author Seth
5
+ * @version 1.2.0
6
+ */
7
+
8
+ import { logger } from '../utilities/logger.js';
9
+ import { iconManager } from '../utilities/icons.js';
10
+
11
+ // Cache DOM elements for performance
12
+ const navMenu = document.getElementById('menu');
13
+ const prevButton = document.getElementById('prevBtn');
14
+ const nextButton = document.getElementById('nextBtn');
15
+ const engagementIndicator = document.getElementById('engagement-indicator');
16
+ const engagementProgress = engagementIndicator?.querySelector('.engagement-progress');
17
+ const engagementCheckmark = engagementIndicator?.querySelector('.engagement-checkmark');
18
+
19
+ // Header progress elements
20
+ const headerProgress = document.getElementById('header-progress');
21
+ const headerProgressText = headerProgress?.querySelector('.header-progress-text');
22
+ const headerProgressFill = headerProgress?.querySelector('.header-progress-fill');
23
+ const headerProgressBar = headerProgress?.querySelector('.header-progress-bar');
24
+
25
+ // Track if DOM has been validated
26
+ let isDOMValidated = false;
27
+
28
+ /**
29
+ * Validates that required DOM elements are present.
30
+ * @private
31
+ * @throws {Error} If required DOM elements are missing
32
+ */
33
+ function _ensureDOMReady() {
34
+ const missing = [];
35
+ if (!navMenu) missing.push('#menu');
36
+ if (!prevButton) missing.push('#prevBtn');
37
+ if (!nextButton) missing.push('#nextBtn');
38
+
39
+ if (missing.length > 0) {
40
+ logger.fatal(`NavigationUI: Required DOM elements not found: ${missing.join(', ')}. Check framework/index.html.`, { domain: 'navigation', operation: 'NavigationUI._ensureDOMReady' });
41
+ return;
42
+ }
43
+
44
+ // Inject icons into prev/next buttons using iconManager
45
+ // Insert chevron-left at the start of prevButton
46
+ prevButton.insertAdjacentHTML('afterbegin', iconManager.getIcon('chevron-left'));
47
+
48
+ // Append chevron-right at the end of nextButton
49
+ nextButton.insertAdjacentHTML('beforeend', iconManager.getIcon('chevron-right'));
50
+
51
+ isDOMValidated = true;
52
+ }
53
+
54
+ /**
55
+ * Renders the navigation sidebar menu based on the hierarchical menu tree structure.
56
+ * @param {object[]} menuTree - The hierarchical menu tree from getMenuTree().
57
+ * @param {string[]} visitedSlides - An array of slide IDs that have been visited.
58
+ * @param {Map<string, {allowed: boolean, message: string|null}>} accessibilityMap - A map of slide accessibility states.
59
+ * @throws {Error} If parameters are invalid or DOM elements are missing
60
+ */
61
+ export function renderMenu(menuTree, visitedSlides, accessibilityMap = new Map()) {
62
+ // Validate DOM on first use
63
+ if (!isDOMValidated) {
64
+ _ensureDOMReady();
65
+ }
66
+
67
+ // Validate parameters
68
+ if (!Array.isArray(menuTree)) {
69
+ throw new Error('NavigationUI.renderMenu: menuTree must be an array');
70
+ }
71
+ if (!Array.isArray(visitedSlides)) {
72
+ throw new Error('NavigationUI.renderMenu: visitedSlides must be an array');
73
+ }
74
+ if (!(accessibilityMap instanceof Map)) {
75
+ throw new Error('NavigationUI.renderMenu: accessibilityMap must be a Map');
76
+ }
77
+
78
+ navMenu.innerHTML = `<ul class="nav-list">${renderMenuItems(menuTree, visitedSlides, accessibilityMap)}</ul>`;
79
+
80
+ // Add or update sidebar footer with settings & exit button
81
+ const sidebar = navMenu.closest('.sidebar');
82
+ if (sidebar) {
83
+ let sidebarFooter = sidebar.querySelector('.nav-sidebar-footer');
84
+ if (!sidebarFooter) {
85
+ sidebarFooter = document.createElement('div');
86
+ sidebarFooter.className = 'nav-sidebar-footer';
87
+
88
+ // Horizontal footer bar: exit left, settings icons right
89
+ sidebarFooter.innerHTML = `
90
+ <button class="sidebar-exit-link" data-action="exit-course" data-testid="nav-sidebar-exit" data-tooltip="Exit Course">
91
+ ${iconManager.getIcon('log-out', { size: 'sm' })}
92
+ <span>Exit</span>
93
+ </button>
94
+ <div class="sidebar-settings-icons">
95
+ <button class="sidebar-icon-btn" data-action="toggle-theme" data-testid="sidebar-theme-toggle" data-tooltip="Toggle Dark Mode">
96
+ ${iconManager.getIcon('moon', { size: 'sm' })}
97
+ </button>
98
+ <button class="sidebar-icon-btn" data-action="toggle-font-size" data-testid="sidebar-font-size-toggle" data-tooltip="Toggle Font Size">
99
+ <span class="icon-text">A+</span>
100
+ </button>
101
+ <button class="sidebar-icon-btn" data-action="toggle-contrast" data-testid="sidebar-contrast-toggle" data-tooltip="Toggle High Contrast">
102
+ ${iconManager.getIcon('contrast', { size: 'sm' })}
103
+ </button>
104
+ </div>
105
+ `;
106
+ sidebar.appendChild(sidebarFooter);
107
+ }
108
+ }
109
+
110
+ // Tooltips auto-initialize via event delegation - no manual init needed
111
+
112
+ // Add event listeners for collapsible sections
113
+ const sectionToggles = navMenu.querySelectorAll('.section-toggle');
114
+ sectionToggles.forEach(toggle => {
115
+ toggle.addEventListener('click', (e) => {
116
+ e.preventDefault();
117
+ const section = toggle.closest('.nav-section');
118
+ const isExpanded = section.classList.toggle('expanded');
119
+ toggle.setAttribute('aria-expanded', isExpanded);
120
+ });
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Recursively checks if all children (slides) in a section are locked.
126
+ * @private
127
+ * @param {object[]} children - Array of child items (can include nested sections).
128
+ * @param {Map<string, {allowed: boolean, message: string|null}>} accessibilityMap - A map of slide accessibility states.
129
+ * @returns {boolean} True if all children are locked, false otherwise.
130
+ */
131
+ function areAllChildrenLocked(children, accessibilityMap) {
132
+ if (!children || children.length === 0) {
133
+ return false;
134
+ }
135
+
136
+ let allLocked = true;
137
+ let hasSlides = false;
138
+
139
+ for (const child of children) {
140
+ if (child.type === 'section') {
141
+ // Recursively check nested sections
142
+ if (!areAllChildrenLocked(child.children || [], accessibilityMap)) {
143
+ allLocked = false;
144
+ break;
145
+ }
146
+ } else {
147
+ // It's a slide - check if it's accessible
148
+ hasSlides = true;
149
+ const access = accessibilityMap.get(child.id);
150
+ if (!access || access.allowed !== false) {
151
+ // At least one child is accessible
152
+ allLocked = false;
153
+ break;
154
+ }
155
+ }
156
+ }
157
+
158
+ // Only return true if we found slides and they're all locked
159
+ return hasSlides && allLocked;
160
+ }
161
+
162
+ /**
163
+ * Recursively renders menu items (sections and slides).
164
+ * @private
165
+ * @param {object[]} items - Array of menu items (sections or slides).
166
+ * @param {string[]} visitedSlides - Array of visited slide IDs.
167
+ * @param {Map<string, {allowed: boolean, message: string|null}>} accessibilityMap - A map of slide accessibility states.
168
+ * @returns {string} HTML string for the menu items.
169
+ */
170
+ function renderMenuItems(items, visitedSlides, accessibilityMap) {
171
+ return items.map(item => {
172
+ if (item.type === 'section') {
173
+ const expandedClass = item.defaultExpanded ? 'expanded' : '';
174
+ const collapsibleClass = item.collapsible !== false ? 'collapsible' : '';
175
+
176
+ // Check if all children are locked
177
+ const allChildrenLocked = areAllChildrenLocked(item.children || [], accessibilityMap);
178
+ const allLockedClass = allChildrenLocked ? 'all-children-locked' : '';
179
+
180
+ // Debug logging
181
+ if (allChildrenLocked) {
182
+ logger.debug(`[NavigationUI] Section "${item.label}" (${item.id}) has all children locked`);
183
+ }
184
+
185
+ return `
186
+ <li class="nav-section ${expandedClass} ${collapsibleClass} ${allLockedClass}" data-section-id="${item.id}" data-testid="nav-section-${item.id}">
187
+ <button class="section-toggle" aria-expanded="${item.defaultExpanded}" aria-controls="section-${item.id}" data-testid="nav-section-toggle-${item.id}">
188
+ ${item.icon ? `<span class="section-icon" aria-hidden="true">${iconManager.getIcon(item.icon)}</span>` : ''}
189
+ <span class="section-label">${item.label}</span>
190
+ <span class="toggle-indicator" aria-hidden="true">${iconManager.getIcon('chevron-right')}</span>
191
+ </button>
192
+ <ul class="section-children" id="section-${item.id}">
193
+ ${renderMenuItems(item.children || [], visitedSlides, accessibilityMap)}
194
+ </ul>
195
+ </li>
196
+ `;
197
+ } else {
198
+ // Slide item
199
+ const visitedClass = visitedSlides.includes(item.id) ? 'visited' : '';
200
+ const access = accessibilityMap.get(item.id);
201
+ const isLocked = access && access.allowed === false;
202
+ const lockedClass = isLocked ? 'locked' : '';
203
+ const ariaDisabled = isLocked ? 'aria-disabled="true"' : '';
204
+ const tabIndex = isLocked ? 'tabindex="-1"' : '';
205
+
206
+ // Use JS tooltip via data-tooltip attribute (no .tooltip class needed)
207
+ const tooltipData = isLocked && access.message ? `data-tooltip="${access.message}"` : '';
208
+ const ariaLabel = isLocked && access.message
209
+ ? `aria-label="Go to ${item.label} (${access.message})"`
210
+ : `aria-label="Go to ${item.label}"`;
211
+
212
+ return `
213
+ <li class="nav-item ${visitedClass} ${lockedClass}" data-slide-id="${item.id}" data-slide-index="${item.slideIndex}" data-action="nav-menu-item" data-testid="nav-menu-item-${item.id}">
214
+ <button type="button" ${ariaLabel} ${ariaDisabled} ${tabIndex} ${tooltipData}>
215
+ ${item.icon ? `<span class="slide-icon" aria-hidden="true">${iconManager.getIcon(item.icon)}</span>` : ''}
216
+ <span class="slide-label">${item.label}</span>
217
+ ${isLocked ? `<span class="lock-icon" aria-hidden="true">${iconManager.getIcon('lock')}</span>` : ''}
218
+ </button>
219
+ </li>
220
+ `;
221
+ }
222
+ }).join('');
223
+ }
224
+
225
+ /**
226
+ * Highlights the active slide in the navigation menu and manages section expansion.
227
+ * Collapses all collapsible sections except the one containing the active slide.
228
+ * @param {string} slideId - The ID of the slide to mark as active.
229
+ * @throws {Error} If slideId is invalid
230
+ */
231
+ export function setActiveItem(slideId) {
232
+ if (!slideId || typeof slideId !== 'string') {
233
+ throw new Error(`NavigationUI.setActiveItem: Invalid slideId: ${slideId}`);
234
+ }
235
+ if (!navMenu) return;
236
+
237
+ // Find the active item and its parent section
238
+ const activeItem = navMenu.querySelector(`.nav-item[data-slide-id="${slideId}"]`);
239
+ const activeSection = activeItem?.closest('.nav-section');
240
+
241
+ // Update active state on all items
242
+ const items = navMenu.querySelectorAll('.nav-item');
243
+ items.forEach(item => {
244
+ const isActive = item.dataset.slideId === slideId;
245
+ item.classList.toggle('active', isActive);
246
+ item.querySelector('button').setAttribute('aria-current', isActive ? 'page' : 'false');
247
+ });
248
+
249
+ // Manage section expansion: collapse others, expand the active one
250
+ const sections = navMenu.querySelectorAll('.nav-section.collapsible');
251
+ sections.forEach(section => {
252
+ const toggle = section.querySelector('.section-toggle');
253
+ if (!toggle) return;
254
+
255
+ if (section === activeSection) {
256
+ // Expand the section containing the active slide
257
+ section.classList.add('expanded');
258
+ toggle.setAttribute('aria-expanded', 'true');
259
+ } else {
260
+ // Collapse other collapsible sections
261
+ section.classList.remove('expanded');
262
+ toggle.setAttribute('aria-expanded', 'false');
263
+ }
264
+ });
265
+ }
266
+
267
+ /**
268
+ * Updates the enabled/disabled state and ARIA attributes of the previous and next buttons.
269
+ * @param {object} config - Navigation state with isFirstSlide, isLastSlide, nextBlocked, nextBlockedMessage, engagementProgress.
270
+ */
271
+ export function updateNavButtonState(config) {
272
+ // New API: config object only
273
+ const { isFirstSlide, isLastSlide, nextBlocked = false, nextBlockedMessage = null, engagementProgress = null } = config;
274
+
275
+ // Update previous button (no tooltip - icon is self-explanatory)
276
+ if (prevButton) {
277
+ prevButton.disabled = isFirstSlide;
278
+ prevButton.setAttribute('aria-disabled', String(isFirstSlide));
279
+ }
280
+
281
+ // Update next button
282
+ if (nextButton) {
283
+ const shouldDisable = isLastSlide || nextBlocked;
284
+ nextButton.disabled = shouldDisable;
285
+ nextButton.setAttribute('aria-disabled', String(shouldDisable));
286
+
287
+ // Manage gated state for progressive ring indicator
288
+ // Only show ring when blocked due to engagement (has progress data and not complete)
289
+ if (nextBlocked && engagementProgress !== null && engagementProgress < 100) {
290
+ nextButton.classList.add('gated');
291
+ nextButton.classList.remove('engagement-complete');
292
+ nextButton.style.setProperty('--engagement-progress', engagementProgress);
293
+ } else if (!nextButton.classList.contains('engagement-complete')) {
294
+ // Clear gated state unless animation is playing
295
+ nextButton.classList.remove('gated');
296
+ nextButton.style.removeProperty('--engagement-progress');
297
+ }
298
+
299
+ // Only show tooltip when blocked with a specific message (provides real value)
300
+ // Skip tooltips for normal states - arrow icon is universally understood
301
+ if (nextBlocked && nextBlockedMessage) {
302
+ nextButton.setAttribute('data-tooltip', nextBlockedMessage);
303
+ nextButton.setAttribute('aria-label', `Next (${nextBlockedMessage})`);
304
+ } else {
305
+ nextButton.removeAttribute('data-tooltip');
306
+ nextButton.setAttribute('aria-label', isLastSlide ? 'Next (No next slide)' : 'Next');
307
+ }
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Triggers the completion animation on the next button.
313
+ * Called by the engagement:complete event handler.
314
+ */
315
+ export function triggerEngagementCompleteAnimation() {
316
+ if (!nextButton) return;
317
+
318
+ nextButton.classList.add('gated', 'engagement-complete');
319
+ nextButton.style.setProperty('--engagement-progress', 100);
320
+
321
+ // Remove classes after animation completes
322
+ setTimeout(() => {
323
+ nextButton.classList.remove('gated', 'engagement-complete');
324
+ nextButton.style.removeProperty('--engagement-progress');
325
+ }, 500);
326
+ }
327
+
328
+ /**
329
+ * Adds a 'visited' class to a menu item.
330
+ * @param {string} slideId - The ID of the slide to mark as visited.
331
+ * @throws {Error} If slideId is invalid
332
+ */
333
+ export function markAsVisited(slideId) {
334
+ if (!slideId || typeof slideId !== 'string') {
335
+ throw new Error(`NavigationUI.markAsVisited: Invalid slideId: ${slideId}`);
336
+ }
337
+ if (!navMenu) return;
338
+ const item = navMenu.querySelector(`.nav-item[data-slide-id="${slideId}"]`);
339
+ if (item) {
340
+ item.classList.add('visited');
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Removes the 'visited' class from a menu item.
346
+ * @param {string} slideId - The ID of the slide to mark as unvisited.
347
+ * @throws {Error} If slideId is invalid
348
+ */
349
+ export function markAsUnvisited(slideId) {
350
+ if (!slideId || typeof slideId !== 'string') {
351
+ throw new Error(`NavigationUI.markAsUnvisited: Invalid slideId: ${slideId}`);
352
+ }
353
+ if (!navMenu) return;
354
+ const item = navMenu.querySelector(`.nav-item[data-slide-id="${slideId}"]`);
355
+ if (item) {
356
+ item.classList.remove('visited');
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Marks a slide as locked (inaccessible) in the navigation menu.
362
+ * @param {string} slideId - The ID of the slide to mark as locked.
363
+ * @throws {Error} If slideId is invalid
364
+ */
365
+ export function markAsLocked(slideId) {
366
+ if (!slideId || typeof slideId !== 'string') {
367
+ throw new Error(`NavigationUI.markAsLocked: Invalid slideId: ${slideId}`);
368
+ }
369
+ if (!navMenu) return;
370
+ const item = navMenu.querySelector(`.nav-item[data-slide-id="${slideId}"]`);
371
+ if (item) {
372
+ item.classList.add('locked');
373
+ const link = item.querySelector('button');
374
+ if (link) {
375
+ link.setAttribute('aria-disabled', 'true');
376
+ link.setAttribute('tabindex', '-1');
377
+
378
+ // Add lock icon if not already present
379
+ if (!link.querySelector('.lock-icon')) {
380
+ const lockIconSpan = document.createElement('span');
381
+ lockIconSpan.className = 'lock-icon';
382
+ lockIconSpan.setAttribute('aria-hidden', 'true');
383
+ lockIconSpan.innerHTML = iconManager.getIcon('lock');
384
+ link.appendChild(lockIconSpan);
385
+ }
386
+ }
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Removes the locked state from a slide in the navigation menu.
392
+ * @param {string} slideId - The ID of the slide to unlock.
393
+ * @throws {Error} If slideId is invalid
394
+ */
395
+ export function markAsUnlocked(slideId) {
396
+ if (!slideId || typeof slideId !== 'string') {
397
+ throw new Error(`NavigationUI.markAsUnlocked: Invalid slideId: ${slideId}`);
398
+ }
399
+ if (!navMenu) return;
400
+ const item = navMenu.querySelector(`.nav-item[data-slide-id="${slideId}"]`);
401
+ if (item) {
402
+ item.classList.remove('locked');
403
+ const link = item.querySelector('button');
404
+ if (link) {
405
+ link.removeAttribute('aria-disabled');
406
+ link.removeAttribute('tabindex');
407
+
408
+ // Remove lock icon if present
409
+ const lockIcon = link.querySelector('.lock-icon');
410
+ if (lockIcon) {
411
+ lockIcon.remove();
412
+ }
413
+ }
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Updates the locked state of all sections based on child slide accessibility.
419
+ * A section gets the 'all-children-locked' class if all its child slides are locked.
420
+ * @param {Map<string, {allowed: boolean, message: string|null}>} accessibilityMap - A map of slide accessibility states.
421
+ */
422
+ export function updateSectionStates(accessibilityMap) {
423
+ if (!navMenu) return;
424
+ if (!(accessibilityMap instanceof Map)) {
425
+ throw new Error('NavigationUI.updateSectionStates: accessibilityMap must be a Map');
426
+ }
427
+
428
+ const sections = navMenu.querySelectorAll('.nav-section');
429
+ sections.forEach(section => {
430
+ // Get all child slides in this section (including nested)
431
+ const childSlides = section.querySelectorAll('.nav-item[data-slide-id]');
432
+
433
+ if (childSlides.length === 0) {
434
+ // No slides in section, remove locked state
435
+ section.classList.remove('all-children-locked');
436
+ return;
437
+ }
438
+
439
+ // Check if ALL child slides are locked
440
+ let allLocked = true;
441
+ childSlides.forEach(slideItem => {
442
+ const slideId = slideItem.dataset.slideId;
443
+ const access = accessibilityMap.get(slideId);
444
+ // If access is missing or allowed is not false, the slide is accessible
445
+ if (!access || access.allowed !== false) {
446
+ allLocked = false;
447
+ }
448
+ });
449
+
450
+ if (allLocked) {
451
+ section.classList.add('all-children-locked');
452
+ } else {
453
+ section.classList.remove('all-children-locked');
454
+ }
455
+ });
456
+ }
457
+
458
+ /**
459
+ * Shows the engagement indicator with current progress.
460
+ * @param {object} progress - Progress object from EngagementManager.getProgress()
461
+ * @param {number} progress.percentage - Completion percentage (0-100)
462
+ * @param {string} progress.tooltip - Pre-built tooltip text
463
+ * @throws {Error} If progress object is invalid
464
+ */
465
+ export function showEngagementIndicator(progress) {
466
+ if (!progress || typeof progress.percentage !== 'number') {
467
+ throw new Error('NavigationUI.showEngagementIndicator: Invalid progress object');
468
+ }
469
+ if (!engagementIndicator) return;
470
+
471
+ // Show the indicator
472
+ engagementIndicator.hidden = false;
473
+
474
+ // Update progress
475
+ updateEngagementProgress(progress.percentage, progress.percentage === 100);
476
+
477
+ // Use tooltip from engagement manager
478
+ engagementIndicator.setAttribute('data-tooltip', progress.tooltip);
479
+
480
+ // Update aria-live announcement for screen readers
481
+ const percentText = `${progress.percentage}% of content requirements completed`;
482
+ engagementIndicator.setAttribute('aria-label', percentText);
483
+ }
484
+
485
+ /**
486
+ * Hides the engagement indicator.
487
+ */
488
+ export function hideEngagementIndicator() {
489
+ if (!engagementIndicator) return;
490
+ engagementIndicator.hidden = true;
491
+ }
492
+
493
+ /**
494
+ * Updates the circular progress indicator.
495
+ * @param {number} percentage - Completion percentage (0-100)
496
+ * @param {boolean} complete - Whether all requirements are met
497
+ */
498
+ export function updateEngagementProgress(percentage, complete) {
499
+ if (!engagementProgress || !engagementCheckmark) return;
500
+
501
+ // Apply threshold: if percentage is below 15, display as 0 for visual fill
502
+ const displayPercentage = percentage < 5 ? 0 : percentage;
503
+
504
+ // Update progress circle (circumference = 2πr, r=14, so ~87.96)
505
+ const circumference = 2 * Math.PI * 14;
506
+ const offset = circumference - (displayPercentage / 100) * circumference;
507
+
508
+ engagementProgress.style.strokeDasharray = `${circumference} ${circumference}`;
509
+ engagementProgress.style.strokeDashoffset = `${offset}`;
510
+
511
+ // Show/hide checkmark
512
+ if (complete) {
513
+ engagementCheckmark.style.opacity = '1';
514
+ engagementProgress.classList.add('complete');
515
+ } else {
516
+ engagementCheckmark.style.opacity = '0';
517
+ engagementProgress.classList.remove('complete');
518
+ }
519
+
520
+ // Update data attribute for CSS targeting
521
+ engagementIndicator?.setAttribute('data-progress', percentage);
522
+ engagementIndicator?.setAttribute('data-complete', complete ? 'true' : 'false');
523
+ }
524
+
525
+ /**
526
+ * Updates the header progress indicator with current slide position.
527
+ * @param {number} currentIndex - Current slide index (0-based)
528
+ * @param {number} totalSlides - Total number of slides in sequence
529
+ * @param {number} [visitedCount] - Optional: number of visited slides for progress bar
530
+ */
531
+ export function updateHeaderProgress(currentIndex, totalSlides, visitedCount = null) {
532
+ if (!headerProgress) return;
533
+
534
+ // Show the progress indicator
535
+ headerProgress.hidden = false;
536
+
537
+ // Update text: "Slide X of Y" (1-based for display)
538
+ if (headerProgressText) {
539
+ headerProgressText.textContent = `Slide ${currentIndex + 1} of ${totalSlides}`;
540
+ }
541
+
542
+ // Update progress bar
543
+ if (headerProgressFill && headerProgressBar) {
544
+ // Use visited count if provided, otherwise use current position
545
+ const progressValue = visitedCount !== null ? visitedCount : currentIndex + 1;
546
+ const percentage = totalSlides > 0 ? (progressValue / totalSlides) * 100 : 0;
547
+ headerProgressFill.style.width = `${percentage}%`;
548
+
549
+ // Update ARIA
550
+ headerProgressBar.setAttribute('aria-valuenow', Math.round(percentage));
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Hides the header progress indicator.
556
+ */
557
+ export function hideHeaderProgress() {
558
+ if (!headerProgress) return;
559
+ headerProgress.hidden = true;
560
+ }
561
+
562
+ /**
563
+ * Provides access to the cached DOM elements.
564
+ * @returns {{navMenu: HTMLElement, prevButton: HTMLElement, nextButton: HTMLElement, engagementIndicator: HTMLElement, headerProgress: HTMLElement}}
565
+ */
566
+ export function getElements() {
567
+ return {
568
+ navMenu,
569
+ prevButton,
570
+ nextButton,
571
+ engagementIndicator,
572
+ headerProgress
573
+ };
574
+ }