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,442 @@
1
+ /**
2
+ * @file accordion.js
3
+ * @description Accessible accordion interface with keyboard navigation and engagement tracking.
4
+ *
5
+ * Usage:
6
+ * import { initAccordion } from '../framework/js/components/ui-components/accordion.js';
7
+ *
8
+ * initAccordion(root, {
9
+ * id: 'faq-accordion',
10
+ * mode: 'single', // 'single' or 'multi'
11
+ * items: [
12
+ * { id: 'panel1', title: 'Question 1', content: '<p>Answer...</p>' },
13
+ * { id: 'panel2', title: 'Question 2', content: '<p>Answer...</p>' }
14
+ * ],
15
+ * defaultOpen: ['panel1']
16
+ * });
17
+ *
18
+ * Data Attributes:
19
+ * data-mode="single" - Only one panel open at a time
20
+ * data-mode="multi" - Multiple panels can be open (default)
21
+ * data-always-open - Prevent closing the last open panel (requires at least one open)
22
+ *
23
+ * Note: Audio is NOT supported on accordions due to multi-panel complexity.
24
+ * Use tabs (single panel visible) or modals for audio content.
25
+ */
26
+
27
+ import { announceToScreenReader } from './index.js';
28
+ import engagementManager from '../../engagement/engagement-manager.js';
29
+ import * as NavigationState from '../../navigation/NavigationState.js';
30
+ import { eventBus } from '../../core/event-bus.js';
31
+ import { logger } from '../../utilities/logger.js';
32
+
33
+ // Schema for validation, linting, and AI-assisted authoring
34
+ export const schema = {
35
+ type: 'accordion',
36
+ description: 'Accessible accordion with keyboard navigation and engagement tracking',
37
+ example: `<div data-component="accordion" id="preview-accordion" data-mode="multi">
38
+ <div class="accordion-item" data-panel-id="getting-started">
39
+ <button class="accordion-button" data-panel="getting-started" data-action="toggle-accordion-panel" aria-expanded="true" aria-controls="preview-accordion-panel-getting-started"><span class="accordion-title">Getting Started</span><span class="accordion-icon"></span></button>
40
+ <div id="preview-accordion-panel-getting-started" class="accordion-content show" role="region"><div class="accordion-body">Learn the basics of course authoring with step-by-step guidance.</div></div>
41
+ </div>
42
+ <div class="accordion-item" data-panel-id="advanced">
43
+ <button class="accordion-button collapsed" data-panel="advanced" data-action="toggle-accordion-panel" aria-expanded="false" aria-controls="preview-accordion-panel-advanced"><span class="accordion-title">Advanced Features</span><span class="accordion-icon"></span></button>
44
+ <div id="preview-accordion-panel-advanced" class="accordion-content" role="region" hidden><div class="accordion-body">Explore components, interactions, and layout patterns.</div></div>
45
+ </div>
46
+ <div class="accordion-item" data-panel-id="publishing">
47
+ <button class="accordion-button collapsed" data-panel="publishing" data-action="toggle-accordion-panel" aria-expanded="false" aria-controls="preview-accordion-panel-publishing"><span class="accordion-title">Publishing</span><span class="accordion-icon"></span></button>
48
+ <div id="preview-accordion-panel-publishing" class="accordion-content" role="region" hidden><div class="accordion-body">Export your course for LMS deployment.</div></div>
49
+ </div>
50
+ </div>`,
51
+ properties: {
52
+ mode: { type: 'string', enum: ['single', 'multi'], default: 'multi', description: 'single = one panel open, multi = multiple open' },
53
+ alwaysOpen: { type: 'boolean', default: false, description: 'Prevent closing last open panel (data-always-open attribute)' }
54
+ },
55
+ structure: {
56
+ container: '[data-component="accordion"]',
57
+ children: {
58
+ item: { selector: '.accordion-item', required: true, minItems: 1 },
59
+ button: { selector: '.accordion-button', parent: '.accordion-item', required: true },
60
+ content: { selector: '.accordion-content', parent: '.accordion-item', required: true }
61
+ }
62
+ }
63
+ };
64
+
65
+ export const metadata = {
66
+ category: 'ui-component',
67
+ cssFile: 'components/accordions.css',
68
+ engagementTracking: 'viewAllPanels',
69
+ emitsEvents: ['accordion:toggled']
70
+ };
71
+
72
+
73
+ /**
74
+ * Creates and initializes an accordion component
75
+ * @param {HTMLElement|string} root - Container element or selector
76
+ * @param {Object} options - Configuration options
77
+ * @param {string} options.id - Unique accordion ID
78
+ * @param {string} [options.mode='multi'] - 'single' (one open) or 'multi' (multiple open)
79
+ * @param {Array} options.items - Array of {id, title, content}
80
+ * @param {Array} [options.defaultOpen=[]] - Array of panel IDs to open by default
81
+ * @param {Function} [options.onToggle] - Callback when panel toggles
82
+ * @returns {Object} Accordion API
83
+ */
84
+ export function init(root, options = {}) {
85
+ const accordionElement = typeof root === 'string' ? document.querySelector(root) : root;
86
+ if (!accordionElement) {
87
+ logger.fatal('UIComponents.initAccordion: container not found', { domain: 'ui', operation: 'initAccordion' });
88
+ return;
89
+ }
90
+
91
+ // Configuration is read from the element's ID and data attributes
92
+ const id = accordionElement.id;
93
+ const mode = accordionElement.dataset.mode || 'multi';
94
+ const alwaysOpen = accordionElement.hasAttribute('data-always-open');
95
+ const onToggle = typeof options.onToggle === 'function' ? options.onToggle : null;
96
+
97
+ if (!id) {
98
+ logger.fatal('UIComponents.initAccordion: The accordion container element must have an ID.', { domain: 'ui', operation: 'initAccordion' });
99
+ return;
100
+ }
101
+ if (mode !== 'single' && mode !== 'multi') {
102
+ logger.fatal('UIComponents.initAccordion: data-mode attribute must be "single" or "multi"', { domain: 'ui', operation: 'initAccordion' });
103
+ return;
104
+ }
105
+
106
+ // HYDRATION: Support simplified syntax
107
+ // If no .accordion-item elements exist, check for children with data-title
108
+ if (accordionElement.querySelectorAll('.accordion-item').length === 0) {
109
+ const simpleItems = Array.from(accordionElement.children).filter(el => el.hasAttribute('data-title'));
110
+
111
+ if (simpleItems.length > 0) {
112
+ simpleItems.forEach((item, index) => {
113
+ const title = item.getAttribute('data-title');
114
+ // Preserve existing content
115
+ const content = item.innerHTML;
116
+ // Generate a unique ID suffix for this panel
117
+ const uniqueSuffix = `item-${index + 1}`;
118
+
119
+ const wrapper = document.createElement('div');
120
+ wrapper.className = 'accordion-item';
121
+ wrapper.setAttribute('data-panel-id', uniqueSuffix);
122
+
123
+ wrapper.innerHTML = `
124
+ <button class="accordion-button collapsed" data-panel="${uniqueSuffix}" data-action="toggle-accordion-panel">
125
+ <span class="accordion-title">${title}</span>
126
+ <span class="accordion-icon"></span>
127
+ </button>
128
+ <div id="${id}-panel-${uniqueSuffix}" class="accordion-content" hidden>
129
+ <div class="accordion-body">${content}</div>
130
+ </div>
131
+ `;
132
+
133
+ accordionElement.replaceChild(wrapper, item);
134
+ });
135
+ }
136
+ }
137
+
138
+ const buttons = Array.from(accordionElement.querySelectorAll('.accordion-button'));
139
+ const items = Array.from(accordionElement.querySelectorAll('.accordion-item')).map(itemEl => {
140
+ // Try to get ID from the item wrapper first, then fallback to the button inside
141
+ const button = itemEl.querySelector('.accordion-button');
142
+ const id = itemEl.dataset.panelId || button?.dataset.panel;
143
+
144
+ return {
145
+ id,
146
+ title: itemEl.querySelector('.accordion-title')?.textContent || '',
147
+ };
148
+ });
149
+
150
+ if (!buttons.length) {
151
+ // No buttons found, nothing to initialize.
152
+ return {
153
+ destroy: () => {}
154
+ };
155
+ }
156
+
157
+ // Validate structure: Check if all buttons have corresponding panels
158
+ const errors = [];
159
+ buttons.forEach((btn, index) => {
160
+ const panelId = btn.dataset.panel;
161
+ if (!panelId) {
162
+ errors.push(`Button ${index + 1} is missing data-panel attribute.`);
163
+ return;
164
+ }
165
+ const content = accordionElement.querySelector(`#${id}-panel-${panelId}`);
166
+ if (!content) {
167
+ errors.push(`Panel content not found for button ${index + 1} (expected id="${id}-panel-${panelId}").`);
168
+ }
169
+ });
170
+
171
+ if (errors.length > 0) {
172
+ logger.fatal(`UIComponents.initAccordion: Invalid structure in #${id}:\n${errors.join('\n')}`, { domain: 'ui', operation: 'initAccordion' });
173
+ return;
174
+ }
175
+
176
+ // Determine initially open panels by inspecting the DOM
177
+ const openPanels = new Set(
178
+ buttons.filter(btn => btn.getAttribute('aria-expanded') === 'true').map(btn => btn.dataset.panel)
179
+ );
180
+
181
+ // Initialize ARIA attributes
182
+ buttons.forEach(btn => {
183
+ const panelId = btn.dataset.panel;
184
+ const content = accordionElement.querySelector(`#${id}-panel-${panelId}`);
185
+
186
+ if (!btn.hasAttribute('aria-expanded')) {
187
+ btn.setAttribute('aria-expanded', 'false');
188
+ }
189
+ if (!btn.hasAttribute('aria-controls') && content) {
190
+ btn.setAttribute('aria-controls', content.id);
191
+ }
192
+
193
+ // Ensure content has role="region" and aria-labelledby
194
+ if (content) {
195
+ if (!content.hasAttribute('role')) {
196
+ content.setAttribute('role', 'region');
197
+ }
198
+ if (!content.hasAttribute('aria-labelledby') && btn.id) {
199
+ content.setAttribute('aria-labelledby', btn.id);
200
+ }
201
+ }
202
+ });
203
+
204
+ // If alwaysOpen is set and no panels are open, open the first one
205
+ if (alwaysOpen && openPanels.size === 0 && buttons.length > 0) {
206
+ const firstPanelId = buttons[0].dataset.panel;
207
+ const firstContent = accordionElement.querySelector(`#${id}-panel-${firstPanelId}`);
208
+ if (firstPanelId && firstContent) {
209
+ openPanels.add(firstPanelId);
210
+ buttons[0].classList.remove('collapsed');
211
+ buttons[0].setAttribute('aria-expanded', 'true');
212
+ firstContent.classList.add('show');
213
+ firstContent.removeAttribute('hidden');
214
+ logger.debug(`[Accordion] Auto-opened first panel for data-always-open: ${firstPanelId}`);
215
+ }
216
+ }
217
+
218
+ // Register with engagement tracking
219
+ const currentSlideId = NavigationState.getCurrentSlideId();
220
+ if (currentSlideId) {
221
+ const panelIds = items.map(item => item.id);
222
+ engagementManager.registerAccordion(currentSlideId, panelIds);
223
+
224
+ // Track initially open panels for engagement progress.
225
+ // Users who see open content should get credit for viewing it.
226
+ // This tracks panels that are open when the accordion initializes.
227
+ if (openPanels.size > 0) {
228
+ openPanels.forEach(panelId => {
229
+ engagementManager.trackAccordionPanel(currentSlideId, panelId);
230
+ });
231
+ logger.debug(`[Accordion] Tracked ${openPanels.size} initially open panels for engagement: ${Array.from(openPanels).join(', ')}`);
232
+ }
233
+ }
234
+
235
+ // Track pending transitions to prevent overlapping animations
236
+ let transitionTimeout = null;
237
+
238
+ // Update locked state for always-open accordions
239
+ // When only one panel is open and alwaysOpen is set, mark it as locked (can't close)
240
+ function updateLockedState() {
241
+ if (!alwaysOpen) return;
242
+
243
+ buttons.forEach(btn => {
244
+ const panelId = btn.dataset.panel;
245
+ const isOpen = openPanels.has(panelId);
246
+ const isLastOpen = isOpen && openPanels.size === 1;
247
+
248
+ btn.classList.toggle('accordion-locked', isLastOpen);
249
+ });
250
+ }
251
+
252
+ // Initialize locked state
253
+ updateLockedState();
254
+
255
+ function togglePanel(panelId) {
256
+ const button = accordionElement.querySelector(`[data-panel="${panelId}"]`);
257
+ const content = accordionElement.querySelector(`#${id}-panel-${panelId}`);
258
+
259
+ if (!button || !content) return;
260
+
261
+ // Cancel any pending open actions from rapid clicks
262
+ if (transitionTimeout) {
263
+ clearTimeout(transitionTimeout);
264
+ transitionTimeout = null;
265
+ }
266
+
267
+ const isCurrentlyOpen = openPanels.has(panelId);
268
+
269
+ if (isCurrentlyOpen) {
270
+ // Case 1: Closing the currently open panel
271
+ // If alwaysOpen is set and this is the last open panel, prevent closing
272
+ if (alwaysOpen && openPanels.size === 1) {
273
+ logger.debug('[Accordion] Cannot close last panel - data-always-open is set');
274
+ announceToScreenReader('At least one panel must remain open');
275
+ return;
276
+ }
277
+ // Action: Close immediately
278
+ closePanel(panelId);
279
+ notifyToggle(panelId, false);
280
+ } else {
281
+ // Case 2: Opening a new panel
282
+ if (mode === 'single' && openPanels.size > 0) {
283
+ // Crossfade: Open new and close old simultaneously
284
+ // Both panels animate at the same time for smooth transition
285
+ const panelsToClose = Array.from(openPanels);
286
+
287
+ // Start opening new panel first (so it's visible during transition)
288
+ openPanel(panelId);
289
+ notifyToggle(panelId, true);
290
+
291
+ // Close old panels (animated close is now built into closePanel)
292
+ // Notify for each closed panel so linked components (e.g. interactive-image) can update
293
+ panelsToClose.forEach(openId => {
294
+ closePanel(openId, false);
295
+ notifyToggle(openId, false);
296
+ });
297
+ } else {
298
+ // Case 3: Opening panel (Multi mode OR Single mode with nothing open)
299
+ // Action: Open immediately
300
+ openPanel(panelId);
301
+ notifyToggle(panelId, true);
302
+ }
303
+ }
304
+ }
305
+
306
+ function notifyToggle(panelId, isOpen) {
307
+ if (currentSlideId && isOpen) {
308
+ engagementManager.trackAccordionPanel(currentSlideId, panelId);
309
+ }
310
+
311
+ if (onToggle) {
312
+ onToggle({ panelId, isOpen, mode });
313
+ }
314
+
315
+ eventBus.emit('accordion:toggled', {
316
+ accordionId: id,
317
+ panelId: panelId,
318
+ isOpen: isOpen
319
+ });
320
+ }
321
+
322
+ function openPanel(panelId, announce = true) {
323
+ const button = accordionElement.querySelector(`[data-panel="${panelId}"]`);
324
+ const content = accordionElement.querySelector(`#${id}-panel-${panelId}`);
325
+
326
+ if (!button || !content) return;
327
+
328
+ openPanels.add(panelId);
329
+ button.classList.remove('collapsed');
330
+ button.setAttribute('aria-expanded', 'true');
331
+ content.classList.add('show');
332
+ content.removeAttribute('hidden');
333
+
334
+ updateLockedState();
335
+
336
+ if (announce) {
337
+ const title = button.querySelector('.accordion-title')?.textContent || 'Panel';
338
+ announceToScreenReader(`${title} expanded`);
339
+ }
340
+ }
341
+
342
+ function closePanel(panelId, announce = true) {
343
+ const button = accordionElement.querySelector(`[data-panel="${panelId}"]`);
344
+ const content = accordionElement.querySelector(`#${id}-panel-${panelId}`);
345
+
346
+ if (!button || !content) return;
347
+
348
+ openPanels.delete(panelId);
349
+ button.classList.add('collapsed');
350
+ button.setAttribute('aria-expanded', 'false');
351
+ content.classList.remove('show'); // Starts shrinking + fade animation
352
+ // Don't add hidden yet - let it animate first
353
+
354
+ updateLockedState();
355
+
356
+ if (announce) {
357
+ const title = button.querySelector('.accordion-title')?.textContent || 'Panel';
358
+ announceToScreenReader(`${title} collapsed`);
359
+ }
360
+
361
+ // Add hidden after transition completes (matches CSS transition duration)
362
+ setTimeout(() => {
363
+ // Only add hidden if panel is still closed (wasn't re-opened)
364
+ if (!content.classList.contains('show')) {
365
+ content.setAttribute('hidden', 'hidden');
366
+ }
367
+ }, 350); // Matches CSS transition duration
368
+ }
369
+
370
+ function openAll() {
371
+ if (mode === 'single') {
372
+ throw new Error('UIComponents.initAccordion: openAll() is not available in single mode. Use mode="multi" or call togglePanel() individually.');
373
+ }
374
+ items.forEach(item => openPanel(item.id, false));
375
+ announceToScreenReader('All panels expanded');
376
+ }
377
+
378
+ function closeAll() {
379
+ items.forEach(item => closePanel(item.id, false));
380
+ announceToScreenReader('All panels collapsed');
381
+ }
382
+
383
+ const clickHandler = (event) => {
384
+ const button = event.target.closest('[data-action="toggle-accordion-panel"]');
385
+ if (!button) return;
386
+
387
+ event.preventDefault();
388
+ const panelId = button.dataset.panel;
389
+ if (panelId) {
390
+ togglePanel(panelId);
391
+ }
392
+ };
393
+
394
+ const keydownHandler = (event) => {
395
+ const button = event.target.closest('.accordion-button');
396
+ if (!button) return;
397
+
398
+ const currentIndex = buttons.indexOf(button);
399
+
400
+ switch (event.key) {
401
+ case 'ArrowDown':
402
+ case 'ArrowRight':
403
+ event.preventDefault();
404
+ const nextIndex = (currentIndex + 1) % buttons.length;
405
+ buttons[nextIndex].focus();
406
+ break;
407
+
408
+ case 'ArrowUp':
409
+ case 'ArrowLeft':
410
+ event.preventDefault();
411
+ const prevIndex = (currentIndex - 1 + buttons.length) % buttons.length;
412
+ buttons[prevIndex].focus();
413
+ break;
414
+
415
+ case 'Home':
416
+ event.preventDefault();
417
+ buttons[0].focus();
418
+ break;
419
+
420
+ case 'End':
421
+ event.preventDefault();
422
+ buttons[buttons.length - 1].focus();
423
+ break;
424
+ }
425
+ };
426
+
427
+ accordionElement.addEventListener('click', clickHandler);
428
+ accordionElement.addEventListener('keydown', keydownHandler);
429
+
430
+ return {
431
+ togglePanel,
432
+ openPanel,
433
+ closePanel,
434
+ openAll,
435
+ closeAll,
436
+ getOpenPanels: () => Array.from(openPanels),
437
+ destroy: () => {
438
+ accordionElement.removeEventListener('click', clickHandler);
439
+ accordionElement.removeEventListener('keydown', keydownHandler);
440
+ }
441
+ };
442
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @file alert.js
3
+ * @description Handles dismissible, in-page alerts.
4
+ *
5
+ * Usage (Declarative):
6
+ * <div class="alert alert-warning" data-component="alert">
7
+ * <p>This is a warning message.</p>
8
+ * <button class="alert-close" data-action="dismiss-alert" aria-label="Dismiss alert">&times;</button>
9
+ * </div>
10
+ */
11
+
12
+ export const schema = {
13
+ type: 'alert',
14
+ description: 'Dismissible in-page alert message',
15
+ example: `<div class="alert alert-warning" data-component="alert">
16
+ <p><strong>Important:</strong> Please review the course requirements before proceeding.</p>
17
+ <button class="alert-close" data-action="dismiss-alert" aria-label="Dismiss alert">&times;</button>
18
+ </div>`,
19
+ properties: {},
20
+ structure: {
21
+ container: '[data-component="alert"]',
22
+ children: {
23
+ close: { selector: '[data-action="dismiss-alert"]', required: false }
24
+ }
25
+ }
26
+ };
27
+
28
+ export const metadata = {
29
+ category: 'ui-component',
30
+ cssFile: 'components/callouts.css',
31
+ engagementTracking: null,
32
+ emitsEvents: []
33
+ };
34
+
35
+ import { iconManager } from '../../utilities/icons.js';
36
+ import { logger } from '../../utilities/logger.js';
37
+
38
+ /**
39
+ * Initializes a dismissible alert component.
40
+ * @param {HTMLElement} alertElement - The alert container element.
41
+ * @returns {object} An object with a `destroy` method.
42
+ */
43
+ export function init(alertElement) {
44
+ if (!alertElement) {
45
+ logger.fatal('initAlert: alertElement not found.', { domain: 'ui', operation: 'initAlert' });
46
+ return;
47
+ }
48
+
49
+ const closeButton = alertElement.querySelector('[data-action="dismiss-alert"]');
50
+
51
+ // Inject standardize icon if button exists
52
+ if (closeButton) {
53
+ closeButton.innerHTML = iconManager.getIcon('x');
54
+ }
55
+
56
+ if (!closeButton) {
57
+ // If there's no close button, there's nothing to initialize.
58
+ return {
59
+ destroy: () => { }
60
+ };
61
+ }
62
+
63
+ const dismiss = () => {
64
+ alertElement.style.opacity = '0';
65
+ alertElement.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out, max-height 0.3s 0.1s, padding 0.3s 0.1s, margin 0.3s 0.1s';
66
+ alertElement.style.transform = 'scaleY(0.8)';
67
+ alertElement.style.maxHeight = '0';
68
+ alertElement.style.paddingTop = '0';
69
+ alertElement.style.paddingBottom = '0';
70
+ alertElement.style.marginTop = '0';
71
+ alertElement.style.marginBottom = '0';
72
+ alertElement.style.borderWidth = '0';
73
+
74
+
75
+ // Remove from DOM after transition
76
+ setTimeout(() => {
77
+ alertElement.remove();
78
+ }, 400);
79
+ };
80
+
81
+ closeButton.addEventListener('click', dismiss);
82
+
83
+ return {
84
+ destroy: () => {
85
+ closeButton.removeEventListener('click', dismiss);
86
+ }
87
+ };
88
+ }