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,215 @@
1
+ /**
2
+ * @file engagement-trackers.js
3
+ * @description Component registration and engagement tracking methods.
4
+ * These are mixed into EngagementManager's prototype to keep the manager slim.
5
+ *
6
+ * Uses factory functions to eliminate boilerplate — all generated methods
7
+ * follow the same validate → getState → update → save → emit pattern.
8
+ */
9
+
10
+ import { logger } from '../utilities/logger.js';
11
+
12
+ // =========================================================================
13
+ // Factory Functions
14
+ // =========================================================================
15
+
16
+ /**
17
+ * Creates a batch registration function that sets tracked[totalField] = ids.length.
18
+ */
19
+ function makeRegister(totalField, label) {
20
+ // Derive the viewed field name from the total field (e.g., 'tabsTotal' → 'tabsViewed')
21
+ const viewedField = totalField.replace('Total', 'Viewed');
22
+ return function (slideId, ids) {
23
+ if (!slideId || !Array.isArray(ids)) return;
24
+ const state = this._getState();
25
+ if (!state[slideId]) return;
26
+ state[slideId].tracked[totalField] = ids.length;
27
+ // Reset viewed array on re-registration to prevent viewed > total inconsistency
28
+ state[slideId].tracked[viewedField] = [];
29
+ this._setState(state);
30
+ logger.debug(`[EngagementManager] Registered ${ids.length} ${label}: ${slideId}`);
31
+ this._checkAndEmitProgress(slideId);
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Creates an incremental registration function that adds to total and inits viewed array.
37
+ */
38
+ function makeRegisterIncremental(totalField, viewedField, label) {
39
+ return function (slideId, ids) {
40
+ if (!slideId || !Array.isArray(ids)) return;
41
+ const state = this._getState();
42
+ if (!state[slideId]) return;
43
+ state[slideId].tracked[totalField] = (state[slideId].tracked[totalField] || 0) + ids.length;
44
+ if (!state[slideId].tracked[viewedField]) {
45
+ state[slideId].tracked[viewedField] = [];
46
+ }
47
+ this._setState(state);
48
+ logger.debug(`[EngagementManager] Registered ${ids.length} ${label}: ${slideId}`);
49
+ this._checkAndEmitProgress(slideId);
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Creates an array-push tracker that deduplicates and logs progress.
55
+ * @param {string} viewedField - tracked[viewedField] array to push into
56
+ * @param {string} [totalField] - tracked[totalField] for progress logging (optional)
57
+ * @param {string} label - human-readable label for debug logs
58
+ */
59
+ function makeArrayTracker(viewedField, totalField, label) {
60
+ return function (slideId, itemId) {
61
+ if (!slideId || !itemId) return;
62
+ const state = this._getState();
63
+ if (!state[slideId]) return;
64
+ const tracked = state[slideId].tracked;
65
+ if (!tracked[viewedField]) tracked[viewedField] = [];
66
+ if (!tracked[viewedField].includes(itemId)) {
67
+ tracked[viewedField].push(itemId);
68
+ this._setState(state);
69
+ const progress = totalField ? ` (${tracked[viewedField].length}/${tracked[totalField]})` : '';
70
+ logger.debug(`[EngagementManager] ${label}: ${itemId}${progress}`);
71
+ this._checkAndEmitProgress(slideId);
72
+ }
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Creates a boolean-flag tracker that sets tracked[field] = true once.
78
+ */
79
+ function makeBoolTracker(field, label) {
80
+ return function (slideId) {
81
+ if (!slideId) return;
82
+ const state = this._getState();
83
+ if (!state[slideId]) return;
84
+ if (!state[slideId].tracked[field]) {
85
+ state[slideId].tracked[field] = true;
86
+ this._setState(state);
87
+ logger.debug(`[EngagementManager] ${label}: ${slideId}`);
88
+ this._checkAndEmitProgress(slideId);
89
+ }
90
+ };
91
+ }
92
+
93
+ // =========================================================================
94
+ // Generated Registration Methods
95
+ // =========================================================================
96
+
97
+ export const registerTabs = makeRegister('tabsTotal', 'tabs');
98
+ export const registerAccordion = makeRegister('accordionPanelsTotal', 'accordion panels');
99
+ export const registerFlipCards = makeRegister('flipCardsTotal', 'flip cards');
100
+ export const registerTimeline = makeRegister('timelineEventsTotal', 'timeline events');
101
+ export const registerModals = makeRegister('modalsTotal', 'modals');
102
+
103
+ export const registerInteractiveImage = makeRegisterIncremental(
104
+ 'interactiveImageHotspotsTotal', 'interactiveImageHotspotsViewed', 'hotspots'
105
+ );
106
+ export const registerLightbox = makeRegisterIncremental(
107
+ 'lightboxesTotal', 'lightboxesViewed', 'lightboxes'
108
+ );
109
+
110
+ // =========================================================================
111
+ // Special-Case Registration
112
+ // =========================================================================
113
+
114
+ /** Single flip-card registration (incremental, updates total from registered list). */
115
+ export function registerFlipCard(slideId, cardId) {
116
+ if (!slideId || !cardId) return;
117
+ const state = this._getState();
118
+ if (!state[slideId]) return;
119
+ const tracked = state[slideId].tracked;
120
+ if (!tracked.flipCardsRegistered) tracked.flipCardsRegistered = [];
121
+ if (!tracked.flipCardsRegistered.includes(cardId)) {
122
+ tracked.flipCardsRegistered.push(cardId);
123
+ // Use whichever is larger: batch-registered total or incremental count
124
+ tracked.flipCardsTotal = Math.max(tracked.flipCardsTotal || 0, tracked.flipCardsRegistered.length);
125
+ this._setState(state);
126
+ logger.debug(`[EngagementManager] Registered flip card: ${cardId} (total: ${tracked.flipCardsTotal})`);
127
+ this._checkAndEmitProgress(slideId);
128
+ }
129
+ }
130
+
131
+ // =========================================================================
132
+ // Generated Tracking Methods
133
+ // =========================================================================
134
+
135
+ export const trackTabView = makeArrayTracker('tabsViewed', 'tabsTotal', 'Tab viewed');
136
+ export const trackAccordionPanel = makeArrayTracker('accordionPanelsViewed', 'accordionPanelsTotal', 'Panel viewed');
137
+ export const trackFlipCardView = makeArrayTracker('flipCardsViewed', 'flipCardsTotal', 'Flip card viewed');
138
+ export const trackTimelineView = makeArrayTracker('timelineEventsViewed', 'timelineEventsTotal', 'Timeline event viewed');
139
+ export const trackInteractiveImageView = makeArrayTracker('interactiveImageHotspotsViewed', 'interactiveImageHotspotsTotal', 'Hotspot viewed');
140
+ export const trackLightboxView = makeArrayTracker('lightboxesViewed', 'lightboxesTotal', 'Lightbox viewed');
141
+ export const trackModalView = makeArrayTracker('modalsViewed', 'modalsTotal', 'Modal viewed');
142
+ export const trackStandaloneAudioComplete = makeArrayTracker('standaloneAudioComplete', null, 'Standalone audio completed');
143
+ export const trackStandaloneVideoComplete = makeArrayTracker('standaloneVideoComplete', null, 'Standalone video completed');
144
+ export const trackModalAudioComplete = makeArrayTracker('modalsAudioComplete', null, 'Modal audio completed');
145
+
146
+ export const trackSlideAudioComplete = makeBoolTracker('audioComplete', 'Slide audio completed');
147
+ export const trackSlideVideoComplete = makeBoolTracker('videoComplete', 'Slide video completed');
148
+
149
+ // =========================================================================
150
+ // Special-Case Tracking
151
+ // =========================================================================
152
+
153
+ /** Interaction tracking — uses object map keyed by interactionId, not array. */
154
+ export function trackInteraction(slideId, interactionId, completed, correct) {
155
+ if (!slideId || !interactionId) return;
156
+ const state = this._getState();
157
+ if (!state[slideId]) return;
158
+ state[slideId].tracked.interactionsCompleted[interactionId] = { completed, correct };
159
+ this._setState(state);
160
+ logger.debug(`[EngagementManager] Interaction: ${interactionId} (completed: ${completed}, correct: ${correct})`);
161
+ this._checkAndEmitProgress(slideId);
162
+ }
163
+
164
+ /** Scroll depth — numeric high-water mark, not array/boolean. */
165
+ export function trackScrollDepth(slideId, percentage) {
166
+ if (!slideId || typeof percentage !== 'number') return;
167
+ const state = this._getState();
168
+ if (!state[slideId]) return;
169
+ const currentDepth = state[slideId].tracked.scrollDepth;
170
+ if (percentage > currentDepth) {
171
+ state[slideId].tracked.scrollDepth = Math.min(100, Math.max(0, percentage));
172
+ this._setState(state);
173
+ logger.debug(`[EngagementManager] Scroll: ${percentage}% for ${slideId}`);
174
+ this._checkAndEmitProgress(slideId);
175
+ }
176
+ }
177
+
178
+ // =========================================================================
179
+ // Active Selection Persistence
180
+ // =========================================================================
181
+
182
+ /** Save the currently active tab so it can be restored on revisit. */
183
+ export function saveActiveTab(slideId, tabId) {
184
+ if (!slideId || !tabId) return;
185
+ const state = this._getState();
186
+ if (!state[slideId]) return;
187
+ state[slideId].tracked.activeTab = tabId;
188
+ this._setState(state);
189
+ }
190
+
191
+ /** Get the last active tab for a slide, or null if none saved. */
192
+ export function getActiveTab(slideId) {
193
+ if (!slideId) return null;
194
+ const state = this._getState();
195
+ if (!state[slideId]) return null;
196
+ return state[slideId].tracked.activeTab ?? null;
197
+ }
198
+
199
+ // =========================================================================
200
+ // Queries
201
+ // =========================================================================
202
+
203
+ export function isSlideVideoComplete(slideId) {
204
+ if (!slideId) return false;
205
+ const state = this._getState();
206
+ if (!state[slideId]) return true;
207
+ return state[slideId].tracked?.videoComplete || false;
208
+ }
209
+
210
+ export function isSlideAudioComplete(slideId) {
211
+ if (!slideId) return false;
212
+ const state = this._getState();
213
+ if (!state[slideId]) return true;
214
+ return state[slideId].tracked?.audioComplete || false;
215
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * @file requirement-strategies.js
3
+ * @description Strategy map for engagement requirement types.
4
+ * Each strategy encapsulates evaluate, progress, label, and fields logic
5
+ * for a single requirement type.
6
+ *
7
+ * Strategy interface:
8
+ * fields: { key: defaultValue } — tracked state fields this strategy reads
9
+ * evaluate(req, tracked, ctx) → { met, type, message, ...extra }
10
+ * progress(req, tracked, result, ctx) → number (0–1)
11
+ * label(req, tracked, result, ctx) → string
12
+ *
13
+ * ctx: { slideId, stateManager, interactionRegistry, formatTime, formatInteractionId }
14
+ */
15
+
16
+ // ── Helpers for "view all" pattern ──────────────────────────────────
17
+ function viewAllStrategy(trackedKey, totalKey, defaultLabel) {
18
+ return {
19
+ fields: { [trackedKey]: [], [totalKey]: 0 },
20
+ evaluate(req, tracked) {
21
+ const total = tracked[totalKey] || 0;
22
+ const viewed = tracked[trackedKey] || [];
23
+ return {
24
+ met: total > 0 && viewed.length >= total,
25
+ type: req.type,
26
+ message: req.message,
27
+ viewed: viewed.length,
28
+ total
29
+ };
30
+ },
31
+ progress(req, tracked) {
32
+ const total = tracked[totalKey] || 0;
33
+ return total > 0 ? (tracked[trackedKey]?.length || 0) / total : 0;
34
+ },
35
+ label(req, tracked) {
36
+ const total = tracked[totalKey] || 0;
37
+ const viewed = tracked[trackedKey]?.length || 0;
38
+ return `${defaultLabel} (${viewed}/${total})`;
39
+ }
40
+ };
41
+ }
42
+
43
+ /** @type {Record<string, {fields?, evaluate, progress, label}>} */
44
+ const strategies = {
45
+ viewAllTabs: viewAllStrategy('tabsViewed', 'tabsTotal', 'View all tabs'),
46
+ viewAllPanels: viewAllStrategy('accordionPanelsViewed', 'accordionPanelsTotal', 'View all sections'),
47
+ viewAllFlipCards: viewAllStrategy('flipCardsViewed', 'flipCardsTotal', 'Flip all cards'),
48
+ viewAllHotspots: viewAllStrategy('interactiveImageHotspotsViewed', 'interactiveImageHotspotsTotal', 'View all hotspots'),
49
+ viewAllModals: viewAllStrategy('modalsViewed', 'modalsTotal', 'View all modals'),
50
+ viewAllLightboxes: viewAllStrategy('lightboxesViewed', 'lightboxesTotal', 'View all lightboxes'),
51
+ viewAllTimelineEvents: viewAllStrategy('timelineEventsViewed', 'timelineEventsTotal', 'View all events'),
52
+
53
+ interactionComplete: {
54
+ fields: { interactionsCompleted: {} },
55
+ evaluate(req, tracked) {
56
+ const interaction = tracked.interactionsCompleted[req.interactionId];
57
+ const completed = interaction?.completed || false;
58
+ const correct = interaction?.correct || false;
59
+ const met = req.requireCorrect ? (completed && correct) : completed;
60
+ return { met, type: req.type, message: req.message, interactionId: req.interactionId, completed, correct };
61
+ },
62
+ progress(req, tracked, result) {
63
+ return result.met ? 1 : 0;
64
+ },
65
+ label(req, _tracked, _result, ctx) {
66
+ const registered = ctx.interactionRegistry?.getAll().find(i => i.id === req.interactionId);
67
+ const interactionLabel = req.label || registered?.config?.label || ctx.formatInteractionId(req.interactionId);
68
+ return `Complete: ${interactionLabel}`;
69
+ }
70
+ },
71
+
72
+ allInteractionsComplete: {
73
+ // shares interactionsCompleted field with interactionComplete
74
+ evaluate(req, tracked, ctx) {
75
+ const completedCount = Object.values(tracked.interactionsCompleted)
76
+ .filter(i => req.requireCorrect ? (i.completed && i.correct) : i.completed).length;
77
+ const registered = ctx.interactionRegistry?.getAll() || [];
78
+ const total = registered.length;
79
+ return { met: total > 0 && completedCount >= total, type: req.type, message: req.message, completed: completedCount, total };
80
+ },
81
+ progress(req, tracked, result, ctx) {
82
+ const registered = ctx.interactionRegistry?.getAll() || [];
83
+ const total = registered.length;
84
+ if (total <= 0) return 0;
85
+ const completedCount = Object.values(tracked.interactionsCompleted)
86
+ .filter(i => req.requireCorrect ? (i.completed && i.correct) : i.completed).length;
87
+ return completedCount / total;
88
+ },
89
+ label(_req, _tracked, result) {
90
+ return `Complete all interactions (${result.completed}/${result.total})`;
91
+ }
92
+ },
93
+
94
+ scrollDepth: {
95
+ fields: { scrollDepth: 0 },
96
+ evaluate(req, tracked) {
97
+ const required = req.percentage || req.minPercentage || 95;
98
+ return { met: tracked.scrollDepth >= required, type: req.type, message: req.message, current: tracked.scrollDepth, required };
99
+ },
100
+ progress(req, tracked) {
101
+ const required = req.percentage || req.minPercentage || 95;
102
+ return Math.min(1, tracked.scrollDepth / required);
103
+ },
104
+ label(_req, _tracked, result) {
105
+ return `Scroll: ${result.current}% / ${result.required}%`;
106
+ }
107
+ },
108
+
109
+ timeOnSlide: {
110
+ // No tracked fields — reads from sessionData domain, not engagement tracked state
111
+ evaluate(req, _tracked, ctx) {
112
+ const sessionData = ctx.stateManager.getDomainState('sessionData') || {};
113
+ const slideDurations = sessionData.slideDurations || {};
114
+ const slideStartTimes = sessionData.slideStartTimes || {};
115
+ let timeSpentMs = slideDurations[ctx.slideId] || 0;
116
+ const activeStartTime = slideStartTimes[ctx.slideId];
117
+ if (activeStartTime) timeSpentMs += Date.now() - activeStartTime;
118
+ const current = Math.floor(timeSpentMs / 1000);
119
+ const required = req.minSeconds || 0;
120
+ return { met: current >= required, type: req.type, message: req.message, current, required };
121
+ },
122
+ progress(req, _tracked, _result, ctx) {
123
+ const sessionData = ctx.stateManager.getDomainState('sessionData') || {};
124
+ const slideDurations = sessionData.slideDurations || {};
125
+ const slideStartTimes = sessionData.slideStartTimes || {};
126
+ let timeSpentMs = slideDurations[ctx.slideId] || 0;
127
+ const activeStartTime = slideStartTimes[ctx.slideId];
128
+ if (activeStartTime) timeSpentMs += Date.now() - activeStartTime;
129
+ const minMs = (req.minSeconds || 0) * 1000;
130
+ return minMs > 0 ? Math.min(1, timeSpentMs / minMs) : 0;
131
+ },
132
+ label(_req, _tracked, result, ctx) {
133
+ return `Please spend ${ctx.formatTime(result.required)} on this slide (${ctx.formatTime(result.current)} so far)`;
134
+ }
135
+ },
136
+
137
+ flag: {
138
+ // No tracked fields — reads from flags domain via ctx.stateManager
139
+ evaluate(req, _tracked, ctx) {
140
+ const flags = ctx.stateManager.getDomainState('flags') || {};
141
+ const flagValue = flags[req.key];
142
+ const met = req.equals !== undefined ? flagValue === req.equals : !!flagValue;
143
+ return { met, type: req.type, message: req.message || `Flag "${req.key}" must be set`, flagKey: req.key, currentValue: flagValue, requiredValue: req.equals };
144
+ },
145
+ progress(_req, _tracked, result) {
146
+ return result.met ? 1 : 0;
147
+ },
148
+ label(req) {
149
+ return req.message || 'Complete required step';
150
+ }
151
+ },
152
+
153
+ allFlags: {
154
+ // No tracked fields — reads from flags domain via ctx.stateManager
155
+ evaluate(req, _tracked, ctx) {
156
+ const flags = ctx.stateManager.getDomainState('flags') || {};
157
+ const requiredFlags = req.flags || [];
158
+ const results = requiredFlags.map(flagConfig => {
159
+ const key = typeof flagConfig === 'string' ? flagConfig : flagConfig.key;
160
+ const value = flags[key];
161
+ const isMet = (typeof flagConfig === 'object' && flagConfig.equals !== undefined) ? value === flagConfig.equals : !!value;
162
+ return { key, met: isMet, value };
163
+ });
164
+ const metCount = results.filter(r => r.met).length;
165
+ return { met: metCount === requiredFlags.length, type: req.type, message: req.message || 'Complete all required steps', flags: results, completed: metCount, total: requiredFlags.length };
166
+ },
167
+ progress(_req, _tracked, result) {
168
+ return result.total > 0 ? result.completed / result.total : 0;
169
+ },
170
+ label(req) {
171
+ return req.message || 'Complete all required steps';
172
+ }
173
+ },
174
+
175
+ slideAudioComplete: {
176
+ fields: { audioComplete: false },
177
+ evaluate(req, tracked) {
178
+ return { met: tracked.audioComplete || false, type: req.type, message: req.message || 'Listen to the audio narration' };
179
+ },
180
+ progress(_req, _tracked, result) {
181
+ return result.met ? 1 : 0;
182
+ },
183
+ label(req) {
184
+ return req.message || 'Listen to the slide narration';
185
+ }
186
+ },
187
+
188
+ audioComplete: {
189
+ fields: { standaloneAudioComplete: [] },
190
+ evaluate(req, tracked) {
191
+ if (!req.audioId) throw new Error("[EngagementManager] audioComplete requirement requires 'audioId' property. For slide-level audio, use 'slideAudioComplete' instead.");
192
+ const complete = (tracked.standaloneAudioComplete || []).includes(req.audioId);
193
+ return { met: complete, type: req.type, audioId: req.audioId, message: req.message || `Listen to the audio: ${req.audioId}` };
194
+ },
195
+ progress(_req, _tracked, result) {
196
+ return result.met ? 1 : 0;
197
+ },
198
+ label(req) {
199
+ return req.message || 'Listen to the audio';
200
+ }
201
+ },
202
+
203
+ modalAudioComplete: {
204
+ fields: { modalsAudioComplete: [] },
205
+ evaluate(req, tracked) {
206
+ if (!req.modalId) throw new Error("[EngagementManager] modalAudioComplete requirement requires 'modalId' property.");
207
+ const complete = (tracked.modalsAudioComplete || []).includes(req.modalId);
208
+ return { met: complete, type: req.type, modalId: req.modalId, message: req.message || `Listen to the modal audio: ${req.modalId}` };
209
+ },
210
+ progress(_req, _tracked, result) {
211
+ return result.met ? 1 : 0;
212
+ },
213
+ label(req) {
214
+ return req.message || 'Listen to the modal audio';
215
+ }
216
+ },
217
+
218
+ slideVideoComplete: {
219
+ fields: { videoComplete: false },
220
+ evaluate(req, tracked) {
221
+ return { met: tracked.videoComplete || false, type: req.type, message: req.message || 'Watch the video' };
222
+ },
223
+ progress(_req, _tracked, result) {
224
+ return result.met ? 1 : 0;
225
+ },
226
+ label(req) {
227
+ return req.message || 'Watch the video';
228
+ }
229
+ },
230
+
231
+ videoComplete: {
232
+ fields: { standaloneVideoComplete: [] },
233
+ evaluate(req, tracked) {
234
+ if (!req.videoId) throw new Error("[EngagementManager] videoComplete requirement requires 'videoId' property. For slide-level video, use 'slideVideoComplete' instead.");
235
+ const complete = (tracked.standaloneVideoComplete || []).includes(req.videoId);
236
+ return { met: complete, type: req.type, videoId: req.videoId, message: req.message || `Watch the video: ${req.videoId}` };
237
+ },
238
+ progress(_req, _tracked, result) {
239
+ return result.met ? 1 : 0;
240
+ },
241
+ label(req) {
242
+ return req.message || 'Watch the video';
243
+ }
244
+ }
245
+ };
246
+
247
+ export default strategies;
248
+
249
+ /** All valid requirement type names */
250
+ export const validTypes = Object.keys(strategies);
251
+
252
+ // ── Core fields not owned by any single strategy ────────────────────
253
+ const CORE_FIELDS = { flipCardsRegistered: [] };
254
+
255
+ /**
256
+ * Aggregate all tracked field defaults from strategy declarations.
257
+ * Used by engagement-manager (initSlide) and engagement-progress (merge/strip).
258
+ * @returns {Record<string, any>} field name → default value
259
+ */
260
+ export function getTrackedFieldDefaults() {
261
+ const defaults = { ...CORE_FIELDS };
262
+ for (const strategy of Object.values(strategies)) {
263
+ if (strategy.fields) {
264
+ Object.assign(defaults, strategy.fields);
265
+ }
266
+ }
267
+ return defaults;
268
+ }