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,1088 @@
1
+ # SCORM Framework Development Guide
2
+
3
+ > **Intended Audience: AI Agents** — This document is a machine-readable reference for AI agents developing the framework itself. For human-readable documentation, see `USER_GUIDE.md`.
4
+
5
+ **Related Docs:**
6
+ - `COURSE_AUTHORING_GUIDE.md` - For course authors (not framework devs)
7
+
8
+ - `DATA_MODEL.md` - Complete learner data schemas and storage architecture
9
+
10
+ ---
11
+
12
+ ## Development Commands (Framework Repo)
13
+
14
+ The framework source repo has a different structure than course projects created with `coursecode create`. Use these commands from the repo root:
15
+
16
+ | Command | Purpose | Output |
17
+ |---------|---------|--------|
18
+ | `npm run dev` | Build watch only (no server) | `dist/` |
19
+ | `npm run preview` | **Stub LMS player + build watch + live reload** | `dist/` + server on :4173 |
20
+ | `npm run preview:scorm12` | Preview with SCORM 1.2 format | `dist/` + server on :4173 |
21
+ | `npm run preview:cmi5` | Preview with cmi5 format | `dist/` + server on :4173 |
22
+ | `npm run build` | One-time development build | `dist/` |
23
+ | `npm run build:scorm2004` | Build SCORM 2004 package | `dist/` |
24
+ | `npm run build:scorm12` | Build SCORM 1.2 package | `dist/` |
25
+ | `npm run build:cmi5` | Build cmi5 package | `dist/` |
26
+ | `npm run build:lti` | Build LTI 1.3 package | `dist/` |
27
+
28
+ ### Quick Start for Framework Development
29
+
30
+ ```bash
31
+ # Start the preview server with stub LMS
32
+ npm run preview
33
+
34
+ # Opens http://localhost:4173 with:
35
+ # - Stub SCORM API (localStorage persistence)
36
+ # - Live reload on file changes
37
+ # - Debug panel for API inspection with LMS Compatibility Warnings
38
+ # - Content viewer for course review
39
+ ```
40
+
41
+ ### LMS Compatibility Warnings
42
+ The preview server (stub player) includes an advanced diagnostic system that monitors API usage and data limits to detect potential issues before deployment to a real LMS.
43
+
44
+ It flags issues with color-coded badges in the debug panel:
45
+ - **Red Error Badge**: API misuse (e.g., calling `GetValue` before `Initialize`, `Terminate` errors)
46
+ - **Yellow Warning Badge**: Potential compatibility issues (e.g., suspend data exceeding 4KB/64KB limits)
47
+
48
+ Check the **Errors** tab in the debug panel to see detailed logs and remediation steps.
49
+
50
+ ### Multi-Format Support
51
+
52
+ The framework supports multiple LMS formats:
53
+
54
+ | Format | Description | Limit |
55
+ |--------|-------------|-------|
56
+ | `cmi5` | cmi5/xAPI (default) | Unlimited |
57
+ | `scorm2004` | SCORM 2004 4th Edition | 64KB suspend_data |
58
+ | `scorm1.2` | SCORM 1.2 (legacy LMS) | 4KB suspend_data (Strict Diet Mode found in `scorm-12-driver.js`) |
59
+ | `lti` | LTI 1.3 (remote-hosted, JWT launch) | Unlimited (host-dependent) |
60
+
61
+ > **Note on SCORM 1.2 Strict Diet Mode:** To stay within the 4KB limit, this mode only persists interaction responses for the *current* slide. If a user navigates away and returns, their previous answers on other slides may not be restored visually, although their completion status/score for assessments is always preserved.
62
+
63
+ **Set format in `course-config.js`** (for local CLI builds):
64
+ ```javascript
65
+ export const courseConfig = {
66
+ // format: 'cmi5', // Default is 'cmi5'. Options: 'scorm2004' | 'scorm1.2' | 'lti'
67
+ // ... rest of config
68
+ };
69
+ ```
70
+
71
+ **Or override via CLI:**
72
+ ```bash
73
+ # Environment variable
74
+ LMS_FORMAT=scorm1.2 npm run preview
75
+
76
+ # Build script
77
+ npm run build:scorm12
78
+ npm run build:lti
79
+ ```
80
+
81
+ > **Cloud courses ignore this setting.** When a course is deployed to CourseCode Cloud, the format in `course-config.js` is irrelevant — the cloud uses the universal build to generate ZIPs for any format on demand by re-stamping the meta tag and generating the appropriate manifest. Authors never need to choose a format for cloud-deployed courses.
82
+
83
+ ### Universal Build Architecture
84
+
85
+ The framework produces a **universal build** — a single `dist/` output that contains all LMS drivers as lazy-loaded chunks. The active format is determined at **runtime**, not build time. This enables:
86
+
87
+ - **One build, any format**: `dist/` works with cmi5, SCORM 2004, SCORM 1.2, or LTI without rebuilding
88
+ - **Cloud-ready**: Platforms can serve any format from a single upload by re-stamping a meta tag
89
+ - **No user code execution**: Format-specific ZIPs can be assembled using only framework utilities (no Vite, no course JS)
90
+
91
+ #### How Runtime Format Detection Works
92
+
93
+ `lms-connection.js` resolves the active format via a priority chain:
94
+
95
+ | Priority | Source | Purpose |
96
+ |----------|--------|---------|
97
+ | 1 | `<meta name="lms-format">` tag in HTML | Primary — stamped at build time, re-stampable by cloud/CI |
98
+ | 2 | `import.meta.env.LMS_FORMAT` (Vite define) | Fallback for preview server / dev builds |
99
+ | 3 | `'cmi5'` | Default |
100
+
101
+ The meta tag is injected into `dist/index.html` during the post-build step. Its value comes from `course-config.js` (or the `LMS_FORMAT` env var override). Any platform can re-stamp it with a simple HTML string replacement — no build tools needed.
102
+
103
+ #### Driver Bundling
104
+
105
+ All drivers use `await import()` in `driver-factory.js`. Vite emits each as a separate lazy chunk:
106
+
107
+ ```
108
+ dist/assets/
109
+ main.js ← Framework entry (shared across all formats)
110
+ scorm-2004-driver.js ← Lazy chunk, only fetched if format = 'scorm2004'
111
+ scorm-12-driver.js ← Lazy chunk, only fetched if format = 'scorm1.2'
112
+ cmi5-driver.js ← Lazy chunk, only fetched if format = 'cmi5'
113
+ lti-driver.js ← Lazy chunk, only fetched if format = 'lti'
114
+ proxy-driver.js ← Lazy chunk, only fetched if format = '*-proxy'
115
+ jose.js ← Lazy chunk, only fetched by lti-driver.js
116
+ ```
117
+
118
+ The browser only downloads the one chunk matching the meta tag. Unused driver chunks sit on disk (~20-30 KB each) and are never requested.
119
+
120
+ #### Build Outputs by Scenario
121
+
122
+ | Command | Output | Contains |
123
+ |---------|--------|---------|
124
+ | `coursecode build` | `dist/` | Universal build + format manifest + meta tag stamped |
125
+ | `coursecode build` (with `PACKAGE=true`) | `dist/` + ZIP | Same + format-specific ZIP for LMS upload |
126
+ | `coursecode preview --export` | `course-preview/` | Copy of `dist/` wrapped in stub player (for Netlify/GitHub Pages) |
127
+ | `coursecode deploy` | Uploads `dist/` | Cloud hosts universal build, assembles format ZIPs on demand |
128
+
129
+ The ZIP never includes preview/stub player assets. Preview is a separate concern (see below).
130
+
131
+ #### Re-Stamping for Different Formats
132
+
133
+ `lib/build-packaging.js` exports `stampFormatInHtml(htmlPath, format)` for re-stamping the meta tag:
134
+
135
+ ```javascript
136
+ import { stampFormatInHtml } from 'coursecode/build-packaging';
137
+ import { generateManifest } from 'coursecode/manifest';
138
+
139
+ // Re-stamp dist/index.html for a different format
140
+ stampFormatInHtml('/path/to/dist/index.html', 'scorm2004');
141
+
142
+ // Generate the format-specific manifest
143
+ const { filename, content } = generateManifest('scorm2004', config, files, options);
144
+ fs.writeFileSync(`/path/to/dist/${filename}`, content);
145
+ ```
146
+
147
+ Both are pure Node utilities
148
+
149
+ #### Key Files
150
+
151
+ | File | Purpose |
152
+ |------|---------|
153
+ | `framework/js/state/lms-connection.js` | `getLMSFormat()` — runtime priority chain (meta → env → default) |
154
+ | `framework/js/drivers/driver-factory.js` | Dynamic `import()` switch — loads one driver at runtime |
155
+ | `lib/build-packaging.js` | `stampFormatInHtml()` — meta tag re-stamping utility |
156
+ | `lib/manifest/manifest-factory.js` | `generateManifest()` — generates format-specific manifests |
157
+ | Both `vite.config.js` files | Post-build `closeBundle` hook stamps meta tag into `dist/index.html` |
158
+
159
+ ### cmi5 xAPI Features
160
+
161
+ When using cmi5 format, the framework automatically sends rich xAPI statements:
162
+
163
+ | Event | xAPI Verb | Data |
164
+ |-------|-----------|------|
165
+ | Slide navigation | `experienced` | Slide ID, time spent |
166
+ | Interaction answered | `answered` | Response, result, duration |
167
+ | Objective updated | `completed`/`passed`/`failed` | Objective ID, score |
168
+ | Assessment submitted | `completed` | Score, pass/fail, duration, attempt# |
169
+
170
+ **LMS Launch Data:** cmi5 exposes `masteryScore` and `moveOn` from LMS launch parameters. If the LMS sets `masteryScore`, it overrides the course's configured `passingScore` for assessments.
171
+
172
+ ```javascript
173
+ // Access launch data programmatically (cmi5 only)
174
+ const launchData = stateManager.getLaunchData();
175
+ // Returns: { moveOn, masteryScore, launchMode, activityId, registration }
176
+ ```
177
+
178
+ ### External Hosting (CDN Deployment)
179
+
180
+ For hot-fixable courses, the framework supports **external hosting**: course content lives on a CDN while a minimal proxy package is uploaded to the LMS.
181
+
182
+ | Format | Description | Use Case |
183
+ |--------|-------------|----------|
184
+ | `scorm1.2-proxy` | SCORM 1.2 proxy | Legacy LMS, CDN hosting |
185
+ | `scorm2004-proxy` | SCORM 2004 proxy | Modern SCORM LMS, CDN hosting |
186
+ | `cmi5-remote` | cmi5 with absolute AU URL | Modern LMS/LRS, CDN hosting |
187
+
188
+ **Configuration:**
189
+ ```javascript
190
+ // course-config.js
191
+ export const courseConfig = {
192
+ format: 'scorm1.2-proxy',
193
+ externalUrl: 'https://cdn.example.com/my-course', // Required
194
+ accessControl: { // Required for proxy/remote formats
195
+ clients: {
196
+ 'acme-corp': { token: 'abc123' },
197
+ 'globex': { token: 'def456' }
198
+ }
199
+ }
200
+ };
201
+ ```
202
+
203
+ **Generate tokens:**
204
+ ```bash
205
+ coursecode token # Generate random token
206
+ coursecode token --add acme-corp # Add client to config
207
+ ```
208
+
209
+ **Build outputs:**
210
+ - `dist/` — Deploy to CDN
211
+ - `*_acme-corp_proxy.zip`, `*_globex_proxy.zip` — One package per client
212
+
213
+ **Access Control:**
214
+ - Tokens are injected into URLs: `https://cdn.example.com/my-course?clientId=acme-corp&token=abc123`
215
+ - Runtime validation in `access-control.js` blocks unauthorized access
216
+ - To disable a client: remove from config → redeploy CDN
217
+
218
+ **Architecture:**
219
+ - **Proxy formats**: LMS loads a lightweight proxy (proxy.html + bridge) that iframes the CDN-hosted course. The bridge relays `postMessage` calls to the LMS SCORM API via pipwerks.
220
+ - **cmi5-remote**: The cmi5 manifest's AU URL points directly to the CDN. Course communicates with LRS via xAPI (no iframe/bridge).
221
+
222
+ **Key files:**
223
+ | File | Purpose |
224
+ |------|---------|
225
+ | `framework/js/drivers/proxy-driver.js` | Course-side postMessage LMSDriver |
226
+ | `framework/js/utilities/access-control.js` | Token validation + unauthorized screen |
227
+ | `lib/proxy-templates/proxy.html` | Proxy package entry point |
228
+ | `lib/proxy-templates/scorm-bridge.js` | postMessage ↔ pipwerks bridge |
229
+ | `lib/token.js` | CLI token generator |
230
+
231
+ **Preview mode**: Proxy/remote suffixes are stripped (e.g., `scorm1.2-proxy` → `scorm1.2`) so the stub LMS works normally.
232
+
233
+ ---
234
+
235
+ ## Development Roles & Testing Tools
236
+
237
+ | Role | Testing Tool | Why |
238
+ |------|--------------|-----|
239
+ | **Framework Developers** | `npm run preview` (stub server + example course) | Test framework changes against `template/course/` with full LMS simulation |
240
+ | **Course Authors** | `coursecode preview` (stub server) | Fast iteration on content, CSS, gating, interactions |
241
+
242
+ **Framework developers** modify code in `framework/` that directly interfaces with the LMS via drivers. The preview server with the example course in `template/course/` provides a complete testing environment for validating driver behavior, state management, and component functionality.
243
+
244
+ **Course authors** only edit `course/` files and never touch LMS APIs directly. The framework abstracts all persistence, so the stub server's simple key-value storage is sufficient for testing content, styling, engagement tracking, and assessment logic.
245
+
246
+ ---
247
+
248
+ ## AI Agent Rules
249
+
250
+ **Development & Testing Environment:**
251
+ - Use `npm run preview` for framework development with stub LMS
252
+ - **CRITICAL: Watch Vite Build Warnings!** Errors like "X is not exported by Y" mean code WILL fail at runtime with "(void 0) is not a function". Always check build output for export/import mismatches.
253
+
254
+ ---
255
+
256
+ ## Golden Rules
257
+
258
+ 1. **NEVER call `window.doSetValue/doGetValue` directly** - use `stateManager` methods only
259
+ 2. **ALWAYS use `stateManager`** - single source of truth for persistence
260
+ 3. **TIERED ERROR HANDLING** - Tier 1 (contract violations) always throw; Tier 2 (runtime/init) use `logger.fatal()` → throws in DEV, degrades in PROD
261
+ 4. **NEVER use `console.*` directly** - use `logger.*` instead (enforced by build linter)
262
+ 5. **State-UI-Actions pattern** - for complex components
263
+ 6. **ES Modules** - `import/export`, `const/let` (no `var`)
264
+ 7. **Standardized errors** - ALWAYS emit `{ domain, operation, message, stack, context }` on events ending in `:error`
265
+
266
+ ---
267
+
268
+ ## Directory Structure
269
+
270
+ | Path | Purpose |
271
+ |------|---------|
272
+ | `framework/js/app/` | Global lifecycle, UI (modals, notifications), state |
273
+ | `framework/js/core/` | Core services: `EventBus`, `runtime` |
274
+ | `framework/js/drivers/` | LMS format drivers: SCORM 2004, SCORM 1.2, cmi5, LTI |
275
+ | `framework/js/vendor/` | Third-party libs (`pipwerks.js`) |
276
+ | `framework/js/state/` | State management facade, LMS connection, xAPI service (see `DATA_MODEL.md`) |
277
+ | `framework/js/managers/` | Feature managers: objectives, interactions, engagement, accessibility, etc. |
278
+ | `framework/js/components/` | Reusable UI (tabs, dropdowns) & interactions (MCQ, drag-drop) |
279
+ | `framework/js/navigation/` | Navigation: menu, buttons, document gallery |
280
+ | `framework/js/utilities/` | Helpers (incl. `validation-helpers.js` for SCORM 2004 4E validation) |
281
+ | `framework/js/dev/` | Dev-only code (linter, automation API) - tree-shaken in prod |
282
+
283
+ ---
284
+
285
+ ## Architecture Patterns
286
+
287
+ ### State-UI-Actions Pattern
288
+
289
+ Separates data, presentation, logic (used in `app/`, `navigation/`):
290
+
291
+ - **State (`*-State.js`)**: In-memory data container. No DOM, no `stateManager` access
292
+ - **UI (`*-UI.js`)**: DOM manipulation only. No internal state
293
+ - **Actions (`*-Actions.js`)**: Orchestrates: handles input → updates State → directs UI → calls managers
294
+
295
+ ### ViewManager
296
+
297
+ - **Single ViewManager** in `main.js` controls slide navigation
298
+ - **No caching** - views render fresh each `showView()` to prevent stale data
299
+ - Slide signature: `render(root, context)` where `root` is framework-provided container
300
+ - Components with sub-views (assessments) create own ViewManager
301
+
302
+ ### Event Delegation
303
+
304
+ - Use `data-action="action-name"` attributes
305
+ - Single listener on container delegates based on attribute
306
+ - Works with ViewManager's dynamic rendering
307
+
308
+ ### Course Writer (`lib/course-writer.js`)
309
+
310
+ Unified write operations for course data. All edits go through one endpoint:
311
+
312
+ ```javascript
313
+ POST /__write
314
+ { "target": "config", "id": "navigation.sidebar.enabled", "value": true }
315
+ ```
316
+
317
+ | Target | What it edits | `id` format |
318
+ |--------|---------------|-------------|
319
+ | `config` | `course-config.js` properties | Dot-notation path |
320
+ | `slide` | Slide config in structure array | `slideId` |
321
+ | `objective` | Objective in objectives array | `objectiveId` |
322
+ | `gating` | Gating conditions for slide | `slideId` |
323
+
324
+ **Process**: Import config as object → modify property at path → serialize → write back. No regex.
325
+
326
+ ### PowerPoint Import (`lib/import.js`)
327
+
328
+ Converts `.pptx` to a CourseCode project. Two paths for slide image acquisition:
329
+
330
+ 1. **Auto-export (macOS):** AppleScript drives Microsoft PowerPoint to export each slide as PNG → temp directory → copied to `course/assets/slides/`
331
+ 2. **Manual (`--slides-dir`):** User points at pre-exported images (any platform, no PowerPoint needed)
332
+
333
+ After images are acquired, the module:
334
+ - Extracts text via `node-pptx-parser` → `course/references/converted/`
335
+ - Scaffolds project via `create.js`
336
+ - Removes template slides, generates `slide-XX.html` files (each an `<img class="img-contain">`)
337
+ - Writes `course-config.js` with `layout: 'presentation'`, no engagement tracking
338
+
339
+ ---
340
+
341
+ ## Key Managers (Internals)
342
+
343
+ > For complete domain schemas, storage architecture, and data flow, see [`DATA_MODEL.md`](./DATA_MODEL.md).
344
+
345
+ | Manager | Location | Purpose |
346
+ |---------|----------|--------|
347
+ | `stateManager` | `state/` | **Sole public API** for all LMS and state operations |
348
+ | `assessmentManager` | `managers/` | Graded assessments: question banks, randomization, scoring |
349
+ | `engagementManager` | `managers/` | Tracks content interaction (tabs, accordions, scroll, time) |
350
+ | `objectiveManager` | `managers/` | Learning objectives (CMI-backed domain) |
351
+ | `interactionManager` | `managers/` | LMS interaction reporting (CMI-backed, append-only) |
352
+ | `accessibilityManager` | `managers/` | A11y preferences persisted via `stateManager` |
353
+ | `flagManager` | `managers/` | Arbitrary key-value flags in suspend data |
354
+ | `audioManager` | `managers/` | Audio playback: position persistence, completion tracking |
355
+ | `videoManager` | `managers/` | Video playback: position persistence, completion tracking |
356
+ | `scoreManager` | `managers/` | Course-level scoring (dynamically loaded) |
357
+ | `commentManager` | `managers/` | End-of-course comments/ratings |
358
+ | `breakpointManager` | `app/` | Responsive breakpoints, `.bp-*` classes |
359
+ | `navigationState` | `navigation/` | Current slide, visited slides |
360
+
361
+ ### stateManager: The Sole LMS Gateway
362
+
363
+ **stateManager** (from `state/index.js`) is the single entry point for all LMS communication. It composes:
364
+
365
+ | Internal Module | Responsibility |
366
+ |----------------|----------------|
367
+ | `lms-connection.js` | Driver lifecycle, connection init/terminate, keep-alive |
368
+ | `xapi-statement-service.js` | Bridges events to xAPI statements (cmi5 only) |
369
+ | `state-domains.js` | Domain CRUD with append-only semantics |
370
+ | `state-commits.js` | Auto-batched commit scheduling (500ms debounce) |
371
+ | `state-validation.js` | State hydration, migration, validation |
372
+ | `transaction-log.js` | Ring buffer for debugging |
373
+
374
+ > **Compression:** `cmi.suspend_data` is automatically compressed via `lz-string` (UTF16) in the driver layer. This is transparent to all consumers.
375
+
376
+ #### Domain API
377
+
378
+ All state access uses the domain pattern:
379
+
380
+ ```javascript
381
+ // Read/write domain state — stateManager routes to appropriate storage
382
+ stateManager.getDomainState('objectives'); // → cmi.objectives.* (SCORM) or suspend_data (cmi5/LTI)
383
+ stateManager.getDomainState('navigation'); // → suspend_data
384
+ stateManager.setDomainState('objectives', data);
385
+ stateManager.setDomainState('flags', data);
386
+
387
+ // Semantic LMS methods — no raw CMI access
388
+ stateManager.reportScore({ raw: 85, scaled: 0.85, min: 0, max: 100 });
389
+ stateManager.reportCompletion('completed');
390
+ stateManager.setBookmark('slide-03');
391
+ stateManager.flush(); // Commit now, don't wait for debounce
392
+ ```
393
+
394
+ > **Rule:** Never bypass `stateManager` for LMS access. Never import `lms-connection.js` directly.
395
+
396
+ #### Write Batching
397
+
398
+ LMS writes are auto-batched with a **500ms debounce**. Rapid `setDomainState` calls combine into a single commit. **Critical actions** (exit, terminate) automatically flush pending writes before proceeding.
399
+
400
+ ### flagManager Events
401
+
402
+ ```javascript
403
+ eventBus.on('flag:updated', ({ key, value }) => { /* handle */ });
404
+ eventBus.on('flag:removed', ({ key }) => { /* handle */ });
405
+ ```
406
+
407
+ ### State Validation (Course Updates)
408
+
409
+ Handles LMS data mismatches when course structure changes after learners have started:
410
+
411
+ | Behavior | Dev Mode | Prod Mode |
412
+ |----------|----------|----------|
413
+ | Invalid slide in `cmi.location` | Throws error | Reverts to slide 0 |
414
+ | Missing assessment questions | Throws error | Filters out missing |
415
+ | Orphaned engagement/navigation data | Warns | Silently removes |
416
+ | Schema version newer (downgrade) | Throws error | Resets to fresh state |
417
+ | Schema version older (upgrade) | Runs migrations | Runs migrations |
418
+
419
+ **Setup:** `stateManager.setCourseValidationConfig(config)` called in `main.js` before `initialize()`.
420
+
421
+ **Event:** `state:recovered` emitted when prod mode gracefully recovers.
422
+
423
+ **Schema versioning:** `STATE_SCHEMA_VERSION` constant in `state-validation.js`. Increment when state structure changes incompatibly.
424
+
425
+ ### Schema Migrations
426
+
427
+ When incrementing `STATE_SCHEMA_VERSION`, add a migration function in `STATE_MIGRATIONS`:
428
+
429
+ ```javascript
430
+ const STATE_MIGRATIONS = {
431
+ 2: (state) => {
432
+ // Migrate from v1 → v2: rename 'oldDomain' to 'newDomain'
433
+ if (state.oldDomain) {
434
+ state.newDomain = state.oldDomain;
435
+ delete state.oldDomain;
436
+ }
437
+ return state;
438
+ },
439
+ 3: (state) => {
440
+ // Migrate from v2 → v3: restructure nested data
441
+ // ...
442
+ return state;
443
+ }
444
+ };
445
+ ```
446
+
447
+ **When to add migrations:**
448
+ - Renaming domains (`navigation` → `nav`)
449
+ - Restructuring nested data paths
450
+ - Changing data types (array → object)
451
+ - Adding required fields that code expects
452
+
453
+ **When NOT needed (validation handles):**
454
+ - Adding optional fields or new domains
455
+ - Adding new slide/interaction types
456
+
457
+ Migrations run sequentially (v1→v2→v3), so each only handles one version jump.
458
+
459
+ ---
460
+
461
+ ## Logging & Error Handling
462
+
463
+ **Two complementary systems - use BOTH:**
464
+
465
+ ### Logger (Observability)
466
+
467
+ ```javascript
468
+ logger.debug('Initializing slide', { slideId: 'intro' }); // Dev only
469
+ logger.info('User action recorded'); // Dev only
470
+ logger.warn('Deprecated feature used'); // Dev + Prod
471
+ logger.error('Operation failed', error); // Dev + Prod
472
+ ```
473
+
474
+ Global as `logger` or `window.logger` (no import needed). Auto-filtered by environment.
475
+
476
+ ### Event Bus (Error Communication)
477
+
478
+ **Tiered Error Strategy:**
479
+
480
+ | Tier | When | Behavior | Example |
481
+ |------|------|----------|---------|
482
+ | **Tier 1** | Contract violations, programming errors | Always `throw` | Wrong parameter types, API misuse, double-init |
483
+ | **Tier 2** | Runtime/init failures (missing DOM, bad config) | `logger.fatal()` → throws in DEV, logs+degrades in PROD | Missing container element, invalid data attributes |
484
+ | **Tier 3** | Existing dual-mode functions | Logger + eventBus only | Driver warnings, state persistence failures |
485
+
486
+ **`logger.fatal(message, context)`** — Tier 2 handler in `framework/js/utilities/logger.js`:
487
+
488
+ ```javascript
489
+ if (!container) {
490
+ logger.fatal('initTabs: container not found', { domain: 'ui', operation: 'initTabs' });
491
+ return; // Required — exit the function after calling logger.fatal
492
+ }
493
+ ```
494
+
495
+ - **DEV**: throws `Error` with formatted message for immediate visibility
496
+ - **PROD**: calls `logger.warn()` for graceful degradation
497
+
498
+ All error events follow the standardized shape:
499
+
500
+ ```javascript
501
+ { domain, operation, message, stack?, context? }
502
+ ```
503
+
504
+ **When to catch:** Only to show user notification or cleanup, then MUST re-throw:
505
+
506
+ ```javascript
507
+ try {
508
+ objectiveManager.setCompletionStatus('obj', 'completed');
509
+ } catch (error) {
510
+ showNotification('Failed to save', 'error');
511
+ throw error; // Required - no silent failures
512
+ }
513
+ ```
514
+
515
+ ### External Communications
516
+
517
+ Three optional, config-driven utilities for outbound communication. All in `framework/js/utilities/`, initialized in `main.js`. Each activates when its `endpoint` is configured — otherwise silently skips.
518
+
519
+ | Utility | Config Key | Transport | Events |
520
+ |---------|-----------|-----------|--------|
521
+ | `error-reporter.js` | `environment.errorReporting` | POST per error (60s dedup) | `*:error` (14 event types) |
522
+ | `data-reporter.js` | `environment.dataReporting` | Batched POST + `sendBeacon` on unload | `assessment:submitted`, `objective:updated`, `interaction:recorded` |
523
+ | `course-channel.js` | `environment.channel` | POST to send, SSE to receive | `channel:message`, `channel:connected`, `channel:disconnected` |
524
+
525
+ **Error Reporter** — Subscribes to all `*:error` events, deduplicates by domain+operation+message (60s window), POSTs to endpoint. Optional `enableUserReports: true` adds "Report Issue" to settings menu. `submitUserReport()` for programmatic user reports.
526
+
527
+ **Data Reporter** — Queues assessment/objective/interaction records, flushes on batch size (default 10) or timer (default 30s). `sendBeacon` fallback on page unload.
528
+
529
+ **Course Channel** — Generic pub/sub pipe. `sendChannelMessage(data)` POSTs any JSON to `endpoint/channelId`. SSE listener on same URL bridges incoming messages to EventBus. Exponential backoff reconnect (1s → 30s cap). Content-agnostic — the relay is a dumb fan-out router.
530
+
531
+ ```javascript
532
+ // Config (all optional)
533
+ environment: {
534
+ errorReporting: { endpoint: '...', apiKey: '...', includeContext: true, enableUserReports: true },
535
+ dataReporting: { endpoint: '...', apiKey: '...', batchSize: 10, flushInterval: 30000 },
536
+ channel: { endpoint: '...', apiKey: '...', channelId: 'session-123' }
537
+ }
538
+ ```
539
+
540
+ **Authentication** — All reporters support an optional `apiKey` field. When set, it's sent as `Authorization: Bearer <apiKey>` on `fetch()` calls. For `sendBeacon` (page unload), `fetch()` with `keepalive: true` is used instead since `sendBeacon` doesn't support custom headers. For SSE (`EventSource`), the token is passed as a `?token=` URL parameter.
541
+
542
+ **Example backends:** `framework/docs/examples/cloudflare-{error-worker,data-worker,channel-relay}.js`
543
+
544
+ > **Local dev:** Error and data reporters are automatically disabled during watch builds (`coursecode preview`, `coursecode dev`, `npm run dev`). The CLI sets `VITE_COURSECODE_LOCAL=true` in the Vite build env, which the reporters check at init. Production builds (`coursecode build`) do not set this flag.
545
+
546
+ ## Dual-Context Architecture
547
+
548
+ This codebase serves two roles:
549
+
550
+ 1. **Framework source repo** — where the framework itself is developed (`vite.framework-dev.config.js`, `template/course/` as test content)
551
+ 2. **Course project template** — `template/` is what course authors get when they create a new project (`template/vite.config.js`, their own `course/`)
552
+
553
+ | File | Used by | Purpose |
554
+ |------|---------|---------|
555
+ | `vite.framework-dev.config.js` | Framework developers (this repo) | Builds from `template/course/`, references `lib/` directly |
556
+ | `template/vite.config.js` | Course authors (their project) | Builds from `course/`, imports from `coursecode` package |
557
+ ### Preview Architecture
558
+
559
+ Preview is **not** part of the build output. It is platform infrastructure, served separately from `dist/`:
560
+
561
+ | Scenario | Who provides preview? | Where preview lives |
562
+ |----------|----------------------|--------------------|
563
+ | `coursecode preview` (local) | `preview-server.js` | In-memory, never written to disk |
564
+ | `coursecode preview --export` | `preview-export.js` | Separate `course-preview/` directory (copies `dist/` + wraps in stub player) |
565
+ | CourseCode Cloud | Cloud platform | Cloud hosts its own stub player, wraps any uploaded `dist/` |
566
+ | Self-hosted CDN (proxy) | N/A | User tests locally with `coursecode preview`, CDN serves `dist/` only |
567
+
568
+ **Key principle:** `dist/` never contains preview/stub player assets. The `--export` flag produces a separate `course-preview/` folder for static hosting (Netlify, GitHub Pages). The stub player is generic — it wraps any course's `index.html` in an iframe with fake LMS APIs.
569
+
570
+ ### Cloud Integration Architecture
571
+
572
+ The universal build enables cloud platforms to assemble format-specific outputs **without running any framework build tools or user-uploaded code**:
573
+
574
+ ```
575
+ User: coursecode build → uploads dist/
576
+
577
+ ┌────────────────────┼─────────────────────┐
578
+ ▼ ▼ ▼
579
+ Cloud Preview Cloud ZIP (SCORM 2004) Cloud ZIP (SCORM 1.2)
580
+ ┌────────────┐ ┌──────────────────┐ ┌──────────────────┐
581
+ │ Cloud's own │ │ Copy dist/ │ │ Copy dist/ │
582
+ │ stub player │ │ stampFormatInHtml│ │ stampFormatInHtml│
583
+ │ iframes the │ │ generateManifest │ │ generateManifest │
584
+ │ uploaded │ │ → ZIP │ │ → ZIP │
585
+ │ dist/ │ └──────────────────┘ └──────────────────┘
586
+ └────────────┘
587
+ ```
588
+
589
+ **Cloud dependencies:** The cloud app imports `stampFormatInHtml` and `generateManifest` directly from the `coursecode` npm package. These are pure Node utilities — no Vite, no dynamic imports of user code, no `eval`. All inputs (title, version, file list) come from scanning the uploaded `dist/` or the cloud's own database.
590
+
591
+ **Security boundary:** The cloud never executes `course-config.js` or any user-authored JavaScript. The meta tag and manifest are the only format-specific artifacts, and both are generated from trusted framework source code.
592
+ ---
593
+
594
+ ## Course Validation
595
+
596
+ Two linters validate courses at different stages, each in a different environment:
597
+
598
+ ### Runtime Linter (`framework/js/dev/runtime-linter.js`)
599
+
600
+ Runs **in the browser** during preview when `import.meta.env.DEV === true`. Called from `main.js` before course initialization.
601
+
602
+ **What it checks (needs a real DOM):**
603
+ - Renders each slide to detached DOM, validates engagement requirements match content
604
+ - Visual layout: contrast ratios, touch target sizes, nested cards, heading hierarchy
605
+ - Component structure validation against schemas
606
+ - Assessment config validation
607
+ - Audio configuration conflicts (slide audio vs modal audio)
608
+
609
+ **On failure:** Halts initialization with formatted error showing slide ID and fix needed.
610
+
611
+ **Production:** Tree-shaken out — never included in production builds.
612
+
613
+ ### Build Linter (`lib/build-linter.js`)
614
+
615
+ Runs **in Node.js** during build (via `vite.framework-dev.config.js` `closeBundle` hook) and via MCP/CLI.
616
+
617
+ **What it checks (no DOM needed):**
618
+ - Course config validation (structure, objectives, gating conditions)
619
+ - CSS class validation via PostCSS — flags hallucinated class names
620
+ - Schema-driven requirement validation from source templates
621
+ - Duplicate interaction ID detection
622
+
623
+ **Invoked by:**
624
+ - `npm run build` — automatically in `closeBundle` hook
625
+ - `coursecode lint` CLI command
626
+ - MCP `coursecode_lint` tool
627
+
628
+ **Errors fail the build; warnings print but don't block.**
629
+
630
+ ### Shared Rules (`lib/validation-rules.js`)
631
+
632
+ Pure validation functions used by **both** linters. No environment-specific code (no DOM, no `fs`). Includes assessment validation, engagement validation, and result formatting.
633
+
634
+ ## Engagement Tracking (Implementation)
635
+
636
+ All slides MUST have `engagement` config in `course-config.js`.
637
+
638
+ ### Requirement Types
639
+
640
+ | Type | Required Props | Validates Against |
641
+ |------|----------------|-------------------|
642
+ | `viewAllTabs` | - | `[data-component="tabs"]` |
643
+ | `viewAllPanels` | - | `[data-component="accordion"]` |
644
+ | `viewAllFlipCards` | - | `[data-flip-card-id]` |
645
+ | `viewAllTimelineEvents` | - | `[data-component="interactive-timeline"]` |
646
+ | `viewAllHotspots` | - | `[data-component="interactive-image"]` |
647
+ | `viewAllModals` | - | `[data-component="modal-trigger"]` with `data-modal-id` |
648
+ | `interactionComplete` | `interactionId`, optional `label` | `[data-interaction-id]` |
649
+ | `allInteractionsComplete` | - | Any `[data-interaction-id]` |
650
+ | `scrollDepth` | `percentage` | Scroll event tracking |
651
+ | `timeOnSlide` | `minSeconds` | Session timer |
652
+ | `slideAudioComplete` | - | Slide audio config |
653
+ | `audioComplete` | `audioId` | `[data-audio-id]` |
654
+ | `modalAudioComplete` | `modalId` | Modal audio config |
655
+ | `flag` | `key`, optional `equals` | flagManager |
656
+ | `allFlags` | `flags` array | flagManager |
657
+
658
+ ### Tooltip Labels
659
+
660
+ - **`message`** (engagement-level): Full override of tooltip text
661
+ - **`label`** (requirement-level): Custom name in "Complete: X" format (auto-formats from ID if omitted)
662
+
663
+ ### Progress Indicator
664
+
665
+ When `engagement.required` is `true`, the circular progress indicator displays in the nav footer by default. Set `showIndicator: false` to hide it.
666
+
667
+ Custom indicators use `EngagementManager` API:
668
+ ```javascript
669
+ const progress = engagementManager.getProgress(slideId);
670
+ // Returns: { percentage: 75, items: [{type, label, complete}, ...] }
671
+ ```
672
+
673
+ ---
674
+
675
+ ## Declarative UI Components
676
+
677
+ Components auto-initialize via `data-component` attributes. Registration in `framework/js/components/`.
678
+
679
+ | Component | Attribute | Events Emitted |
680
+ |-----------|-----------|----------------|
681
+ | Tabs | `data-component="tabs"` | `tab:selected` |
682
+ | Accordion | `data-component="accordion"` | `accordion:panel-opened` |
683
+ | Carousel | `data-component="carousel"` | `carousel:slide-changed` |
684
+ | Collapse | `data-component="collapse"` | `collapse:toggle` |
685
+ | Dropdown | `data-component="dropdown"` | `dropdown:change` |
686
+ | Modal | `data-component="modal-trigger"` | `modal:opened`, `modal:closed` |
687
+ | Flip Card | `data-component="flip-card"` | `flipcard:flipped` |
688
+ | Interactive Timeline | `data-component="interactive-timeline"` | `timeline:event-viewed` |
689
+ | Toggle Group | `data-component="toggle-group"` | `toggle:change` |
690
+ | Checkbox Group | `data-component="checkbox-group"` | `checkbox-group:change` |
691
+ | Audio Player | `data-component="audio-player"` | `audio:complete` |
692
+ | Video Player | `data-component="video-player"` | `video:complete` |
693
+
694
+ ### Component Catalog
695
+
696
+ Components and layout patterns are managed via `component-catalog.js`, which mirrors the interaction catalog pattern:
697
+
698
+ ```javascript
699
+ // Auto-discovery via import.meta.glob
700
+ // framework/js/components/ui-components/*.js → All built-in components (tabs, accordion, carousel, modal, hero, steps, timeline, etc.)
701
+ // course/components/*.js → Custom course components
702
+ ```
703
+
704
+ **Schema Pattern** - Every component exports a schema for validation:
705
+
706
+ ```javascript
707
+ // Example: framework/js/components/ui-components/tabs.js
708
+ export const schema = {
709
+ type: 'tabs',
710
+ description: 'Accessible tab interface with keyboard navigation',
711
+ properties: {
712
+ activeClass: { type: 'string', default: 'active' }
713
+ },
714
+ structure: {
715
+ container: '[data-component="tabs"]',
716
+ children: {
717
+ button: { selector: '[data-action="select-tab"]', required: true, minItems: 1 },
718
+ panel: { selector: '.tab-content', required: true }
719
+ }
720
+ }
721
+ };
722
+
723
+ export const metadata = {
724
+ category: 'ui-component',
725
+ engagementTracking: 'viewAllTabs',
726
+ emitsEvents: ['tab:selected']
727
+ };
728
+
729
+ export function initTabs(container) { /* ... */ }
730
+ ```
731
+
732
+ **Linter Validation** - The runtime linter validates component structure against schemas during preview.
733
+
734
+ ---
735
+
736
+ ## Automation API
737
+
738
+ **Access**: `window.CourseCodeAutomation` (dev mode only, tree-shaken in prod)
739
+
740
+ **Enable** in `course/course-config.js`:
741
+ ```javascript
742
+ environment: {
743
+ automation: { enabled: true, disableBeforeUnloadGuard: true, exposeCorrectAnswers: true }
744
+ }
745
+ ```
746
+
747
+ **Requirements**: `import.meta.env.MODE !== 'production'` AND `automation.enabled === true`
748
+
749
+ ### API Methods
750
+
751
+ | Category | Method | Returns/Purpose |
752
+ |----------|--------|-----------------|
753
+ | **Discovery** | `listInteractions()` | `[{id, type, registeredAt}, ...]` |
754
+ | | `getInteractionMetadata(id)` | `{id, type, registeredAt}` |
755
+ | **State Access** | `getResponse(id)` | Current response (format varies by type) |
756
+ | | `getCorrectResponse(id)` | Correct answer (needs `exposeCorrectAnswers`) |
757
+ | **State Mutation** | `setResponse(id, response)` | Sets response |
758
+ | **Evaluation** | `checkAnswer(id)` | `{correct, score, feedback, ...}` |
759
+ | | `checkSlideAnswers(slideId?)` | `[{interactionId, type, evaluation}, ...]` |
760
+ | **Navigation** | `getCourseStructure()` | Structure array from config |
761
+ | | `getCurrentSlide()` | Current slide ID or null |
762
+ | | `goToSlide(slideId, context?)` | Navigate with optional context |
763
+ | **Engagement** | `getEngagementState()` | `{complete, tracked, requirements}` |
764
+ | | `getEngagementProgress()` | `{percentage, items: [{label, complete, type}]}` |
765
+ | | `markTabViewed(tabId)` | Manually track tab (testing) |
766
+ | | `setScrollDepth(percentage)` | Simulate scroll (testing) |
767
+ | | `resetEngagement()` | Reset tracking for current slide |
768
+ | **Flags** | `getFlag(key)` | Get flag value |
769
+ | | `setFlag(key, value)` | Set flag (triggers engagement re-eval) |
770
+ | | `getAllFlags()` | All flags as object |
771
+ | | `removeFlag(key)` | Remove flag |
772
+ | **Audio** | `getAudioState()` | `{currentSrc, position, isPlaying, isMuted, duration}` |
773
+ | | `hasAudio()` | Check if audio loaded |
774
+ | | `playAudio()` / `pauseAudio()` / `toggleAudio()` | Playback control |
775
+ | | `restartAudio()` | Restart from beginning |
776
+ | | `seekAudio(seconds)` / `seekAudioToPercentage(pct)` | Seek |
777
+ | | `toggleAudioMute()` / `setAudioMuted(bool)` | Mute control |
778
+ | | `getAudioProgress()` | Playback percentage |
779
+ | **Observability** | `getAutomationTrace()` | `[{timestamp, action, ...}, ...]` |
780
+ | | `clearAutomationTrace()` | Clears trace log |
781
+ | | `getVersion()` | `{api, phase, features}` |
782
+
783
+ ### Response Formats by Interaction Type
784
+
785
+ | Type | Format |
786
+ |------|--------|
787
+ | Multiple Choice | `'a'`, `'b'`, `'c'` |
788
+ | True/False | `true` / `false` |
789
+ | Fill-in-Blank | `{blankId: 'answer'}` |
790
+ | Drag-Drop | `{itemId: zoneId}` |
791
+ | Numeric | `1.5` |
792
+ | Sequencing | `['id1', 'id2', 'id3']` |
793
+ | Likert | `{questionId: 'value'}` |
794
+
795
+ ### data-testid Attributes
796
+
797
+ **Nav**: `nav-prev`, `nav-next`, `nav-exit`, `nav-menu-toggle`, `nav-menu-item-{slideId}`, `nav-section-{sectionId}`
798
+
799
+ **Interactions**: `{id}-check-answer`, `{id}-reset`, `{id}-controls`, `{id}-feedback`, `{id}-choice-{index}`, `{id}-blank-{index}`, `{id}-input`, `{id}-drag-item-{itemId}`, `{id}-drop-zone-{zoneId}`
800
+
801
+ **Assessments**: `assessment-start`, `assessment-nav-{prev|next}`, `assessment-submit`, `assessment-retake`, `assessment-review-question-{index}`
802
+
803
+
804
+
805
+ ---
806
+
807
+ ## MCP Integration (AI Agent Control)
808
+
809
+ The MCP server runs a **persistent headless Chrome** internally via `puppeteer-core`. All runtime tools execute directly in this headless browser — agents never need to open a browser.
810
+
811
+ ### Preview Server Ownership
812
+
813
+ The MCP does **not** start or manage the preview server. The preview must be running before using runtime tools:
814
+
815
+ - **Human**: run `coursecode preview` (or `npm run preview`) in a terminal
816
+ - **AI agent**: use `run_command` to execute `npm run preview`
817
+
818
+ If the preview is not running, runtime tools fail fast with a clear error message.
819
+
820
+ ### Setup
821
+
822
+ 1. Start the preview server externally (see above)
823
+ 2. Add to IDE MCP config:
824
+
825
+ ```json
826
+ { "mcpServers": { "coursecode": { "command": "coursecode", "args": ["mcp"] } } }
827
+ ```
828
+
829
+ ### How the Headless Browser Works
830
+
831
+ - On the **first runtime tool call**, the MCP launches headless Chrome and connects to the already-running preview server
832
+ - The browser auto-reconnects when Vite rebuilds (file changes trigger SSE reload)
833
+ - All tool calls execute instantly via `page.evaluate()` — no manual waits needed
834
+
835
+ ### Runtime Tools (require preview server)
836
+
837
+ | Tool | Purpose | Returns |
838
+ |------|---------|--------|
839
+ | `coursecode_state` | Full course snapshot | `{slide, structure, interactions, engagement, lmsState, errors}` |
840
+ | `coursecode_navigate` | Go to slide by ID | `{slideId}` → new state |
841
+ | `coursecode_interact` | Set response + evaluate | `{interactionId, response}` → `{correct, score, feedback}` |
842
+ | `coursecode_screenshot` | Visual capture (PNG) | Optional `slideId` to navigate first, `fullPage` for scroll capture |
843
+ | `coursecode_reset` | Clear learner state | `{hard?: boolean}` → reload |
844
+
845
+ ### Navigation API
846
+
847
+ Use MCP tools for all course interaction — never use external browser tools:
848
+
849
+ - `coursecode_state` → get all slide IDs, current position, interactions
850
+ - `coursecode_navigate(slideId)` → instant slide navigation
851
+ - `coursecode_screenshot(slideId)` → navigate + capture in one call
852
+ - `coursecode_interact(id, response)` → answer + evaluate in one call
853
+
854
+ ### Architecture
855
+
856
+ ```
857
+ MCP Server (IDE) ──puppeteer──▶ Headless Chrome ──HTTP──▶ Preview Server
858
+
859
+ └── Course iframe (CourseCodeAutomation API)
860
+ ```
861
+
862
+ - **Preview not running?** → Tools return clear error: "Start preview server first"
863
+ - **Chrome not found?** → Install Google Chrome or set `CHROME_PATH` env var
864
+
865
+ ---
866
+
867
+ ## Audio Manager (Internals)
868
+
869
+ Singleton at `framework/js/managers/audio-manager.js`. Manages single audio element.
870
+
871
+ ### Key Behaviors
872
+
873
+ - **Position Persistence**: Saves position when leaving slide, restores on return
874
+ - **Completion Tracking**: Tracks max position reached (handles seeks/replays)
875
+ - **Single Instance**: Only one audio plays at a time (slide vs modal vs standalone)
876
+ - **Mute Preference**: Mute state persists across session
877
+ - **Auto-Pause**: Audio pauses on navigation away or modal close
878
+
879
+ ### API
880
+
881
+ ```javascript
882
+ const { audioManager } = CourseCode;
883
+
884
+ audioManager.play();
885
+ audioManager.pause();
886
+ audioManager.togglePlayPause();
887
+ audioManager.restart();
888
+ audioManager.seek(30); // seconds
889
+ audioManager.seekToPercentage(50);
890
+ audioManager.toggleMute();
891
+ audioManager.getState(); // Full state object
892
+ audioManager.hasAudio();
893
+ ```
894
+
895
+ ### Footer Controls
896
+
897
+ - **Slide audio**: Full controls (play/pause, restart, progress bar, mute, time)
898
+ - **Modal audio**: Compact controls (play/pause, restart, mute only)
899
+ - **Standalone**: Full controls inline
900
+
901
+ ---
902
+
903
+ ## Assessment Manager (Internals)
904
+
905
+ `framework/js/managers/assessment-manager.js`
906
+
907
+ ### Features
908
+
909
+ - **Question Banks**: Select N questions from categorized banks
910
+ - **Randomization**: Shuffle questions and/or re-randomize on retake
911
+ - **Progressive Intervention**: Show remedial content after N failures, restart course after M
912
+ - **Auto-Linked Objectives**: Updates objective on submission based on score
913
+ - **Unanswered Handling**: Confirmation modal or immediate submit (configurable)
914
+
915
+ ### Creation
916
+
917
+ ```javascript
918
+ const { AssessmentManager } = CourseCode;
919
+
920
+ const assessment = AssessmentManager.createAssessment(
921
+ { ...config, questions }, // OR questionBanks for random selection
922
+ overrides
923
+ );
924
+ assessment.render(container);
925
+ ```
926
+
927
+ ### Events
928
+
929
+ ```javascript
930
+ eventBus.on('assessment:submitted', ({ id, score, passed }) => { });
931
+ eventBus.on('assessment:retake', ({ id, attemptNumber }) => { });
932
+ ```
933
+
934
+ ---
935
+
936
+ ## CSS Architecture
937
+
938
+ | File | Purpose |
939
+ |------|---------|
940
+ | `design-tokens.css` | CSS variables (colors, spacing, typography) |
941
+ | `01-base.css` | HTML resets, base typography |
942
+ | `02-layout.css` | Content width, stacks, columns, splits |
943
+ | `components/*.css` | Individual UI component styles (cards, hero, tabs, steps, timeline, etc.) |
944
+ | `interactions/*.css` | Interaction-specific styles |
945
+ | `utilities/*.css` | Utility classes (spacing, display, flex) |
946
+ | `framework.css` | Main import file, orchestrates all modules |
947
+
948
+ ### Layout System
949
+
950
+ CSS-only layouts controlled via `data-layout` attribute on `<html>`. Files in `framework/css/layouts/`:
951
+
952
+ | File | Purpose |
953
+ |------|---------|
954
+ | `base.css` | Layout tokens, default structure |
955
+ | `traditional.css` | Full header, sidebar toggle |
956
+ | `article.css` | **Default**: Minimal header, centered content, floating pill nav |
957
+ | `focused.css` | Hidden header, centered content, floating pill nav |
958
+ | `presentation.css` | Full viewport, edge arrow navigation |
959
+ | `canvas.css` | Zero framework CSS — all styles reverted to browser defaults, author BYOs via `theme.css` |
960
+
961
+ > **Note:** Canvas only strips CSS (`all: revert` on `#slide-container`). All JS infrastructure — navigation, gating, interactions, engagement tracking, and LMS drivers — remains fully functional.
962
+
963
+ Set via `layout` in `course-config.js`. The `main.js` automatically applies the attribute from config.
964
+
965
+ ### Auto-Wrapping
966
+
967
+ Slides are automatically wrapped with content width class (default: `.content-medium`).
968
+
969
+ Override per-slide with `data-content-width` attribute or globally via `slideDefaults.contentWidth` in `course-config.js`.
970
+ ### Document Gallery
971
+
972
+ `framework/js/navigation/document-gallery.js` — Collapsible sidebar gallery for reference documents.
973
+
974
+ **Build pipeline:** `vite-plugin-content-discovery.js` scans `course/assets/docs/` and generates `_gallery-manifest.json` at build time. In dev mode, `preview-server.js` generates the manifest dynamically.
975
+
976
+ **Key files:**
977
+
978
+ | File | Purpose |
979
+ |------|---------|
980
+ | `framework/js/navigation/document-gallery.js` | Fetches manifest, renders thumbnails, expand/collapse logic |
981
+ | `framework/css/components/document-gallery.css` | 2-column grid, thumbnail variants |
982
+ | `lib/vite-plugin-content-discovery.js` | `generateGalleryManifest()` — build-time discovery |
983
+
984
+ **Behavior:** Expanding gallery collapses nav menu (inverse toggle). Gallery resets to collapsed on sidebar `transitionend` close. Configured via `navigation.documentGallery` in `course-config.js`.
985
+
986
+ ---
987
+
988
+ ## Icons
989
+
990
+ `framework/js/utilities/icons.js` provides a centralized icon registry. Icons are rendered as SVG strings with no inline width/height - sizing is controlled via CSS classes.
991
+
992
+ ### IconManager API
993
+
994
+ ```javascript
995
+ const { iconManager } = CourseCode;
996
+
997
+ // Basic usage
998
+ iconManager.getIcon('menu'); // Returns SVG string
999
+
1000
+ // With options
1001
+ iconManager.getIcon('check', {
1002
+ size: 'lg', // 'xs'|'sm'|'md'|'lg'|'xl'|'2xl'|'3xl' or px: 12|16|20|24|32|48|64
1003
+ class: 'icon-success', // Additional CSS classes
1004
+ strokeWidth: 2, // SVG stroke width (default: 2)
1005
+ color: 'currentColor' // Stroke color (default: currentColor)
1006
+ });
1007
+
1008
+ // Register custom icons
1009
+ iconManager.register('custom-icon', '<path d="..." />');
1010
+ iconManager.registerAll({ icon1: '...', icon2: '...' });
1011
+ ```
1012
+
1013
+ ### Extending via Course
1014
+
1015
+ Course authors add custom icons in `course/icons.js`:
1016
+
1017
+ ```javascript
1018
+ export const customIcons = {
1019
+ 'rocket': '<path d="..." />'
1020
+ };
1021
+ ```
1022
+
1023
+ These are automatically registered and available via `iconManager.getIcon('rocket')`.
1024
+
1025
+ ---
1026
+
1027
+ ## Breakpoint Manager
1028
+
1029
+ `framework/js/utilities/breakpoint-manager.js` - Dynamically applies responsive CSS classes to `<html>`.
1030
+
1031
+ ### Breakpoints
1032
+
1033
+ | Class | Condition | Width |
1034
+ |-------|-----------|-------|
1035
+ | `.bp-min-large-desktop` | ≥1440px | Large screens |
1036
+ | `.bp-max-desktop` | ≤1439px | Desktop and below |
1037
+ | `.bp-max-tablet-landscape` | ≤1199px | Tablet landscape |
1038
+ | `.bp-max-tablet-portrait` | ≤1023px | Tablet portrait |
1039
+ | `.bp-max-mobile-landscape` | ≤767px | Mobile landscape |
1040
+ | `.bp-max-mobile-portrait` | ≤479px | Mobile portrait |
1041
+
1042
+ Multiple classes cascade (e.g., at 600px: `.bp-max-desktop`, `.bp-max-tablet-landscape`, `.bp-max-tablet-portrait`, `.bp-max-mobile-landscape` all applied).
1043
+
1044
+ ### API
1045
+
1046
+ ```javascript
1047
+ import { breakpointManager } from './utilities/breakpoint-manager.js';
1048
+
1049
+ breakpointManager.getCurrentBreakpoint(); // 'tablet-portrait'
1050
+ breakpointManager.isMobile(); // true if ≤767px
1051
+ breakpointManager.isTablet(); // true if 768-1199px
1052
+ breakpointManager.isDesktop(); // true if ≥1200px
1053
+ breakpointManager.isAtMost('tablet-portrait'); // true if ≤1023px
1054
+ breakpointManager.onChange((newBp, oldBp) => { /* handle */ });
1055
+ breakpointManager.refresh(); // Force re-evaluation
1056
+ ```
1057
+
1058
+ Exposed globally via `CourseCode.breakpointManager`.
1059
+
1060
+ ---
1061
+
1062
+ ## Adding New Interaction Types
1063
+
1064
+ > **Course Authors:** See `course/interactions/PLUGIN_GUIDE.md`. Steps below are for framework developers.
1065
+
1066
+ 1. Create file in `framework/js/components/interactions/`
1067
+ 2. Export `create(container, config)`, `metadata`, and `schema`
1068
+ 3. Register with `catalogInteraction()` in `core/interaction-catalog.js` (auto-discovered via `import.meta.glob`)
1069
+ 4. Add CSS to `framework/css/interactions/`
1070
+ 5. Update automation API if needed for testing support
1071
+ 6. Add to `COURSE_AUTHORING_GUIDE.md` if has author-facing classes
1072
+
1073
+ ### Required Contract
1074
+
1075
+ ```javascript
1076
+ export function create(container, config) {
1077
+ // config must have: id, (type-specific props)
1078
+ // Must set data-interaction-id on root element
1079
+ // Must emit events for state changes
1080
+ // Must support getResponse() / setResponse() for automation
1081
+ return {
1082
+ getResponse: () => currentResponse,
1083
+ setResponse: (val) => { /* update UI and state */ },
1084
+ checkAnswer: () => ({ correct, score, feedback }),
1085
+ reset: () => { /* clear to initial state */ }
1086
+ };
1087
+ }
1088
+ ```