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,357 @@
1
+ /**
2
+ * @file document-gallery.js
3
+ * @description Sidebar document gallery with auto-discovered thumbnails.
4
+ * Renders a collapsible gallery section below the navigation menu.
5
+ * Documents open in the existing lightbox on click.
6
+ *
7
+ * Config (in course-config.js → navigation.documentGallery):
8
+ * enabled: boolean — Master toggle
9
+ * directory: string — Path relative to course/ for auto-discovery
10
+ * label: string — Gallery section header label
11
+ * icon: string — Lucide icon name for header
12
+ * allowDownloads: boolean — Show download button in lightbox
13
+ * fileTypes: string[] — File extensions to include
14
+ */
15
+
16
+ import { iconManager } from '../utilities/icons.js';
17
+ import { logger } from '../utilities/logger.js';
18
+ import { open as openLightbox } from '../components/ui-components/lightbox.js';
19
+
20
+ /** @type {HTMLElement|null} */
21
+ let galleryContainer = null;
22
+
23
+ /** @type {boolean} */
24
+ let isExpanded = false;
25
+
26
+ /** @type {object|null} */
27
+ let galleryConfig = null;
28
+
29
+ /**
30
+ * Initialize the document gallery.
31
+ * @param {object} courseConfig - Full course configuration object
32
+ */
33
+ export async function init(courseConfig) {
34
+ galleryConfig = courseConfig.navigation?.documentGallery;
35
+
36
+ if (!galleryConfig?.enabled) {
37
+ logger.debug('[DocumentGallery] Disabled or not configured');
38
+ return;
39
+ }
40
+
41
+ galleryContainer = document.getElementById('sidebar-gallery');
42
+ if (!galleryContainer) {
43
+ logger.warn('[DocumentGallery] #sidebar-gallery element not found');
44
+ return;
45
+ }
46
+
47
+ // Fetch the gallery manifest
48
+ const items = await _fetchManifest();
49
+ if (!items || items.length === 0) {
50
+ logger.debug('[DocumentGallery] No documents found');
51
+ return;
52
+ }
53
+
54
+ // Render the gallery
55
+ _render(items);
56
+
57
+ // Show the gallery container
58
+ galleryContainer.removeAttribute('hidden');
59
+
60
+ // Listen for sidebar close to reset gallery state
61
+ _setupSidebarResetListener();
62
+
63
+ logger.debug(`[DocumentGallery] Initialized with ${items.length} items`);
64
+ }
65
+
66
+ /**
67
+ * Fetch the gallery manifest from the build output.
68
+ * @returns {Promise<object[]|null>} Array of document items or null
69
+ */
70
+ async function _fetchManifest() {
71
+ try {
72
+ const response = await fetch('./_gallery-manifest.json');
73
+ if (!response.ok) {
74
+ // Manifest doesn't exist yet (dev mode or no docs) — not an error
75
+ logger.debug('[DocumentGallery] No gallery manifest found (this is normal in dev mode)');
76
+ return null;
77
+ }
78
+ const manifest = await response.json();
79
+ return manifest.items || [];
80
+ } catch (_error) {
81
+ logger.debug('[DocumentGallery] Could not load gallery manifest');
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Render the gallery header and thumbnail grid.
88
+ * @param {object[]} items - Array of document items from manifest
89
+ */
90
+ function _render(items) {
91
+ const label = galleryConfig.label || 'Resources';
92
+ const iconName = galleryConfig.icon || 'file-text';
93
+
94
+ const headerIcon = iconManager.getIcon(iconName, { size: 'sm' });
95
+ const chevronIcon = iconManager.getIcon('chevron-down', { size: 'sm' });
96
+
97
+ galleryContainer.innerHTML = `
98
+ <button class="sidebar-gallery-header"
99
+ aria-expanded="false"
100
+ aria-controls="sidebar-gallery-content"
101
+ data-testid="gallery-toggle">
102
+ <span class="sidebar-gallery-header-icon" aria-hidden="true">${headerIcon}</span>
103
+ <span class="sidebar-gallery-header-label">${label}</span>
104
+ <span class="sidebar-gallery-header-count">(${items.length})</span>
105
+ <span class="sidebar-gallery-header-chevron" aria-hidden="true">${chevronIcon}</span>
106
+ </button>
107
+ <div id="sidebar-gallery-content" class="sidebar-gallery-content" role="region" aria-label="${label}">
108
+ <div class="sidebar-gallery-grid">
109
+ ${items.map(item => _renderItem(item)).join('')}
110
+ </div>
111
+ </div>
112
+ `;
113
+
114
+ // Wire up toggle
115
+ const header = galleryContainer.querySelector('.sidebar-gallery-header');
116
+ header.addEventListener('click', _toggleGallery);
117
+ }
118
+
119
+ /**
120
+ * Render a single gallery item thumbnail.
121
+ * @param {object} item - Document item { src, type, label, thumbnail? }
122
+ * @returns {string} HTML string for the item
123
+ */
124
+ function _renderItem(item) {
125
+ const thumbHtml = _renderThumbnail(item);
126
+ const displayLabel = item.label || _formatFilename(item.src);
127
+ const downloadHtml = galleryConfig.allowDownloads
128
+ ? `<a class="sidebar-gallery-download" href="${item.src}" download title="Download" aria-label="Download ${displayLabel}">${iconManager.getIcon('download', { size: 'xs' })}</a>`
129
+ : '';
130
+
131
+ // Use a button for accessibility — lightbox opens on click
132
+ return `
133
+ <button class="sidebar-gallery-item"
134
+ data-action="gallery-open"
135
+ data-gallery-src="${item.src}"
136
+ data-gallery-type="${item.type}"
137
+ data-testid="gallery-item-${_slugify(item.src)}"
138
+ title="${displayLabel}"
139
+ type="button">
140
+ <div class="sidebar-gallery-thumb">
141
+ ${thumbHtml}
142
+ ${downloadHtml}
143
+ </div>
144
+ <span class="sidebar-gallery-label">${displayLabel}</span>
145
+ </button>
146
+ `;
147
+ }
148
+
149
+ /**
150
+ * Render the thumbnail content based on document type.
151
+ * @param {object} item - Document item
152
+ * @returns {string} HTML for the thumbnail interior
153
+ */
154
+ function _renderThumbnail(item) {
155
+ switch (item.type) {
156
+ case 'image':
157
+ return `<img class="sidebar-gallery-thumb-img" src="${item.src}" alt="${item.label || ''}" loading="lazy">`;
158
+
159
+ case 'pdf':
160
+ if (item.thumbnail) {
161
+ return `<img class="sidebar-gallery-thumb-img" src="${item.thumbnail}" alt="${item.label || 'PDF document'}" loading="lazy">`;
162
+ }
163
+ return `
164
+ <div class="sidebar-gallery-thumb-pdf">
165
+ <span class="sidebar-gallery-thumb-pdf-icon" aria-hidden="true">
166
+ ${iconManager.getIcon('file-text', { size: 'lg' })}
167
+ </span>
168
+ <span class="sidebar-gallery-thumb-pdf-badge">PDF</span>
169
+ </div>
170
+ `;
171
+
172
+ case 'markdown':
173
+ // Markdown thumbnails are rendered async after init
174
+ return `
175
+ <div class="sidebar-gallery-thumb-md" data-md-src="${item.src}">
176
+ <div class="sidebar-gallery-thumb-md-content">
177
+ <p style="opacity: 0.5; font-style: italic;">Loading...</p>
178
+ </div>
179
+ </div>
180
+ `;
181
+
182
+ default:
183
+ return `
184
+ <div class="sidebar-gallery-thumb-pdf">
185
+ <span class="sidebar-gallery-thumb-pdf-icon" aria-hidden="true">
186
+ ${iconManager.getIcon('file', { size: 'lg' })}
187
+ </span>
188
+ </div>
189
+ `;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Toggle gallery expanded/collapsed state.
195
+ */
196
+ function _toggleGallery() {
197
+ isExpanded = !isExpanded;
198
+
199
+ const sidebar = galleryContainer.closest('.sidebar');
200
+
201
+ galleryContainer.classList.toggle('expanded', isExpanded);
202
+
203
+ // Update ARIA
204
+ const header = galleryContainer.querySelector('.sidebar-gallery-header');
205
+ header.setAttribute('aria-expanded', String(isExpanded));
206
+
207
+ // Inverse collapse: toggle nav visibility
208
+ if (sidebar) {
209
+ sidebar.classList.toggle('gallery-expanded', isExpanded);
210
+ }
211
+
212
+ // Load markdown thumbnails on first expand
213
+ if (isExpanded) {
214
+ _loadMarkdownThumbnails();
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Reset gallery to collapsed state.
220
+ * Called when sidebar is closed.
221
+ */
222
+ function _resetGallery() {
223
+ if (!isExpanded || !galleryContainer) return;
224
+
225
+ isExpanded = false;
226
+ galleryContainer.classList.remove('expanded');
227
+
228
+ const header = galleryContainer.querySelector('.sidebar-gallery-header');
229
+ if (header) {
230
+ header.setAttribute('aria-expanded', 'false');
231
+ }
232
+
233
+ const sidebar = galleryContainer.closest('.sidebar');
234
+ if (sidebar) {
235
+ sidebar.classList.remove('gallery-expanded');
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Set up listener for sidebar close to reset gallery state.
241
+ * Uses transitionend on the sidebar to detect when it finishes collapsing.
242
+ */
243
+ function _setupSidebarResetListener() {
244
+ const sidebar = document.getElementById('sidebar');
245
+ if (!sidebar) return;
246
+
247
+ sidebar.addEventListener('transitionend', (event) => {
248
+ // Only respond to the sidebar's own transition (not children)
249
+ if (event.target !== sidebar) return;
250
+
251
+ // Check if sidebar is now collapsed
252
+ if (sidebar.classList.contains('collapsed')) {
253
+ _resetGallery();
254
+ }
255
+ });
256
+
257
+ // Also handle click-based open/close for gallery items
258
+ galleryContainer.addEventListener('click', (event) => {
259
+ // Ignore clicks on download links
260
+ if (event.target.closest('.sidebar-gallery-download')) return;
261
+
262
+ const item = event.target.closest('[data-action="gallery-open"]');
263
+ if (!item) return;
264
+
265
+ event.preventDefault();
266
+ const src = item.dataset.gallerySrc;
267
+ const type = item.dataset.galleryType;
268
+ _openInLightbox(src, type);
269
+ });
270
+ }
271
+
272
+ /**
273
+ * Open a document in the lightbox.
274
+ * @param {string} src - Document source path
275
+ * @param {string} type - Document type (pdf, markdown, image)
276
+ */
277
+ function _openInLightbox(src) {
278
+ openLightbox(src, '');
279
+ }
280
+
281
+ /**
282
+ * Load and render markdown thumbnails (deferred until gallery is expanded).
283
+ */
284
+ async function _loadMarkdownThumbnails() {
285
+ const mdThumbs = galleryContainer.querySelectorAll('[data-md-src]');
286
+ for (const thumb of mdThumbs) {
287
+ const src = thumb.dataset.mdSrc;
288
+ if (thumb.dataset.loaded) continue;
289
+ thumb.dataset.loaded = 'true';
290
+
291
+ try {
292
+ const response = await fetch(src);
293
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
294
+ const text = await response.text();
295
+
296
+ // Simple markdown-to-HTML for thumbnail preview (first ~500 chars)
297
+ const preview = _simpleMarkdownToHtml(text.slice(0, 500));
298
+ const content = thumb.querySelector('.sidebar-gallery-thumb-md-content');
299
+ if (content) {
300
+ content.innerHTML = preview;
301
+ }
302
+ } catch (_error) {
303
+ const content = thumb.querySelector('.sidebar-gallery-thumb-md-content');
304
+ if (content) {
305
+ content.innerHTML = '<p style=\'opacity: 0.5; font-style: italic;\'>Preview unavailable</p>';
306
+ }
307
+ }
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Simple markdown to HTML converter for thumbnail previews.
313
+ * Only handles basic elements (headings, paragraphs, lists).
314
+ * @param {string} md - Raw markdown text
315
+ * @returns {string} HTML string
316
+ */
317
+ function _simpleMarkdownToHtml(md) {
318
+ return md
319
+ .split('\n')
320
+ .map(line => {
321
+ const trimmed = line.trim();
322
+ if (!trimmed) return '';
323
+ if (trimmed.startsWith('# ')) return `<h1>${trimmed.slice(2)}</h1>`;
324
+ if (trimmed.startsWith('## ')) return `<h2>${trimmed.slice(3)}</h2>`;
325
+ if (trimmed.startsWith('### ')) return `<h2>${trimmed.slice(4)}</h2>`;
326
+ if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) return `<li>${trimmed.slice(2)}</li>`;
327
+ if (/^\d+\.\s/.test(trimmed)) return `<li>${trimmed.replace(/^\d+\.\s/, '')}</li>`;
328
+ return `<p>${trimmed}</p>`;
329
+ })
330
+ .join('');
331
+ }
332
+
333
+ /**
334
+ * Format a filename for display (remove extension, replace separators).
335
+ * @param {string} src - File path
336
+ * @returns {string} Formatted display name
337
+ */
338
+ function _formatFilename(src) {
339
+ const name = src.split('/').pop();
340
+ return name
341
+ .replace(/\.[^.]+$/, '') // Remove extension
342
+ .replace(/[_-]/g, ' ') // Replace separators with spaces
343
+ .replace(/\b\w/g, c => c.toUpperCase()); // Title case
344
+ }
345
+
346
+ /**
347
+ * Create a URL-safe slug from a file path.
348
+ * @param {string} src - File path
349
+ * @returns {string} Slugified string
350
+ */
351
+ function _slugify(src) {
352
+ return src
353
+ .replace(/[^a-zA-Z0-9]/g, '-')
354
+ .replace(/-+/g, '-')
355
+ .replace(/^-|-$/g, '')
356
+ .toLowerCase();
357
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @file navigation-helpers.js
3
+ * @description Pure utility functions for navigation logic.
4
+ * These functions have no side effects and don't depend on module state.
5
+ * @author Seth
6
+ * @version 1.0.0
7
+ */
8
+
9
+ import { courseConfig } from '../../../course/course-config.js';
10
+
11
+ /**
12
+ * Checks if gating should be bypassed.
13
+ *
14
+ * Bypass conditions (any of):
15
+ * 1. Development mode + disableGating config = true
16
+ * 2. URL parameter ?skipGating=true (for static preview exports)
17
+ * 3. Global flag window.__SCORM_PREVIEW_SKIP_GATING (set by stub player)
18
+ *
19
+ * @returns {boolean} True if gating should be bypassed
20
+ */
21
+ export function shouldBypassGating() {
22
+ // Test override: force gating on regardless of other flags
23
+ if (window.__FORCE_GATING === true) return false;
24
+
25
+ // Check URL parameter (works in any mode - for preview exports)
26
+ const urlParams = new URLSearchParams(window.location.search);
27
+ if (urlParams.get('skipGating') === 'true') {
28
+ return true;
29
+ }
30
+
31
+ // Check global flag (set by stub LMS player)
32
+ if (window.__SCORM_PREVIEW_SKIP_GATING === true) {
33
+ return true;
34
+ }
35
+
36
+ // Check dev mode config
37
+ const buildMode = import.meta?.env?.MODE;
38
+ const isProductionBuild = buildMode === 'production';
39
+ const devGatingDisabled = courseConfig.environment?.development?.disableGating === true;
40
+ return !isProductionBuild && devGatingDisabled;
41
+ }
42
+
43
+ /**
44
+ * Checks if engagement requirements should be bypassed in development mode.
45
+ * Semantically clearer alias for shouldBypassGating.
46
+ * @returns {boolean} True if engagement checks should be bypassed
47
+ */
48
+ export function shouldBypassEngagement() {
49
+ return shouldBypassGating();
50
+ }
51
+
52
+ /**
53
+ * Evaluates a single gating condition against current course state.
54
+ * This is a pure function that takes all dependencies as parameters.
55
+ *
56
+ * @param {object} condition - The condition to evaluate
57
+ * @param {object} stateManager - StateManager instance for reading course state
58
+ * @param {Map} assessmentConfigs - Map of assessment configurations
59
+ * @returns {boolean} True if the condition is met
60
+ * @throws {Error} If condition type is unknown
61
+ */
62
+ export function evaluateGatingCondition(condition, stateManager, assessmentConfigs) {
63
+ if (!condition || typeof condition !== 'object') {
64
+ return false;
65
+ }
66
+
67
+ switch (condition.type) {
68
+ case 'objectiveStatus': {
69
+ const objective = stateManager.getDomainState('objectives')?.[condition.objectiveId];
70
+ if (!objective) return false;
71
+
72
+ // Check completion_status if specified
73
+ if (condition.completion_status !== undefined) {
74
+ return objective.completion_status === condition.completion_status;
75
+ }
76
+
77
+ // Check success_status if specified
78
+ if (condition.success_status !== undefined) {
79
+ return objective.success_status === condition.success_status;
80
+ }
81
+
82
+ // No valid field specified
83
+ return false;
84
+ }
85
+
86
+ case 'assessmentStatus': {
87
+ // Read from the per-assessment domain (e.g., 'assessment_final-exam')
88
+ const assessmentDomain = stateManager.getDomainState(`assessment_${condition.assessmentId}`);
89
+ if (!assessmentDomain) return false;
90
+
91
+ const summary = assessmentDomain.summary;
92
+ if (!summary || !summary.lastResults) return false;
93
+
94
+ const passed = summary.lastResults.passed;
95
+
96
+ switch (condition.requires) {
97
+ case 'completed':
98
+ return summary.submitted === true;
99
+ case 'passed':
100
+ return passed === true;
101
+ case 'failed':
102
+ return summary.submitted === true && passed === false;
103
+ default:
104
+ return false;
105
+ }
106
+ }
107
+
108
+ case 'assessmentAttempts': {
109
+ // Read from the per-assessment domain
110
+ const assessmentDomain = stateManager.getDomainState(`assessment_${condition.assessmentId}`);
111
+ if (!assessmentDomain) return false;
112
+
113
+ const summary = assessmentDomain.summary;
114
+ const attempts = summary?.attempts || 0;
115
+
116
+ if (condition.min !== undefined && attempts < condition.min) return false;
117
+ if (condition.max !== undefined && attempts > condition.max) return false;
118
+ return true;
119
+ }
120
+
121
+ case 'assessmentConfig': {
122
+ const config = assessmentConfigs.get(condition.assessmentId);
123
+ if (!config) return false;
124
+
125
+ // Helper to get nested property value
126
+ const getPropertyValue = (obj, path) => path.split('.').reduce((o, k) => (o || {})[k], obj);
127
+ const value = getPropertyValue(config, condition.property);
128
+
129
+ if (value === undefined) return false;
130
+
131
+ if (condition.equals !== undefined) {
132
+ return value === condition.equals;
133
+ }
134
+ if (condition.greaterThan !== undefined) {
135
+ return typeof value === 'number' && value > condition.greaterThan;
136
+ }
137
+ if (condition.lessThan !== undefined) {
138
+ return typeof value === 'number' && value < condition.lessThan;
139
+ }
140
+ // If just checking for property existence
141
+ return true;
142
+ }
143
+
144
+ case 'stateFlag': {
145
+ const flags = stateManager.getDomainState('flags');
146
+ const value = flags?.[condition.key];
147
+ if (condition.equals !== undefined) {
148
+ return value === condition.equals;
149
+ }
150
+ if (condition.exists !== undefined) {
151
+ return condition.exists ? value !== undefined : value === undefined;
152
+ }
153
+ return !!value;
154
+ }
155
+
156
+ case 'timeOnSlide': {
157
+ const sessionData = stateManager.getDomainState('sessionData');
158
+ const slideDurations = sessionData?.slideDurations || {};
159
+ const slideDurationMs = slideDurations[condition.slideId] || 0;
160
+ const totalSeconds = slideDurationMs / 1000;
161
+ return totalSeconds >= (condition.minSeconds || 0);
162
+ }
163
+
164
+ case 'custom': {
165
+ // Custom conditions can use a function or reference custom state
166
+ if (typeof condition.evaluate === 'function') {
167
+ return condition.evaluate(stateManager);
168
+ }
169
+ return false;
170
+ }
171
+
172
+ default:
173
+ throw new Error(`Unknown gating condition type: ${condition.type}. Valid types: objectiveStatus, assessmentStatus, assessmentAttempts, assessmentConfig, stateFlag, timeOnSlide, custom.`);
174
+ }
175
+ }