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,173 @@
1
+ /**
2
+ * stub-player/content-viewer.js - Content viewer panel component
3
+ *
4
+ * Generates the content viewer panel HTML and handles client-side interactions.
5
+ */
6
+
7
+ /**
8
+ * Generate content viewer panel HTML
9
+ * @param {Object} options
10
+ * @param {boolean} options.isLive - Whether this is live mode (shows refresh button)
11
+ * Note: Content is populated dynamically by JS, this just creates the container
12
+ */
13
+ export function generateContentViewer({ isLive }) {
14
+ return `
15
+ <div id="stub-player-content-panel">
16
+ <div id="stub-player-content-resize-handle"></div>
17
+ <div id="stub-player-content-panel-header">
18
+ <h3>📄 Course Review</h3>
19
+ <div class="header-buttons">
20
+ <label class="checkbox-label" title="Show narration text">
21
+ <input type="checkbox" id="stub-player-show-narration" checked> Narration
22
+ </label>
23
+ ${isLive ? '<button id="stub-player-content-refresh" title="Refresh content from source">🔄 Refresh</button>' : ''}
24
+ <button id="stub-player-content-print" title="Print content">🖨️ Print</button>
25
+ <button id="stub-player-content-new-window" title="Open in new window">↗ New Window</button>
26
+ <button id="stub-player-content-panel-close">Close</button>
27
+ </div>
28
+ </div>
29
+ <div id="stub-player-content-body">
30
+ <div class="content-loading">Loading content...</div>
31
+ </div>
32
+ </div>
33
+ `;
34
+ }
35
+
36
+ /**
37
+ * Initialize Content Viewer Handlers
38
+ */
39
+ export function createContentViewerHandlers(context) {
40
+ // isLive comes from context when passed by app.js (detected from window.location)
41
+
42
+ // Panel elements
43
+ const contentPanel = document.getElementById('stub-player-content-panel');
44
+ const contentBody = document.getElementById('stub-player-content-body');
45
+ const closeBtn = document.getElementById('stub-player-content-panel-close');
46
+ const refreshBtn = document.getElementById('stub-player-content-refresh');
47
+ const printBtn = document.getElementById('stub-player-content-print');
48
+ const newWindowBtn = document.getElementById('stub-player-content-new-window');
49
+ const showNarrationCheck = document.getElementById('stub-player-show-narration');
50
+ const resizeHandle = document.getElementById('stub-player-content-resize-handle');
51
+
52
+ // Close handler
53
+ if (closeBtn) {
54
+ closeBtn.addEventListener('click', () => {
55
+ contentPanel.classList.remove('visible');
56
+ });
57
+ }
58
+
59
+ // Refresh handler
60
+ if (refreshBtn) {
61
+ refreshBtn.addEventListener('click', () => {
62
+ loadContent();
63
+ });
64
+ }
65
+
66
+ // Print handler
67
+ if (printBtn) {
68
+ printBtn.addEventListener('click', () => {
69
+ const printContent = contentBody.innerHTML;
70
+ const printWin = window.open('', '_blank');
71
+ printWin.document.write(`
72
+ <html>
73
+ <head>
74
+ <title>Course Content Review</title>
75
+ <link rel="stylesheet" href="/__stub-player/styles.css">
76
+ <style>
77
+ body { padding: 20px; font-family: sans-serif; }
78
+ @media print {
79
+ button { display: none; }
80
+ }
81
+ </style>
82
+ </head>
83
+ <body>
84
+ ${printContent}
85
+ <script>
86
+ window.onload = function() { window.print(); window.close(); }
87
+ </script>
88
+ </body>
89
+ </html>
90
+ `);
91
+ printWin.document.close();
92
+ });
93
+ }
94
+
95
+ // New Window handler
96
+ if (newWindowBtn) {
97
+ newWindowBtn.addEventListener('click', () => {
98
+ window.open('/__content-view', '_blank');
99
+ });
100
+ }
101
+
102
+ // Narration toggle
103
+ if (showNarrationCheck) {
104
+ showNarrationCheck.addEventListener('change', (e) => {
105
+ if (contentBody) {
106
+ contentBody.classList.toggle('hide-narration', !e.target.checked);
107
+ }
108
+ });
109
+ }
110
+
111
+ // Resize Handler
112
+ if (resizeHandle) {
113
+ let startX, startWidth;
114
+
115
+ resizeHandle.addEventListener('mousedown', (e) => {
116
+ isResizing = true;
117
+ startX = e.clientX;
118
+ startWidth = parseInt(document.defaultView.getComputedStyle(contentPanel).width, 10);
119
+ document.documentElement.addEventListener('mousemove', doDrag, false);
120
+ document.documentElement.addEventListener('mouseup', stopDrag, false);
121
+ e.preventDefault();
122
+ });
123
+
124
+ function doDrag(e) {
125
+ const width = startWidth - (e.clientX - startX); // Dragging left increases width
126
+ if (width > 300 && width < window.innerWidth - 100) {
127
+ contentPanel.style.width = width + 'px';
128
+ }
129
+ }
130
+
131
+ function stopDrag() {
132
+ isResizing = false;
133
+ document.documentElement.removeEventListener('mousemove', doDrag, false);
134
+ document.documentElement.removeEventListener('mouseup', stopDrag, false);
135
+ }
136
+ }
137
+
138
+ // Core load function
139
+ async function loadContent() {
140
+ if (!contentBody) return;
141
+
142
+ // Use preloaded content if available (for static exports)
143
+ if (context.initialContent) {
144
+ contentBody.innerHTML = context.initialContent;
145
+ context.initialContent = null; // Clear so subsequent refreshes (if any) might fetch?
146
+ // Actually in static export refresh doesn't make sense unless refreshing from... what?
147
+ // If live, we probably prefer fetch.
148
+ return;
149
+ }
150
+
151
+ const showNarration = showNarrationCheck ? showNarrationCheck.checked : true;
152
+
153
+ contentBody.innerHTML = '<div class="content-loading">Loading content...</div>';
154
+
155
+ try {
156
+ const response = await fetch('/__content?includeNarration=' + showNarration);
157
+ if (response.ok) {
158
+ const html = await response.text();
159
+ contentBody.innerHTML = html;
160
+ // Re-apply narration visibility class
161
+ contentBody.classList.toggle('hide-narration', !showNarration);
162
+ } else {
163
+ contentBody.innerHTML = '<div class="content-error">Failed to load content</div>';
164
+ }
165
+ } catch (err) {
166
+ contentBody.innerHTML = '<div class="content-error">Error: ' + err.message + '</div>';
167
+ }
168
+ }
169
+
170
+ return {
171
+ loadContent
172
+ };
173
+ }
@@ -0,0 +1,420 @@
1
+ /**
2
+ * stub-player/debug-panel.js - Debug panel component
3
+ *
4
+ * Generates the debug panel HTML with tabs for state, API log, xAPI, and errors.
5
+ */
6
+
7
+ /**
8
+ * Generate debug panel HTML
9
+ */
10
+ export function generateDebugPanel() {
11
+ return `
12
+ <div id="stub-player-debug-panel">
13
+ <div id="stub-player-debug-panel-header">
14
+ <h3>LMS Debug <span id="stub-player-format-badge" class="format-badge"></span></h3>
15
+ <button id="stub-player-debug-panel-close">&times;</button>
16
+ </div>
17
+ <div id="stub-player-debug-tabs">
18
+ <button class="active" data-tab="state">State</button>
19
+ <button data-tab="api-log">API Log</button>
20
+ <button id="stub-player-xapi-tab" data-tab="xapi">xAPI <span id="stub-player-xapi-badge"></span></button>
21
+ <button data-tab="errors">Errors <span id="stub-player-error-badge"></span></button>
22
+ </div>
23
+ <div id="stub-player-debug-content">
24
+ <div id="tab-state">
25
+ <div class="debug-section" id="section-resume">
26
+ <div class="debug-section-header" onclick="this.parentElement.classList.toggle('collapsed')">
27
+ <span class="toggle-icon">▼</span>
28
+ <h4>Resume State</h4>
29
+ </div>
30
+ <div class="debug-section-content">
31
+ <table class="debug-table" id="stub-player-resume-data"></table>
32
+ </div>
33
+ </div>
34
+ <div class="debug-section" id="section-score">
35
+ <div class="debug-section-header" onclick="this.parentElement.classList.toggle('collapsed')">
36
+ <span class="toggle-icon">▼</span>
37
+ <h4>Score</h4>
38
+ </div>
39
+ <div class="debug-section-content">
40
+ <table class="debug-table" id="stub-player-score-data"></table>
41
+ </div>
42
+ </div>
43
+ <div class="debug-section collapsed" id="section-objectives">
44
+ <div class="debug-section-header" onclick="this.parentElement.classList.toggle('collapsed')">
45
+ <span class="toggle-icon">▼</span>
46
+ <h4>Objectives</h4>
47
+ <span class="count-badge" id="stub-player-objectives-count">0</span>
48
+ </div>
49
+ <div class="debug-section-content">
50
+ <div id="stub-player-objectives-data"></div>
51
+ </div>
52
+ </div>
53
+ <div class="debug-section collapsed" id="section-interactions">
54
+ <div class="debug-section-header" onclick="this.parentElement.classList.toggle('collapsed')">
55
+ <span class="toggle-icon">▼</span>
56
+ <h4>Interactions</h4>
57
+ <span class="count-badge" id="stub-player-interactions-count">0</span>
58
+ </div>
59
+ <div class="debug-section-content">
60
+ <div id="stub-player-interactions-data"></div>
61
+ </div>
62
+ </div>
63
+ <div class="debug-section" id="section-suspend">
64
+ <div class="debug-section-header" onclick="this.parentElement.classList.toggle('collapsed')">
65
+ <span class="toggle-icon">▼</span>
66
+ <h4>Suspend Data</h4>
67
+ </div>
68
+ <div class="debug-section-content">
69
+ <div class="suspend-info" id="stub-player-suspend-info"></div>
70
+ <pre id="stub-player-suspend-data" style="background:#252542;padding:8px;border-radius:4px;overflow-x:auto;font-size:10px;max-height:200px;overflow-y:auto;"></pre>
71
+ </div>
72
+ </div>
73
+ <div class="debug-section-actions">
74
+ <button id="stub-player-resume-test-btn" class="debug-action-btn" title="Simulate session end and test resume">🔄 Test Resume</button>
75
+ </div>
76
+ </div>
77
+ <div id="tab-api-log" style="display:none;">
78
+ <div id="stub-player-api-log"></div>
79
+ </div>
80
+ <div id="tab-xapi" style="display:none;">
81
+ <div id="stub-player-xapi-log">
82
+ <div id="stub-player-no-statements">No xAPI statements yet. Navigate through the course to see statements.</div>
83
+ </div>
84
+ </div>
85
+ <div id="tab-errors" style="display:none;">
86
+ <div id="stub-player-error-actions" style="display:none;"><button id="stub-player-copy-all-errors" class="debug-action-btn" title="Copy all errors to clipboard">📋 Copy All</button></div>
87
+ <div id="stub-player-error-log">
88
+ <div id="stub-player-no-errors">✓ No errors or warnings</div>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ `;
94
+ }
95
+
96
+ /**
97
+ * Initialize Client-Side Handlers
98
+ */
99
+ import { cmiData, apiLog, errorLog, xapiLog, saveState, logApiCall, logError, setUiCallbacks } from './lms-api.js';
100
+
101
+ function escapeHtmlInline(str) {
102
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
103
+ }
104
+
105
+ export function createDebugPanelHandlers() {
106
+ // Tab switching
107
+ const tabs = document.querySelectorAll('#stub-player-debug-tabs button');
108
+ tabs.forEach(tab => {
109
+ tab.addEventListener('click', () => {
110
+ tabs.forEach(t => t.classList.remove('active'));
111
+ tab.classList.add('active');
112
+
113
+ const target = tab.dataset.tab;
114
+ ['state', 'api-log', 'xapi', 'errors'].forEach(id => {
115
+ document.getElementById('tab-' + id).style.display = id === target ? 'block' : 'none';
116
+ });
117
+ });
118
+ });
119
+
120
+ // Event delegation for xAPI entry expansion
121
+ const xapiContainer = document.getElementById('stub-player-xapi-log');
122
+ if (xapiContainer) {
123
+ xapiContainer.addEventListener('click', function (e) {
124
+ const entry = e.target.closest('.xapi-entry');
125
+ if (entry) entry.classList.toggle('expanded');
126
+ });
127
+ }
128
+
129
+ // Register callbacks with LMS API to update UI on changes
130
+ setUiCallbacks({
131
+ onStateChange: (type, data) => {
132
+ if (type === 'format') updateFormatBadge(data);
133
+ else updateDebugDisplay();
134
+ },
135
+ onApiLog: updateApiLogDisplay,
136
+ onErrorLog: updateErrorDisplay,
137
+ onXapiLog: updateXapiDisplay
138
+ });
139
+
140
+ // Test Resume Button
141
+ document.getElementById('stub-player-resume-test-btn')?.addEventListener('click', () => {
142
+ // Ensure exit is set to suspend
143
+ if (!cmiData['cmi.exit'] || cmiData['cmi.exit'] === '') {
144
+ logError('Resume Warning', 'cmi.exit was empty - setting to "suspend".', 'Course should set cmi.exit before Terminate', true);
145
+ cmiData['cmi.exit'] = 'suspend';
146
+ }
147
+
148
+ // Clear session time and set entry to resume
149
+ cmiData['cmi.session_time'] = 'PT0H0M0S';
150
+ cmiData['cmi.entry'] = 'resume';
151
+
152
+ saveState();
153
+
154
+ logApiCall('LMS Commit', 'Session ended', JSON.stringify({
155
+ location: cmiData['cmi.location'],
156
+ completion: cmiData['cmi.completion_status'],
157
+ exit: cmiData['cmi.exit']
158
+ }));
159
+
160
+ // Reload page to simulate new session
161
+ setTimeout(() => {
162
+ const frame = document.getElementById('stub-player-course-frame');
163
+ frame.src = 'about:blank';
164
+ setTimeout(() => {
165
+ window.location.reload();
166
+ }, 100);
167
+ }, 200);
168
+ });
169
+
170
+ // Close button handler
171
+ document.getElementById('stub-player-debug-panel-close')?.addEventListener('click', () => {
172
+ document.getElementById('stub-player-debug-panel')?.classList.remove('visible');
173
+ });
174
+
175
+ // Initial render
176
+ updateDebugDisplay();
177
+ updateApiLogDisplay();
178
+ updateErrorDisplay();
179
+ updateXapiDisplay();
180
+ }
181
+
182
+ function updateFormatBadge(format) {
183
+ const badge = document.getElementById('stub-player-format-badge');
184
+ const xapiTab = document.getElementById('stub-player-xapi-tab');
185
+ if (badge) {
186
+ badge.className = 'format-badge' + (format === 'scorm12' ? ' scorm12' : format === 'cmi5' ? ' cmi5' : format === 'lti' ? ' lti' : '');
187
+ badge.textContent = { scorm2004: 'SCORM 2004', 'scorm12': 'SCORM 1.2', cmi5: 'cmi5', lti: 'LTI' }[format] || format;
188
+ }
189
+ if (xapiTab) {
190
+ xapiTab.classList.toggle('visible', format === 'cmi5');
191
+ }
192
+ }
193
+
194
+ function getValueClass(key, value) {
195
+ if (!value || value === '-') return 'value-empty';
196
+ if (key === 'cmi.entry') return value === 'resume' ? 'value-resume' : 'value-ab-initio';
197
+ if (key === 'cmi.completion_status') return value === 'completed' ? 'value-completed' : value === 'incomplete' ? 'value-incomplete' : 'value-unknown';
198
+ if (key === 'cmi.success_status') return value === 'passed' ? 'value-passed' : value === 'failed' ? 'value-failed' : 'value-unknown';
199
+ return '';
200
+ }
201
+
202
+ function formatBytes(bytes) {
203
+ if (bytes < 1024) return bytes + ' B';
204
+ return (bytes / 1024).toFixed(1) + ' KB';
205
+ }
206
+
207
+ function updateDebugDisplay() {
208
+ const resumeTable = document.getElementById('stub-player-resume-data');
209
+ const scoreTable = document.getElementById('stub-player-score-data');
210
+ const objectivesData = document.getElementById('stub-player-objectives-data');
211
+ const interactionsData = document.getElementById('stub-player-interactions-data');
212
+ const suspendPre = document.getElementById('stub-player-suspend-data');
213
+ const suspendInfo = document.getElementById('stub-player-suspend-info');
214
+
215
+ if (!resumeTable) return;
216
+
217
+ // Resume State section
218
+ const resumeKeys = [
219
+ { key: 'cmi.entry', label: 'entry' },
220
+ { key: 'cmi.exit', label: 'exit' },
221
+ { key: 'cmi.location', label: 'location' },
222
+ { key: 'cmi.completion_status', label: 'completion' },
223
+ { key: 'cmi.success_status', label: 'success' },
224
+ { key: 'cmi.session_time', label: 'session_time' },
225
+ { key: 'cmi.total_time', label: 'total_time' }
226
+ ];
227
+ resumeTable.innerHTML = resumeKeys.map(({ key, label }) => {
228
+ const value = cmiData[key] || '-';
229
+ const cls = getValueClass(key, value);
230
+ return '<tr><td>' + label + '</td><td class="' + cls + '">' + (value || '<span class="value-empty">(empty)</span>') + '</td></tr>';
231
+ }).join('');
232
+
233
+ // Score section
234
+ const scoreKeys = [
235
+ { key: 'cmi.score.raw', label: 'raw' },
236
+ { key: 'cmi.score.scaled', label: 'scaled' },
237
+ { key: 'cmi.score.min', label: 'min' },
238
+ { key: 'cmi.score.max', label: 'max' }
239
+ ];
240
+ scoreTable.innerHTML = scoreKeys.map(({ key, label }) => {
241
+ const value = cmiData[key];
242
+ return '<tr><td>' + label + '</td><td>' + (value !== '' && value !== undefined ? value : '<span class="value-empty">-</span>') + '</td></tr>';
243
+ }).join('');
244
+
245
+ // Objectives section
246
+ const objectives = cmiData._objectives || {};
247
+ const objList = Object.values(objectives);
248
+ document.getElementById('stub-player-objectives-count').textContent = objList.length;
249
+ if (objList.length === 0) {
250
+ objectivesData.innerHTML = '<span class="value-empty">No objectives recorded</span>';
251
+ } else {
252
+ objectivesData.innerHTML = '<table class="data-table"><thead><tr><th>ID</th><th>Completion</th><th>Success</th><th>Score</th></tr></thead><tbody>' +
253
+ objList.map(obj => {
254
+ const completion = obj.completion_status || '-';
255
+ const success = obj.success_status || '-';
256
+ const score = obj.score_scaled !== undefined ? obj.score_scaled : '-';
257
+ return '<tr><td>' + (obj.id || '-') + '</td><td>' + completion + '</td><td>' + success + '</td><td>' + score + '</td></tr>';
258
+ }).join('') + '</tbody></table>';
259
+ }
260
+
261
+ // Interactions section
262
+ const interactions = cmiData._interactions || [];
263
+ document.getElementById('stub-player-interactions-count').textContent = interactions.length;
264
+ if (interactions.length === 0) {
265
+ interactionsData.innerHTML = '<span class="value-empty">No interactions recorded</span>';
266
+ } else {
267
+ interactionsData.innerHTML = '<table class="data-table"><thead><tr><th>ID</th><th>Type</th><th>Response</th><th>Result</th></tr></thead><tbody>' +
268
+ interactions.map(int => {
269
+ const result = int.result || '-';
270
+ const resultClass = result === 'correct' ? 'result-correct' : result === 'incorrect' || result === 'wrong' ? 'result-incorrect' : '';
271
+ return '<tr><td title="' + escapeHtmlInline(int.id || '') + '">' + escapeHtmlInline((int.id || '-').substring(0, 20)) + '</td><td>' +
272
+ (int.type || '-') + '</td><td title="' + escapeHtmlInline(int.learner_response || int.student_response || '') + '">' +
273
+ escapeHtmlInline(((int.learner_response || int.student_response || '-') + '').substring(0, 15)) + '</td><td class="' + resultClass + '">' + result + '</td></tr>';
274
+ }).join('') + '</tbody></table>';
275
+ }
276
+
277
+ // Suspend Data section
278
+ try {
279
+ const suspend = cmiData['cmi.suspend_data'];
280
+ if (suspend) {
281
+ const rawBytes = suspend.length;
282
+ let decoded;
283
+ try { decoded = JSON.parse(suspend); } catch { decoded = null; }
284
+
285
+ if (decoded) {
286
+ const prettyJson = JSON.stringify(decoded, null, 2);
287
+ suspendInfo.innerHTML = 'Size: <span class="size">' + formatBytes(rawBytes) + '</span>';
288
+ suspendPre.textContent = prettyJson;
289
+ } else {
290
+ suspendInfo.innerHTML = 'Size: <span class="size">' + formatBytes(rawBytes) + '</span> <span style="color:#f59e0b">(raw/non-JSON)</span>';
291
+ suspendPre.textContent = suspend;
292
+ }
293
+ } else {
294
+ suspendInfo.innerHTML = '';
295
+ suspendPre.textContent = '(empty)';
296
+ }
297
+ } catch {
298
+ suspendInfo.innerHTML = '';
299
+ suspendPre.textContent = cmiData['cmi.suspend_data'] || '(empty)';
300
+ }
301
+ }
302
+
303
+ function updateApiLogDisplay() {
304
+ const logEl = document.getElementById('stub-player-api-log');
305
+ if (!logEl) return;
306
+ logEl.innerHTML = apiLog.map(entry =>
307
+ '<div class="log-entry' + (entry.isError ? ' error' : '') + '">' +
308
+ '<span class="timestamp">' + entry.timestamp + '</span>' +
309
+ '<span class="method">' + entry.method + '</span>' +
310
+ (entry.args ? '<span class="args">' + escapeHtmlInline(entry.args) + '</span>' : '') +
311
+ '<span class="result">→ ' + escapeHtmlInline(entry.result) + '</span>' +
312
+ '</div>'
313
+ ).join('');
314
+ }
315
+
316
+ function updateErrorDisplay() {
317
+ const logEl = document.getElementById('stub-player-error-log');
318
+ const badge = document.getElementById('stub-player-error-badge');
319
+ const inlineBadge = document.getElementById('stub-player-header-error-badge-inline');
320
+ if (!logEl) return;
321
+
322
+ const errors = errorLog.filter(e => !e.isWarning);
323
+ const warnings = errorLog.filter(e => e.isWarning);
324
+ const totalCount = errorLog.length;
325
+ if (inlineBadge) {
326
+ if (totalCount > 0) {
327
+ inlineBadge.textContent = totalCount > 99 ? '99+' : totalCount;
328
+ inlineBadge.classList.add('visible');
329
+ inlineBadge.classList.toggle('has-warnings', errors.length === 0 && warnings.length > 0);
330
+ } else {
331
+ inlineBadge.classList.remove('visible');
332
+ }
333
+ }
334
+
335
+ // Show/hide Copy All button
336
+ const actionsEl = document.getElementById('stub-player-error-actions');
337
+ if (actionsEl) actionsEl.style.display = errorLog.length > 0 ? 'flex' : 'none';
338
+
339
+ if (errorLog.length === 0) {
340
+ logEl.innerHTML = '<div id="stub-player-no-errors">✓ No errors or warnings</div>';
341
+ badge.innerHTML = '';
342
+ } else {
343
+ logEl.innerHTML = errorLog.map((entry, index) => {
344
+ return '<div class="error-entry' + (entry.isWarning ? ' warning' : '') + '">' +
345
+ '<button class="error-copy-btn" data-error-index="' + index + '" title="Copy to clipboard">📋</button>' +
346
+ '<div class="error-type">' + escapeHtmlInline(entry.type) + '</div>' +
347
+ '<div class="error-message">' + escapeHtmlInline(entry.message) + '</div>' +
348
+ (entry.hint ? '<div class="error-hint">' + escapeHtmlInline(entry.hint) + '</div>' : '') +
349
+ '</div>';
350
+ }).join('');
351
+
352
+ // Add click handlers for individual copy buttons
353
+ logEl.querySelectorAll('.error-copy-btn').forEach(btn => {
354
+ btn.addEventListener('click', (e) => {
355
+ e.stopPropagation();
356
+ const idx = parseInt(btn.dataset.errorIndex, 10);
357
+ const entry = errorLog[idx];
358
+ if (entry) {
359
+ const text = `${entry.type}: ${entry.message}${entry.hint ? '\nHint: ' + entry.hint : ''}`;
360
+ navigator.clipboard.writeText(text).then(() => {
361
+ btn.textContent = '✓';
362
+ setTimeout(() => { btn.textContent = '📋'; }, 1500);
363
+ });
364
+ }
365
+ });
366
+ });
367
+
368
+ // Copy All button handler
369
+ const copyAllBtn = document.getElementById('stub-player-copy-all-errors');
370
+ if (copyAllBtn) {
371
+ copyAllBtn.onclick = () => {
372
+ const allText = errorLog.map(entry => {
373
+ const prefix = entry.isWarning ? 'WARNING' : 'ERROR';
374
+ return `[${prefix}] ${entry.type}: ${entry.message}${entry.hint ? '\nHint: ' + entry.hint : ''}`;
375
+ }).join('\n\n');
376
+ navigator.clipboard.writeText(allText).then(() => {
377
+ copyAllBtn.textContent = '✓ Copied';
378
+ setTimeout(() => { copyAllBtn.textContent = '📋 Copy All'; }, 1500);
379
+ });
380
+ };
381
+ }
382
+
383
+ let badgeHtml = '';
384
+ if (errors.length > 0) badgeHtml += '<span class="error-count-badge">' + errors.length + '</span>';
385
+ if (warnings.length > 0) badgeHtml += '<span class="warning-count-badge">' + warnings.length + '</span>';
386
+ badge.innerHTML = badgeHtml;
387
+ }
388
+ }
389
+
390
+ function updateXapiDisplay() {
391
+ const logEl = document.getElementById('stub-player-xapi-log');
392
+ const badge = document.getElementById('stub-player-xapi-badge');
393
+ if (!logEl) return;
394
+
395
+ if (xapiLog.length === 0) {
396
+ logEl.innerHTML = '<div id="stub-player-no-statements">No xAPI statements yet. Navigate through the course to see statements.</div>';
397
+ if (badge) badge.innerHTML = '';
398
+ } else {
399
+ logEl.innerHTML = xapiLog.map(entry => {
400
+ const verb = entry.type || 'unknown';
401
+ const data = entry.data || {};
402
+ const objectId = data.id || data.objectiveId || data.assessmentId || '-';
403
+ const details = JSON.stringify(data, null, 2);
404
+
405
+ return '<div class="xapi-entry verb-' + verb + '" data-expandable>' +
406
+ '<div class="xapi-header">' +
407
+ '<span class="xapi-timestamp">' + escapeHtmlInline(entry.receivedAt) + '</span>' +
408
+ '<span class="xapi-verb">' + escapeHtmlInline(verb) + '</span>' +
409
+ '<span class="xapi-type">' + escapeHtmlInline(entry.type || '') + '</span>' +
410
+ '</div>' +
411
+ '<div class="xapi-object">' + escapeHtmlInline(objectId) + '</div>' +
412
+ '<div class="xapi-details">' + escapeHtmlInline(details) + '</div>' +
413
+ '</div>';
414
+ }).join('');
415
+
416
+ if (badge) {
417
+ badge.innerHTML = '<span class="xapi-count-badge">' + xapiLog.length + '</span>';
418
+ }
419
+ }
420
+ }