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,652 @@
1
+ /**
2
+ * @file lightbox.js
3
+ * @description Declarative lightbox component for click-to-enlarge media viewing.
4
+ * Supports images, videos (native, YouTube, Vimeo), markdown files, and PDFs.
5
+ *
6
+ * Usage:
7
+ * <!-- Single image lightbox -->
8
+ * <a href="full-size.jpg" data-component="lightbox">
9
+ * <img src="thumbnail.jpg" alt="Description">
10
+ * </a>
11
+ *
12
+ * <!-- Video lightbox (YouTube, Vimeo, or native) -->
13
+ * <a href="https://youtu.be/dQw4w9WgXcQ" data-component="lightbox">
14
+ * <img src="video-thumbnail.jpg" alt="Watch Video">
15
+ * </a>
16
+ * <a href="video/demo.mp4" data-component="lightbox">
17
+ * <img src="video-poster.jpg" alt="Demo Video">
18
+ * </a>
19
+ *
20
+ * <!-- Gallery group with prev/next navigation -->
21
+ * <a href="img1.jpg" data-lightbox-gallery="my-gallery">
22
+ * <img src="thumb1.jpg" alt="Image 1">
23
+ * </a>
24
+ * <a href="img2.jpg" data-lightbox-gallery="my-gallery">
25
+ * <img src="thumb2.jpg" alt="Image 2">
26
+ * </a>
27
+ *
28
+ * Attributes:
29
+ * - href: Full-size media URL (for <a> elements)
30
+ * - data-lightbox-src: Full-size media URL (alternative to href)
31
+ * - data-lightbox-gallery: Gallery group ID for prev/next navigation
32
+ * - data-lightbox-caption: Caption text (falls back to alt text)
33
+ *
34
+ * Keyboard:
35
+ * - ESC: Close lightbox
36
+ * - Arrow Left/Right: Navigate gallery (if multiple items)
37
+ */
38
+
39
+ export const schema = {
40
+ type: 'lightbox',
41
+ description: 'Click-to-enlarge media viewer for images, videos, markdown, PDFs',
42
+ example: `<div style="display: flex; gap: 12px;">
43
+ <a href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='300' fill='%23dbeafe'%3E%3Crect width='400' height='300' rx='8'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%233b82f6' font-family='system-ui' font-size='16'%3EPhoto 1%3C/text%3E%3C/svg%3E" data-component="lightbox" data-lightbox-gallery="demo" data-lightbox-caption="First image" style="cursor: zoom-in;">
44
+ <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='80' fill='%23dbeafe'%3E%3Crect width='120' height='80' rx='4'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%233b82f6' font-family='system-ui' font-size='11'%3EPhoto 1%3C/text%3E%3C/svg%3E" alt="Photo 1">
45
+ </a>
46
+ <a href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='300' fill='%23dcfce7'%3E%3Crect width='400' height='300' rx='8'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%2322c55e' font-family='system-ui' font-size='16'%3EPhoto 2%3C/text%3E%3C/svg%3E" data-component="lightbox" data-lightbox-gallery="demo" data-lightbox-caption="Second image" style="cursor: zoom-in;">
47
+ <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='80' fill='%23dcfce7'%3E%3Crect width='120' height='80' rx='4'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%2322c55e' font-family='system-ui' font-size='11'%3EPhoto 2%3C/text%3E%3C/svg%3E" alt="Photo 2">
48
+ </a>
49
+ <a href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='300' fill='%23fef3c7'%3E%3Crect width='400' height='300' rx='8'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%23f59e0b' font-family='system-ui' font-size='16'%3EPhoto 3%3C/text%3E%3C/svg%3E" data-component="lightbox" data-lightbox-gallery="demo" data-lightbox-caption="Third image" style="cursor: zoom-in;">
50
+ <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='80' fill='%23fef3c7'%3E%3Crect width='120' height='80' rx='4'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%23f59e0b' font-family='system-ui' font-size='11'%3EPhoto 3%3C/text%3E%3C/svg%3E" alt="Photo 3">
51
+ </a>
52
+ </div>`,
53
+ properties: {
54
+ gallery: { type: 'string', dataAttribute: 'data-lightbox-gallery' },
55
+ caption: { type: 'string', dataAttribute: 'data-lightbox-caption' },
56
+ src: { type: 'string', dataAttribute: 'data-lightbox-src' },
57
+ thumbnail: { type: 'string', dataAttribute: 'data-lightbox-thumbnail' }
58
+ },
59
+ structure: {
60
+ container: '[data-component="lightbox"]',
61
+ children: {}
62
+ }
63
+ };
64
+
65
+ export const metadata = {
66
+ category: 'ui-component',
67
+ cssFile: 'components/lightbox.css',
68
+ engagementTracking: 'viewAllLightbox',
69
+ emitsEvents: ['lightbox:opened', 'lightbox:closed']
70
+ };
71
+
72
+ import { eventBus } from '../../core/event-bus.js';
73
+ import { iconManager } from '../../utilities/icons.js';
74
+ import { logger } from '../../utilities/logger.js';
75
+ import { escapeHTML } from '../../utilities/utilities.js';
76
+ import { fetchAndRenderMarkdown } from '../../utilities/markdown-renderer.js';
77
+ import engagementManager from '../../engagement/engagement-manager.js';
78
+
79
+ // Lightbox state
80
+ let lightboxElement = null;
81
+ let currentIndex = 0;
82
+ let currentGallery = [];
83
+ let isOpen = false;
84
+ let currentMediaType = 'image'; // 'image' | 'video' | 'markdown' | 'pdf'
85
+
86
+ /**
87
+ * Resolves asset paths relative to course directory.
88
+ * Follows same pattern as embed-frame.js for consistency.
89
+ * @param {string} src - The source path
90
+ * @returns {string} Resolved path
91
+ */
92
+ function _resolvePath(src) {
93
+ if (!src) return src;
94
+ // Already absolute URL or protocol-relative
95
+ if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('//')) {
96
+ return src;
97
+ }
98
+ // Already has leading slash (absolute path from root)
99
+ if (src.startsWith('/')) {
100
+ return src;
101
+ }
102
+ // Already uses ./ or ../ relative paths
103
+ if (src.startsWith('./') || src.startsWith('../')) {
104
+ return src;
105
+ }
106
+ // Otherwise, assume relative to course/ directory
107
+ return `./course/${src}`;
108
+ }
109
+
110
+ /**
111
+ * Get the source URL from a trigger element with path resolution.
112
+ * Uses raw attribute to avoid browser pre-resolution, then applies path resolution.
113
+ * @param {HTMLElement} trigger
114
+ * @returns {string} Resolved source URL
115
+ */
116
+ function getTriggerSrc(trigger) {
117
+ const rawSrc = trigger.getAttribute('href') || trigger.dataset.lightboxSrc;
118
+ return _resolvePath(rawSrc);
119
+ }
120
+
121
+ // ============================================================================
122
+ // Media URL Detection
123
+ // ============================================================================
124
+
125
+ /**
126
+ * Detects if URL is a PDF file.
127
+ * @param {string} url
128
+ * @returns {{ type: 'pdf' } | null}
129
+ */
130
+ function detectPDF(url) {
131
+ if (!url) return null;
132
+ if (/\.pdf($|\?|#)/i.test(url)) return { type: 'pdf' };
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Detects if URL is a markdown file.
138
+ * @param {string} url
139
+ * @returns {{ type: 'markdown' } | null}
140
+ */
141
+ function detectMarkdown(url) {
142
+ if (!url) return null;
143
+ if (/\.md($|\?|#)/i.test(url)) return { type: 'markdown' };
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Detects if a URL is a YouTube video and extracts the video ID.
149
+ * @param {string} url
150
+ * @returns {{ type: 'youtube', id: string } | null}
151
+ */
152
+ function detectYouTube(url) {
153
+ if (!url) return null;
154
+ // youtube.com/watch?v=VIDEO_ID
155
+ const watchMatch = url.match(/(?:youtube\.com\/watch\?.*v=)([a-zA-Z0-9_-]{11})/);
156
+ if (watchMatch) return { type: 'youtube', id: watchMatch[1] };
157
+ // youtu.be/VIDEO_ID
158
+ const shortMatch = url.match(/(?:youtu\.be\/)([a-zA-Z0-9_-]{11})/);
159
+ if (shortMatch) return { type: 'youtube', id: shortMatch[1] };
160
+ // youtube.com/embed/VIDEO_ID
161
+ const embedMatch = url.match(/(?:youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/);
162
+ if (embedMatch) return { type: 'youtube', id: embedMatch[1] };
163
+ return null;
164
+ }
165
+
166
+ /**
167
+ * Detects if a URL is a Vimeo video and extracts the video ID.
168
+ * @param {string} url
169
+ * @returns {{ type: 'vimeo', id: string } | null}
170
+ */
171
+ function detectVimeo(url) {
172
+ if (!url) return null;
173
+ const match = url.match(/(?:vimeo\.com\/)([\d]+)/);
174
+ if (match) return { type: 'vimeo', id: match[1] };
175
+ return null;
176
+ }
177
+
178
+ /**
179
+ * Detects if URL is a native video file.
180
+ * @param {string} url
181
+ * @returns {{ type: 'native' } | null}
182
+ */
183
+ function detectNativeVideo(url) {
184
+ if (!url) return null;
185
+ const videoExtensions = /\.(mp4|webm|ogg|mov|m4v)($|\?)/i;
186
+ if (videoExtensions.test(url)) return { type: 'native' };
187
+ return null;
188
+ }
189
+
190
+ /**
191
+ * Detects if URL is any type of video.
192
+ * @param {string} url
193
+ * @returns {{ type: 'youtube' | 'vimeo' | 'native', id?: string } | null}
194
+ */
195
+ function detectVideo(url) {
196
+ return detectYouTube(url) || detectVimeo(url) || detectNativeVideo(url);
197
+ }
198
+
199
+ /**
200
+ * Initialize a single lightbox trigger element.
201
+ * Called by the component catalog for each [data-component="lightbox"] element.
202
+ * @param {HTMLElement} element - The trigger element
203
+ */
204
+ export function init(element) {
205
+ // Skip if already initialized
206
+ if (element.dataset.lightboxInitialized) return;
207
+ element.dataset.lightboxInitialized = 'true';
208
+
209
+ // Lazy-create the shared lightbox overlay
210
+ if (!lightboxElement) {
211
+ createLightboxElement();
212
+ }
213
+
214
+ element.addEventListener('click', (e) => {
215
+ e.preventDefault();
216
+ openFromTrigger(element);
217
+ });
218
+
219
+ const src = getTriggerSrc(element);
220
+ const mediaType = getMediaType(src);
221
+ const customThumbnail = element.dataset.lightboxThumbnail;
222
+ const subtitle = element.dataset.lightboxSubtitle;
223
+
224
+ // Check for custom thumbnail override (works for any media type)
225
+ if (customThumbnail) {
226
+ renderCustomThumbnail(element, customThumbnail);
227
+ } else if (mediaType === 'markdown') {
228
+ // For markdown files, render a thumbnail preview
229
+ renderMarkdownThumbnail(element, src);
230
+ } else if (mediaType === 'pdf') {
231
+ // For PDF files, render a styled placeholder
232
+ renderPdfThumbnail(element, src);
233
+ } else {
234
+ // Add visual indicator if not already styled
235
+ element.style.cursor = 'zoom-in';
236
+ }
237
+
238
+ // Add subtitle if present - wrap trigger in a container
239
+ if (subtitle) {
240
+ wrapTriggerWithSubtitle(element, subtitle);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Render a custom thumbnail image inside a trigger element.
246
+ * @param {HTMLElement} trigger - The trigger element
247
+ * @param {string} thumbnailSrc - URL of the custom thumbnail image
248
+ */
249
+ function renderCustomThumbnail(trigger, thumbnailSrc) {
250
+ trigger.classList.add('lightbox-custom-thumbnail');
251
+ trigger.style.cursor = 'zoom-in';
252
+ const resolvedSrc = _resolvePath(thumbnailSrc);
253
+ trigger.innerHTML = `<img src="${resolvedSrc}" alt="" class="lightbox-custom-thumbnail-img">`;
254
+ }
255
+
256
+ /**
257
+ * Render a markdown thumbnail preview inside a trigger element.
258
+ * @param {HTMLElement} trigger - The trigger element
259
+ * @param {string} src - URL of the markdown file
260
+ */
261
+ async function renderMarkdownThumbnail(trigger, src) {
262
+ // Add thumbnail container class
263
+ trigger.classList.add('lightbox-md-thumbnail');
264
+
265
+ // Show loading state
266
+ trigger.innerHTML = `
267
+ <div class="lightbox-md-thumbnail-loading">
268
+ ${iconManager.getIcon('loader-2', { size: 'lg', class: 'icon-spin' })}
269
+ </div>
270
+ `;
271
+
272
+ try {
273
+ const html = await fetchAndRenderMarkdown(src);
274
+ trigger.innerHTML = `<div class="lightbox-md-thumbnail-content">${html}</div>`;
275
+ } catch (error) {
276
+ trigger.innerHTML = `
277
+ <div class="lightbox-md-thumbnail-danger">
278
+ ${iconManager.getIcon('file-text', { size: 'xl' })}
279
+ <span>Failed to load</span>
280
+ </div>
281
+ `;
282
+ logger.error('Failed to render markdown thumbnail', { src, error: error.message });
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Render a PDF placeholder thumbnail inside a trigger element.
288
+ * @param {HTMLElement} trigger - The trigger element
289
+ * @param {string} src - URL of the PDF file
290
+ */
291
+ function renderPdfThumbnail(trigger, src) {
292
+ trigger.classList.add('lightbox-pdf-thumbnail');
293
+
294
+ // Extract filename from path
295
+ const filename = src.split('/').pop().split('?')[0] || 'Document.pdf';
296
+
297
+ trigger.innerHTML = `
298
+ <div class="lightbox-pdf-thumbnail-content">
299
+ ${iconManager.getIcon('file-text', { size: '2xl', class: 'lightbox-pdf-thumbnail-icon' })}
300
+ <span class="lightbox-pdf-thumbnail-badge">PDF</span>
301
+ <span class="lightbox-pdf-thumbnail-filename">${filename}</span>
302
+ </div>
303
+ `;
304
+ }
305
+
306
+ /**
307
+ * Wrap a trigger element with a container that includes a subtitle below.
308
+ * @param {HTMLElement} trigger - The trigger element
309
+ * @param {string} subtitle - The subtitle text
310
+ */
311
+ function wrapTriggerWithSubtitle(trigger, subtitle) {
312
+ // Create wrapper container
313
+ const wrapper = document.createElement('div');
314
+ wrapper.className = 'lightbox-thumbnail-wrapper';
315
+
316
+ // Insert wrapper before trigger, then move trigger into wrapper
317
+ trigger.parentNode.insertBefore(wrapper, trigger);
318
+ wrapper.appendChild(trigger);
319
+
320
+ // Add subtitle element
321
+ const subtitleEl = document.createElement('span');
322
+ subtitleEl.className = 'lightbox-thumbnail-subtitle';
323
+ subtitleEl.textContent = subtitle;
324
+ wrapper.appendChild(subtitleEl);
325
+ }
326
+
327
+ /**
328
+ * Create the lightbox DOM element.
329
+ */
330
+ function createLightboxElement() {
331
+ // Find the course container for proper z-index stacking
332
+ const courseContainer = document.querySelector('.course-container') || document.body;
333
+
334
+ lightboxElement = document.createElement('div');
335
+ lightboxElement.className = 'lightbox';
336
+ lightboxElement.innerHTML = `
337
+ <div class="lightbox-backdrop"></div>
338
+ <div class="lightbox-content">
339
+ <button class="lightbox-close" aria-label="Close lightbox">
340
+ ${iconManager.getIcon('x', { size: 'lg' })}
341
+ </button>
342
+ <div class="lightbox-media-wrapper">
343
+ <div class="lightbox-loading">
344
+ ${iconManager.getIcon('loader-2', { size: '2xl', class: 'icon-spin' })}
345
+ </div>
346
+ <!-- Image container -->
347
+ <img class="lightbox-image" src="" alt="">
348
+ <!-- Video container (shown when media is video) -->
349
+ <div class="lightbox-video"></div>
350
+ <!-- Markdown container (shown when media is markdown) -->
351
+ <div class="lightbox-markdown"></div>
352
+ <!-- PDF container (shown when media is pdf) -->
353
+ <iframe class="lightbox-pdf" src="" title="PDF document"></iframe>
354
+ </div>
355
+ <div class="lightbox-caption"></div>
356
+ <button class="lightbox-nav lightbox-prev" aria-label="Previous">
357
+ ${iconManager.getIcon('chevron-left', { size: 'xl' })}
358
+ </button>
359
+ <button class="lightbox-nav lightbox-next" aria-label="Next">
360
+ ${iconManager.getIcon('chevron-right', { size: 'xl' })}
361
+ </button>
362
+ </div>
363
+ `;
364
+
365
+ // Event listeners
366
+ lightboxElement.querySelector('.lightbox-backdrop').addEventListener('click', close);
367
+ lightboxElement.querySelector('.lightbox-close').addEventListener('click', close);
368
+ lightboxElement.querySelector('.lightbox-prev').addEventListener('click', () => navigate(-1));
369
+ lightboxElement.querySelector('.lightbox-next').addEventListener('click', () => navigate(1));
370
+
371
+ // Handle hash anchor links inside markdown container (TOC links)
372
+ const markdownContainer = lightboxElement.querySelector('.lightbox-markdown');
373
+ markdownContainer.addEventListener('click', (e) => {
374
+ const anchor = e.target.closest('a');
375
+ if (!anchor) return;
376
+
377
+ const href = anchor.getAttribute('href');
378
+ if (href && href.startsWith('#')) {
379
+ e.preventDefault();
380
+ const targetId = href.slice(1);
381
+ const targetElement = markdownContainer.querySelector(`#${CSS.escape(targetId)}`);
382
+ if (targetElement) {
383
+ targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
384
+ }
385
+ }
386
+ });
387
+
388
+ // Keyboard navigation
389
+ document.addEventListener('keydown', handleKeydown);
390
+
391
+
392
+ courseContainer.appendChild(lightboxElement);
393
+ }
394
+
395
+ /**
396
+ * Handle keyboard events.
397
+ * @param {KeyboardEvent} e
398
+ */
399
+ function handleKeydown(e) {
400
+ if (!isOpen) return;
401
+
402
+ switch (e.key) {
403
+ case 'Escape':
404
+ close();
405
+ break;
406
+ case 'ArrowLeft':
407
+ navigate(-1);
408
+ break;
409
+ case 'ArrowRight':
410
+ navigate(1);
411
+ break;
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Open lightbox from a trigger element.
417
+ * @param {HTMLElement} trigger
418
+ */
419
+ function openFromTrigger(trigger) {
420
+ const src = getTriggerSrc(trigger);
421
+ const caption = trigger.dataset.lightboxCaption ||
422
+ trigger.querySelector('img')?.alt ||
423
+ trigger.getAttribute('aria-label') || '';
424
+ const galleryId = trigger.dataset.lightboxGallery;
425
+ const lightboxId = trigger.id || trigger.dataset.lightboxId;
426
+
427
+ // Build gallery array if part of a group
428
+ if (galleryId) {
429
+ const galleryTriggers = document.querySelectorAll(`[data-lightbox-gallery="${galleryId}"]`);
430
+ currentGallery = Array.from(galleryTriggers).map(t => {
431
+ const itemSrc = getTriggerSrc(t);
432
+ return {
433
+ src: itemSrc,
434
+ caption: t.dataset.lightboxCaption || t.querySelector('img')?.alt || '',
435
+ mediaType: getMediaType(itemSrc)
436
+ };
437
+ });
438
+ currentIndex = Array.from(galleryTriggers).indexOf(trigger);
439
+ } else {
440
+ currentGallery = [{ src, caption, mediaType: getMediaType(src) }];
441
+ currentIndex = 0;
442
+ }
443
+
444
+ // Track lightbox view for engagement
445
+ const slideId = trigger.closest?.('[data-slide-id]')?.dataset?.slideId ||
446
+ document.querySelector('.slide.active')?.dataset?.slideId ||
447
+ null;
448
+ if (slideId && lightboxId) {
449
+ engagementManager.trackLightboxView(slideId, lightboxId);
450
+ }
451
+
452
+ open(src, caption);
453
+ }
454
+
455
+ /**
456
+ * Determine media type from URL.
457
+ * @param {string} src
458
+ * @returns {'markdown' | 'video' | 'image'}
459
+ */
460
+ function getMediaType(src) {
461
+ if (detectPDF(src)) return 'pdf';
462
+ if (detectMarkdown(src)) return 'markdown';
463
+ if (detectVideo(src)) return 'video';
464
+ return 'image';
465
+ }
466
+
467
+ /**
468
+ * Open the lightbox with an image, video, or markdown file.
469
+ * @param {string} src - Media source URL
470
+ * @param {string} [caption=''] - Optional caption
471
+ */
472
+ export async function open(src, caption = '') {
473
+ if (!lightboxElement) {
474
+ createLightboxElement();
475
+ }
476
+
477
+ const img = lightboxElement.querySelector('.lightbox-image');
478
+ const videoContainer = lightboxElement.querySelector('.lightbox-video');
479
+ const markdownContainer = lightboxElement.querySelector('.lightbox-markdown');
480
+ const pdfContainer = lightboxElement.querySelector('.lightbox-pdf');
481
+ const captionEl = lightboxElement.querySelector('.lightbox-caption');
482
+ const loading = lightboxElement.querySelector('.lightbox-loading');
483
+ const prevBtn = lightboxElement.querySelector('.lightbox-prev');
484
+ const nextBtn = lightboxElement.querySelector('.lightbox-next');
485
+
486
+ // Detect media type
487
+ currentMediaType = getMediaType(src);
488
+
489
+ // Clear previous content and hide all containers
490
+ videoContainer.innerHTML = '';
491
+ videoContainer.style.display = 'none';
492
+ markdownContainer.innerHTML = '';
493
+ markdownContainer.scrollTop = 0; // Reset scroll position before loading new content
494
+ markdownContainer.style.display = 'none';
495
+ pdfContainer.src = '';
496
+ pdfContainer.style.display = 'none';
497
+ img.style.display = 'none';
498
+ img.src = '';
499
+
500
+ if (currentMediaType === 'pdf') {
501
+ // Render PDF in iframe
502
+ loading.style.display = 'none';
503
+ pdfContainer.style.display = 'block';
504
+ pdfContainer.src = src;
505
+ logger.debug('Lightbox opened with PDF', { src });
506
+ } else if (currentMediaType === 'markdown') {
507
+ // Render markdown
508
+ loading.style.display = 'flex';
509
+ markdownContainer.style.display = 'block';
510
+ try {
511
+ const html = await fetchAndRenderMarkdown(src);
512
+ markdownContainer.innerHTML = html;
513
+ // Reset scroll position after DOM renders
514
+ requestAnimationFrame(() => {
515
+ markdownContainer.scrollTop = 0;
516
+ });
517
+ loading.style.display = 'none';
518
+ logger.debug('Lightbox opened with markdown', { src });
519
+ } catch (error) {
520
+ loading.style.display = 'none';
521
+ markdownContainer.innerHTML = `<div class="lightbox-danger">Failed to load markdown: ${escapeHTML(error.message)}</div>`;
522
+ logger.error('Failed to load lightbox markdown', { src, error: error.message });
523
+ }
524
+ } else if (currentMediaType === 'video') {
525
+ // Render video
526
+ const videoInfo = detectVideo(src);
527
+ loading.style.display = 'none';
528
+ videoContainer.style.display = 'block';
529
+ videoContainer.innerHTML = renderVideo(src, videoInfo);
530
+ logger.debug('Lightbox opened with video', { src, type: videoInfo.type });
531
+ } else {
532
+ // Render image (original behavior)
533
+ img.style.display = 'block';
534
+ loading.style.display = 'flex';
535
+ img.style.opacity = '0';
536
+
537
+ img.onload = () => {
538
+ loading.style.display = 'none';
539
+ img.style.opacity = '1';
540
+ };
541
+ img.onerror = () => {
542
+ loading.style.display = 'none';
543
+ logger.error('Failed to load lightbox image', { src });
544
+ };
545
+ img.src = src;
546
+ img.alt = caption || 'Enlarged image';
547
+ }
548
+
549
+ // Caption
550
+ captionEl.textContent = caption;
551
+ captionEl.style.display = caption ? 'block' : 'none';
552
+
553
+ // Gallery navigation visibility
554
+ const isGallery = currentGallery.length > 1;
555
+ prevBtn.style.display = isGallery ? 'flex' : 'none';
556
+ nextBtn.style.display = isGallery ? 'flex' : 'none';
557
+
558
+ // Show lightbox
559
+ lightboxElement.classList.add('active');
560
+ isOpen = true;
561
+
562
+ // Trap focus
563
+ lightboxElement.querySelector('.lightbox-close').focus();
564
+
565
+ eventBus.emit('lightbox:opened', { src, caption, type: currentMediaType });
566
+ }
567
+
568
+ /**
569
+ * Render video HTML based on video type.
570
+ * @param {string} src - Video URL
571
+ * @param {{ type: 'youtube' | 'vimeo' | 'native', id?: string }} videoInfo
572
+ * @returns {string} HTML string
573
+ */
574
+ function renderVideo(src, videoInfo) {
575
+ if (videoInfo.type === 'youtube') {
576
+ return `
577
+ <iframe
578
+ class="lightbox-video-embed"
579
+ src="https://www.youtube.com/embed/${videoInfo.id}?autoplay=1&rel=0"
580
+ title="YouTube video"
581
+ frameborder="0"
582
+ allow="autoplay; fullscreen">
583
+ </iframe>
584
+ `;
585
+ }
586
+
587
+ if (videoInfo.type === 'vimeo') {
588
+ return `
589
+ <iframe
590
+ class="lightbox-video-embed"
591
+ src="https://player.vimeo.com/video/${videoInfo.id}?autoplay=1"
592
+ title="Vimeo video"
593
+ frameborder="0"
594
+ allow="autoplay; fullscreen">
595
+ </iframe>
596
+ `;
597
+ }
598
+
599
+ // Native video
600
+ return `
601
+ <video
602
+ class="lightbox-video-native"
603
+ src="${src}"
604
+ controls
605
+ autoplay>
606
+ Your browser does not support video playback.
607
+ </video>
608
+ `;
609
+ }
610
+
611
+ /**
612
+ * Close the lightbox.
613
+ */
614
+ export function close() {
615
+ if (!lightboxElement || !isOpen) return;
616
+
617
+ lightboxElement.classList.remove('active');
618
+ isOpen = false;
619
+
620
+ // Clear media to stop loading/playback
621
+ const img = lightboxElement.querySelector('.lightbox-image');
622
+ const videoContainer = lightboxElement.querySelector('.lightbox-video');
623
+ const markdownContainer = lightboxElement.querySelector('.lightbox-markdown');
624
+ const pdfContainer = lightboxElement.querySelector('.lightbox-pdf');
625
+ img.src = '';
626
+ videoContainer.innerHTML = ''; // Stops video/iframe playback
627
+ markdownContainer.innerHTML = '';
628
+ pdfContainer.src = ''; // Clear PDF iframe
629
+
630
+ eventBus.emit('lightbox:closed');
631
+ }
632
+
633
+ /**
634
+ * Navigate to next/previous image in gallery.
635
+ * @param {number} direction - 1 for next, -1 for previous
636
+ */
637
+ function navigate(direction) {
638
+ if (currentGallery.length <= 1) return;
639
+
640
+ currentIndex = (currentIndex + direction + currentGallery.length) % currentGallery.length;
641
+ const item = currentGallery[currentIndex];
642
+ open(item.src, item.caption);
643
+ }
644
+
645
+ /**
646
+ * Check if lightbox is currently open.
647
+ * @returns {boolean}
648
+ */
649
+ export function isVisible() {
650
+ return isOpen;
651
+ }
652
+