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,235 @@
1
+ /**
2
+ * @file interactive-image.js
3
+ * @description Interactive image component with hotspots that open modals and track engagement.
4
+ * Supports integration with Accordion for side-by-side interaction.
5
+ */
6
+
7
+ export const schema = {
8
+ type: 'interactive-image',
9
+ description: 'Image with clickable hotspots for modals or accordion integration',
10
+ example: `<div data-component="interactive-image" class="interactive-image" style="position: relative; display: inline-block;">
11
+ <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='250' fill='%23f1f5f9'%3E%3Crect width='400' height='250' rx='8'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%2394a3b8' font-family='system-ui' font-size='14'%3EInteractive Diagram%3C/text%3E%3C/svg%3E" alt="Interactive diagram">
12
+ <button data-hotspot-id="feature-a" data-title="Feature A" data-body="Details about Feature A" class="hotspot" style="position: absolute; top: 30%; left: 25%; width: 24px; height: 24px; border-radius: 50%; background: #3b82f6; border: 2px solid white; cursor: pointer;" aria-label="Feature A"></button>
13
+ <button data-hotspot-id="feature-b" data-title="Feature B" data-body="Details about Feature B" class="hotspot" style="position: absolute; top: 60%; left: 65%; width: 24px; height: 24px; border-radius: 50%; background: #f59e0b; border: 2px solid white; cursor: pointer;" aria-label="Feature B"></button>
14
+ </div>`,
15
+ properties: {
16
+ accordionId: { type: 'string', dataAttribute: 'data-accordion-id' }
17
+ },
18
+ structure: {
19
+ container: '[data-component="interactive-image"]',
20
+ children: {
21
+ hotspot: { selector: '[data-hotspot-id]', required: true, minItems: 1 }
22
+ }
23
+ }
24
+ };
25
+
26
+ export const metadata = {
27
+ category: 'ui-component',
28
+ cssFile: 'interactions/interactive-image.css',
29
+ engagementTracking: 'viewAllHotspots',
30
+ emitsEvents: []
31
+ };
32
+
33
+ import * as Modal from './modal.js';
34
+ import engagementManager from '../../engagement/engagement-manager.js';
35
+ import * as NavigationState from '../../navigation/NavigationState.js';
36
+ import { logger } from '../../utilities/logger.js';
37
+ import { eventBus } from '../../core/event-bus.js';
38
+
39
+ /**
40
+ * Initializes interactive image components.
41
+ * @param {HTMLElement} root - The container element (or selector).
42
+ * @param {object} options - Configuration options.
43
+ */
44
+ export function init(root, _options = {}) {
45
+ const container = typeof root === 'string' ? document.querySelector(root) : root;
46
+ if (!container) return;
47
+
48
+ const hotspots = Array.from(container.querySelectorAll('[data-hotspot-id]'));
49
+ if (!hotspots.length) return;
50
+
51
+ const currentSlideId = NavigationState.getCurrentSlideId();
52
+ const imageId = container.id || `interactive-image-${Math.random().toString(36).substr(2, 9)}`;
53
+ const accordionId = container.dataset.accordionId;
54
+
55
+ // Register with EngagementManager if on a slide
56
+ if (currentSlideId) {
57
+ const hotspotIds = hotspots.map(h => h.dataset.hotspotId);
58
+ if (typeof engagementManager.registerInteractiveImage === 'function') {
59
+ engagementManager.registerInteractiveImage(currentSlideId, hotspotIds);
60
+ }
61
+ }
62
+
63
+ // Setup Accordion Integration if ID is provided
64
+ let accordionElement = null;
65
+ if (accordionId) {
66
+ accordionElement = document.getElementById(accordionId);
67
+ if (accordionElement) {
68
+ setupAccordionIntegration(container, hotspots, accordionElement, accordionId);
69
+ } else {
70
+ logger.warn(`[InteractiveImage] Accordion #${accordionId} not found for image ${imageId}`);
71
+ }
72
+ }
73
+
74
+ hotspots.forEach(hotspot => {
75
+ // Add accessibility attributes
76
+ hotspot.setAttribute('role', 'button');
77
+ hotspot.setAttribute('tabindex', '0');
78
+ if (!hotspot.getAttribute('aria-label')) {
79
+ hotspot.setAttribute('aria-label', hotspot.dataset.title || 'View details');
80
+ }
81
+
82
+ const handleClick = (e) => {
83
+ e.preventDefault();
84
+ e.stopPropagation();
85
+
86
+ const id = hotspot.dataset.hotspotId;
87
+
88
+ // If accordion is linked, toggle it instead of modal
89
+ if (accordionElement) {
90
+ const button = accordionElement.querySelector(`[data-panel="${id}"]`);
91
+ if (button) {
92
+ button.click(); // Simulate click to toggle
93
+ }
94
+ } else {
95
+ // Default Modal Behavior
96
+ const title = hotspot.dataset.title || 'Details';
97
+ const body = hotspot.dataset.body || hotspot.innerHTML;
98
+
99
+ Modal.show({
100
+ title: title,
101
+ body: body,
102
+ footer: '<button class="btn btn-primary" data-action="close-modal">Close</button>',
103
+ config: { closeOnBackdrop: true, closeOnEscape: true }
104
+ });
105
+ }
106
+
107
+ // Mark as viewed visually
108
+ hotspot.classList.add('viewed');
109
+
110
+ // Track engagement
111
+ if (currentSlideId && typeof engagementManager.trackInteractiveImageView === 'function') {
112
+ engagementManager.trackInteractiveImageView(currentSlideId, id);
113
+ }
114
+ };
115
+
116
+ hotspot.addEventListener('click', handleClick);
117
+ hotspot.addEventListener('keydown', (e) => {
118
+ if (e.key === 'Enter' || e.key === ' ') {
119
+ handleClick(e);
120
+ }
121
+ });
122
+ });
123
+
124
+ logger.debug(`[InteractiveImage] Initialized ${hotspots.length} hotspots for ${imageId}`);
125
+ }
126
+
127
+ /**
128
+ * Sets up bi-directional sync between image hotspots and accordion panels.
129
+ */
130
+ function setupAccordionIntegration(imageContainer, hotspots, accordionElement, accordionId) {
131
+ // Map hotspots by ID for quick access
132
+ const hotspotMap = new Map(hotspots.map(h => [h.dataset.hotspotId, h]));
133
+
134
+ // 1. Listen for Accordion Toggles (via EventBus)
135
+ const handleAccordionToggle = (data) => {
136
+ if (data.accordionId !== accordionId) return;
137
+
138
+ const hotspot = hotspotMap.get(data.panelId);
139
+ if (hotspot) {
140
+ if (data.isOpen) {
141
+ hotspot.classList.add('active');
142
+ hotspot.classList.add('viewed'); // Opening accordion counts as viewing
143
+
144
+ // Also track engagement if opened via accordion
145
+ const currentSlideId = NavigationState.getCurrentSlideId();
146
+ if (currentSlideId && typeof engagementManager.trackInteractiveImageView === 'function') {
147
+ engagementManager.trackInteractiveImageView(currentSlideId, data.panelId);
148
+ }
149
+ } else {
150
+ hotspot.classList.remove('active');
151
+ }
152
+ }
153
+ };
154
+
155
+ eventBus.on('accordion:toggled', handleAccordionToggle);
156
+
157
+ // 2. Hover Effects (Accordion Button -> Image Hotspot)
158
+ const buttons = accordionElement.querySelectorAll('[data-panel]');
159
+ buttons.forEach(btn => {
160
+ const panelId = btn.dataset.panel;
161
+ const hotspot = hotspotMap.get(panelId);
162
+
163
+ if (hotspot) {
164
+ btn.addEventListener('mouseenter', () => {
165
+ hotspot.classList.add('highlighted');
166
+ });
167
+
168
+ btn.addEventListener('mouseleave', () => {
169
+ hotspot.classList.remove('highlighted');
170
+ });
171
+
172
+ btn.addEventListener('focus', () => {
173
+ hotspot.classList.add('highlighted');
174
+ });
175
+
176
+ btn.addEventListener('blur', () => {
177
+ hotspot.classList.remove('highlighted');
178
+ });
179
+ }
180
+ });
181
+
182
+ // 3. Click Outside to Deselect
183
+ // Closes any open accordion panel if clicking outside the accordion.
184
+ // Note: Hotspots stop propagation, so clicking a hotspot won't trigger this.
185
+ // Clicking the image background (which bubbles) WILL trigger this, effectively deselecting.
186
+ const handleOutsideClick = (e) => {
187
+ // Check if click is inside accordion
188
+ const isInsideAccordion = accordionElement.contains(e.target);
189
+
190
+ if (!isInsideAccordion) {
191
+ // Find currently open panel button
192
+ const openButton = accordionElement.querySelector('.accordion-button[aria-expanded="true"]');
193
+ if (openButton) {
194
+ openButton.click(); // Simulate click to close
195
+ }
196
+ }
197
+ };
198
+
199
+ // Component lifecycle cleanup using MutationObserver
200
+ // When the container is removed from DOM, we automatically clean up all listeners
201
+ const cleanupListeners = () => {
202
+ document.removeEventListener('click', safeHandleOutsideClick);
203
+ eventBus.off('accordion:toggled', handleAccordionToggle);
204
+ if (observer) {
205
+ observer.disconnect();
206
+ }
207
+ };
208
+
209
+ const safeHandleOutsideClick = (e) => {
210
+ if (!document.body.contains(imageContainer)) {
211
+ cleanupListeners();
212
+ return;
213
+ }
214
+ handleOutsideClick(e);
215
+ };
216
+
217
+ // Use MutationObserver to detect when container is removed from DOM
218
+ const observer = new MutationObserver((mutations) => {
219
+ for (const mutation of mutations) {
220
+ for (const node of mutation.removedNodes) {
221
+ if (node === imageContainer || node.contains?.(imageContainer)) {
222
+ cleanupListeners();
223
+ return;
224
+ }
225
+ }
226
+ }
227
+ });
228
+
229
+ // Observe parent for child removals (handles SPA navigation cleanup)
230
+ if (imageContainer.parentNode) {
231
+ observer.observe(imageContainer.parentNode, { childList: true });
232
+ }
233
+
234
+ document.addEventListener('click', safeHandleOutsideClick);
235
+ }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * @file interactive-timeline.js
3
+ * @description Interactive timeline component with expandable events and engagement tracking.
4
+ *
5
+ * Usage example:
6
+ * <div class="interactive-timeline" data-component="interactive-timeline">
7
+ * <div class="timeline-event" data-event-id="event1">
8
+ * <div class="timeline-marker"></div>
9
+ * <div class="timeline-date">2020</div>
10
+ * <div class="timeline-summary">
11
+ * <h4>Event Title</h4>
12
+ * <p>Brief description...</p>
13
+ * </div>
14
+ * <div class="timeline-details">
15
+ * <p>Expanded content revealed on click...</p>
16
+ * </div>
17
+ * </div>
18
+ * </div>
19
+ */
20
+
21
+ export const schema = {
22
+ type: 'interactive-timeline',
23
+ description: 'Timeline with expandable events and engagement tracking',
24
+ example: `<div data-component="interactive-timeline" class="interactive-timeline">
25
+ <div class="timeline-event active" data-event-id="event-1"><div class="timeline-event-marker"></div><div class="timeline-event-label">Phase 1</div><div class="timeline-event-content"><h3>Discovery</h3><p>Research and planning phase.</p></div></div>
26
+ <div class="timeline-event" data-event-id="event-2"><div class="timeline-event-marker"></div><div class="timeline-event-label">Phase 2</div><div class="timeline-event-content"><h3>Development</h3><p>Building core functionality.</p></div></div>
27
+ <div class="timeline-event" data-event-id="event-3"><div class="timeline-event-marker"></div><div class="timeline-event-label">Phase 3</div><div class="timeline-event-content"><h3>Launch</h3><p>Deployment and go-live.</p></div></div>
28
+ </div>`,
29
+ properties: {
30
+ mode: { type: 'string', enum: ['free', 'sequential'], default: 'free', dataAttribute: 'data-timeline-mode' }
31
+ },
32
+ structure: {
33
+ container: '[data-component="interactive-timeline"]',
34
+ children: {
35
+ event: { selector: '.timeline-event', required: true, minItems: 1 }
36
+ }
37
+ }
38
+ };
39
+
40
+ export const metadata = {
41
+ category: 'ui-component',
42
+ cssFile: 'components/interactive-timeline.css',
43
+ engagementTracking: 'viewAllTimelineEvents',
44
+ emitsEvents: ['timeline:event-viewed']
45
+ };
46
+
47
+ import { announceToScreenReader } from './index.js';
48
+ import engagementManager from '../../engagement/engagement-manager.js';
49
+ import * as NavigationState from '../../navigation/NavigationState.js';
50
+ import { eventBus } from '../../core/event-bus.js';
51
+ import { logger } from '../../utilities/logger.js';
52
+
53
+ /**
54
+ * Initializes an interactive timeline component.
55
+ * @param {Element|string} root - The timeline container element or selector
56
+ * @param {object} options - Configuration options
57
+ * @param {string} options.mode - 'free' (any order) or 'sequential' (must view in order)
58
+ * @returns {object} API with activate, deactivate, destroy methods
59
+ */
60
+ export function init(root, options = {}) {
61
+ const container = typeof root === 'string' ? document.querySelector(root) : root;
62
+ if (!container) {
63
+ logger.fatal('initInteractiveTimeline: container not found', { domain: 'ui', operation: 'initInteractiveTimeline' });
64
+ return;
65
+ }
66
+
67
+ const mode = options.mode || container.dataset.timelineMode || 'free';
68
+ const events = Array.from(container.querySelectorAll('.timeline-event'));
69
+
70
+ if (!events.length) {
71
+ logger.fatal('initInteractiveTimeline: no timeline events found', { domain: 'ui', operation: 'initInteractiveTimeline' });
72
+ return;
73
+ }
74
+
75
+ // Get current slide ID for engagement tracking
76
+ const currentSlideId = NavigationState.getCurrentSlideId();
77
+
78
+ // Register timeline events for engagement tracking
79
+ if (currentSlideId) {
80
+ const eventIds = events.map(ev => ev.dataset.eventId).filter(Boolean);
81
+ engagementManager.registerTimeline(currentSlideId, eventIds);
82
+ }
83
+
84
+ // Track which events have been viewed
85
+ const viewedEvents = new Set();
86
+ let lastViewedIndex = -1;
87
+
88
+ /**
89
+ * Expands a timeline event to show its details.
90
+ * @param {Element} event - The timeline event element
91
+ * @param {boolean} announce - Whether to announce to screen reader
92
+ */
93
+ function expandEvent(event, announce = true) {
94
+ const eventId = event.dataset.eventId;
95
+ const eventIndex = events.indexOf(event);
96
+
97
+ // In sequential mode, check if previous events are viewed
98
+ if (mode === 'sequential' && eventIndex > 0) {
99
+ const previousViewed = events.slice(0, eventIndex).every(
100
+ ev => viewedEvents.has(ev.dataset.eventId)
101
+ );
102
+ if (!previousViewed) {
103
+ announceToScreenReader('Please view previous events first', 'polite');
104
+ return;
105
+ }
106
+ }
107
+
108
+ // Collapse other events (single-expand behavior)
109
+ events.forEach(ev => {
110
+ if (ev !== event && ev.classList.contains('expanded')) {
111
+ ev.classList.remove('expanded');
112
+ ev.setAttribute('aria-expanded', 'false');
113
+ }
114
+ });
115
+
116
+ // Toggle this event
117
+ const isExpanding = !event.classList.contains('expanded');
118
+ event.classList.toggle('expanded');
119
+ event.setAttribute('aria-expanded', isExpanding ? 'true' : 'false');
120
+
121
+ if (isExpanding) {
122
+ // Track view
123
+ if (!viewedEvents.has(eventId)) {
124
+ viewedEvents.add(eventId);
125
+ lastViewedIndex = Math.max(lastViewedIndex, eventIndex);
126
+
127
+ // Track in engagement manager
128
+ if (currentSlideId && eventId) {
129
+ engagementManager.trackTimelineView(currentSlideId, eventId);
130
+ }
131
+
132
+ // Emit event for other listeners
133
+ eventBus.emit('timeline:event-viewed', {
134
+ timelineId: container.id || 'timeline',
135
+ eventId,
136
+ eventIndex,
137
+ viewedCount: viewedEvents.size,
138
+ totalEvents: events.length
139
+ });
140
+ }
141
+
142
+ // Update visual progress
143
+ updateProgress();
144
+
145
+ if (announce) {
146
+ const title = event.querySelector('h4, h3, .timeline-title')?.textContent || `Event ${eventIndex + 1}`;
147
+ announceToScreenReader(`Expanded: ${title}. ${viewedEvents.size} of ${events.length} events viewed.`);
148
+ }
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Updates the progress indicator if present.
154
+ */
155
+ function updateProgress() {
156
+ const progressEl = container.querySelector('.timeline-progress');
157
+ if (progressEl) {
158
+ progressEl.textContent = `${viewedEvents.size} of ${events.length}`;
159
+ progressEl.setAttribute('aria-valuenow', viewedEvents.size);
160
+ progressEl.setAttribute('aria-valuemax', events.length);
161
+ }
162
+
163
+ // Update visual state of markers
164
+ events.forEach((ev, index) => {
165
+ const eventId = ev.dataset.eventId;
166
+ if (viewedEvents.has(eventId)) {
167
+ ev.classList.add('viewed');
168
+ }
169
+
170
+ // In sequential mode, mark locked events
171
+ if (mode === 'sequential') {
172
+ const isLocked = index > 0 && !events.slice(0, index).every(
173
+ prev => viewedEvents.has(prev.dataset.eventId)
174
+ );
175
+ ev.classList.toggle('locked', isLocked);
176
+ ev.setAttribute('aria-disabled', isLocked ? 'true' : 'false');
177
+ }
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Handles click events on timeline items.
183
+ */
184
+ function handleClick(e) {
185
+ const event = e.target.closest('.timeline-event');
186
+ if (event && events.includes(event)) {
187
+ e.preventDefault();
188
+ expandEvent(event);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Handles keyboard navigation.
194
+ */
195
+ function handleKeydown(e) {
196
+ const event = e.target.closest('.timeline-event');
197
+ if (!event || !events.includes(event)) return;
198
+
199
+ const currentIndex = events.indexOf(event);
200
+ let targetIndex = null;
201
+
202
+ switch (e.key) {
203
+ case 'ArrowDown':
204
+ case 'ArrowRight':
205
+ targetIndex = Math.min(currentIndex + 1, events.length - 1);
206
+ break;
207
+ case 'ArrowUp':
208
+ case 'ArrowLeft':
209
+ targetIndex = Math.max(currentIndex - 1, 0);
210
+ break;
211
+ case 'Home':
212
+ targetIndex = 0;
213
+ break;
214
+ case 'End':
215
+ targetIndex = events.length - 1;
216
+ break;
217
+ case 'Enter':
218
+ case ' ':
219
+ e.preventDefault();
220
+ expandEvent(event);
221
+ return;
222
+ }
223
+
224
+ if (targetIndex !== null && targetIndex !== currentIndex) {
225
+ e.preventDefault();
226
+ const targetEvent = events[targetIndex];
227
+ targetEvent.focus();
228
+ }
229
+ }
230
+
231
+ // Set up ARIA attributes
232
+ container.setAttribute('role', 'list');
233
+ container.setAttribute('aria-label', container.dataset.timelineLabel || 'Interactive timeline');
234
+
235
+ events.forEach((event, index) => {
236
+ event.setAttribute('role', 'listitem');
237
+ event.setAttribute('tabindex', index === 0 ? '0' : '-1');
238
+ event.setAttribute('aria-expanded', 'false');
239
+
240
+ // Ensure event has an ID for accessibility
241
+ if (!event.id && event.dataset.eventId) {
242
+ event.id = `timeline-event-${event.dataset.eventId}`;
243
+ }
244
+ });
245
+
246
+ // Add event listeners
247
+ container.addEventListener('click', handleClick);
248
+ container.addEventListener('keydown', handleKeydown);
249
+
250
+ // Initial progress update
251
+ updateProgress();
252
+
253
+ // Return API
254
+ return {
255
+ /**
256
+ * Expands a specific event by ID or index.
257
+ */
258
+ expandEvent(eventIdOrIndex) {
259
+ const event = typeof eventIdOrIndex === 'number'
260
+ ? events[eventIdOrIndex]
261
+ : events.find(ev => ev.dataset.eventId === eventIdOrIndex);
262
+ if (event) expandEvent(event);
263
+ },
264
+
265
+ /**
266
+ * Gets the current progress.
267
+ */
268
+ getProgress() {
269
+ return {
270
+ viewed: viewedEvents.size,
271
+ total: events.length,
272
+ percentage: Math.round((viewedEvents.size / events.length) * 100),
273
+ viewedIds: Array.from(viewedEvents)
274
+ };
275
+ },
276
+
277
+ /**
278
+ * Cleans up event listeners.
279
+ */
280
+ destroy() {
281
+ container.removeEventListener('click', handleClick);
282
+ container.removeEventListener('keydown', handleKeydown);
283
+ }
284
+ };
285
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Intro Cards Layout Pattern
3
+ *
4
+ * CSS-only component for introduction/overview slides.
5
+ */
6
+
7
+ export const schema = {
8
+ type: 'intro-cards',
9
+ description: 'Introduction section with centered intro text and card grid',
10
+ example: `<div data-component="intro-cards">
11
+ <div class="intro"><h2>Welcome to Courseware</h2><p class="lead">Explore our key features below.</p></div>
12
+ <div class="card-grid">
13
+ <div class="card"><div class="card-body"><h3>Interactive</h3><p>Drag-drop, quizzes, and more.</p></div></div>
14
+ <div class="card"><div class="card-body"><h3>Accessible</h3><p>WCAG-compliant out of the box.</p></div></div>
15
+ </div>
16
+ </div>`,
17
+ properties: {},
18
+ structure: {
19
+ container: '[data-component="intro-cards"]',
20
+ children: {
21
+ intro: { selector: '.intro' },
22
+ lead: { selector: '.intro .lead' },
23
+ cardGrid: { selector: '.card-grid', required: true }
24
+ }
25
+ }
26
+ };
27
+
28
+ export const metadata = {
29
+ category: 'ui-component',
30
+ cssOnly: true,
31
+ cssFile: 'components/intro-cards.css'
32
+ };
33
+
34
+ /** No-op initializer — CSS-only component, registered for consistency. */
35
+ export function init() {}