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,354 @@
1
+ /**
2
+ * stub-player/header-bar.js - Header bar component
3
+ *
4
+ * Generates the header bar HTML with all control buttons.
5
+ */
6
+
7
+ const HEADER_ICON_PATHS = {
8
+ monitor: '<rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/>',
9
+ edit: '<path d="M13 21h8"/><path d="m15 5 4 4"/><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/>',
10
+ review: '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
11
+ debug: '<path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/>',
12
+ more: '<circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/>',
13
+ guide: '<path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z"/><circle cx="12" cy="12" r="10"/>',
14
+ config: '<line x1="4" x2="4" y1="21" y2="14"/><line x1="4" x2="4" y1="10" y2="3"/><line x1="12" x2="12" y1="21" y2="12"/><line x1="12" x2="12" y1="8" y2="3"/><line x1="20" x2="20" y1="21" y2="16"/><line x1="20" x2="20" y1="12" y2="3"/><line x1="2" x2="6" y1="14" y2="14"/><line x1="10" x2="14" y1="8" y2="8"/><line x1="18" x2="22" y1="16" y2="16"/>',
15
+ interactions: '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
16
+ catalog: '<path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/>',
17
+ slideId: '<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/>',
18
+ skipGating: '<circle cx="15" cy="12" r="3"/><rect width="20" height="14" x="2" y="5" rx="7"/>',
19
+ reset: '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>',
20
+ minimize: '<path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/>',
21
+ maximize: '<path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/>'
22
+ };
23
+
24
+ function renderHeaderIcon(name, className = 'header-icon') {
25
+ const path = HEADER_ICON_PATHS[name];
26
+ if (!path) return '';
27
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="${className}" aria-hidden="true">${path}</svg>`;
28
+ }
29
+
30
+ function renderCourseCodeLogo(className = 'header-logo') {
31
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 18 100 64" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" class="${className}" aria-hidden="true"><polyline points="25,22 5,50 25,78"/><polyline points="75,22 95,50 75,78"/><path d="M50,28 C40,28 33,36 33,45 C33,52 38,56 42,60 L42,65 L58,65 L58,60 C62,56 67,52 67,45 C67,36 60,28 50,28" stroke-width="4"/><line x1="44" y1="70" x2="56" y2="70" stroke-width="4"/><line x1="46" y1="75" x2="54" y2="75" stroke-width="4"/></svg>`;
32
+ }
33
+
34
+ const SHOW_SLIDE_ID_STORAGE_KEY = 'coursecode-showSlideId';
35
+ const SKIP_GATING_STORAGE_KEY = 'coursecode-skipGating';
36
+
37
+ /**
38
+ * Generate header bar HTML
39
+ * @param {Object} options
40
+ * @param {boolean} options.isLive - Whether this is live mode
41
+ * @param {boolean} options.hasContent - Whether content viewer should be shown
42
+ */
43
+ export function generateHeaderBar({ isLive, hasContent }) {
44
+ return `
45
+ <div id="stub-player-header">
46
+ <div class="header-content">
47
+ <span class="label">
48
+ ${renderCourseCodeLogo('header-logo')}
49
+ CourseCode
50
+ </span>
51
+ ${isLive ? '<span id="stub-player-slide-id" class="slide-id-badge" title="Current slide ID"></span>' : ''}
52
+ <div class="spacer"></div>
53
+ ${isLive ? `<button id="stub-player-edit-mode-btn" class="visible" title="Edit text in course">${renderHeaderIcon('edit')} <span class="btn-label">Edit</span></button>` : ''}
54
+ ${hasContent ? `<button id="stub-player-content-btn" title="Review content">${renderHeaderIcon('review')} <span class="btn-label">Review</span></button>` : ''}
55
+ ${isLive ? `<button id="stub-player-debug-btn" title="Debug panel">${renderHeaderIcon('debug')} <span class="btn-label">Debug</span><span id="stub-player-header-error-badge-inline"></span></button>` : ''}
56
+ <div class="more-menu-wrap">
57
+ <button id="stub-player-more-btn" title="More tools">${renderHeaderIcon('more')} <span class="btn-label">More</span></button>
58
+ <div id="stub-player-more-menu" class="more-menu">
59
+ ${isLive ? `<button id="stub-player-status-btn" class="menu-item" title="Workflow guide">${renderHeaderIcon('guide', 'header-icon menu-icon')} Guide</button>` : ''}
60
+ ${isLive ? `<button id="stub-player-config-btn" class="menu-item" title="Course config">${renderHeaderIcon('config', 'header-icon menu-icon')} Config</button>` : ''}
61
+ ${isLive ? `<button id="stub-player-interactions-btn" class="menu-item" title="Interactions">${renderHeaderIcon('interactions', 'header-icon menu-icon')} Interactions</button>` : ''}
62
+ ${isLive ? `<button id="stub-player-catalog-btn" class="menu-item" title="Component catalog">${renderHeaderIcon('catalog', 'header-icon menu-icon')} UI Catalog</button>` : ''}
63
+ ${isLive ? `<div class="menu-divider"></div>
64
+ <div class="menu-toggle-row" title="Show or hide current slide ID in header">
65
+ <span class="menu-toggle-label-wrap">${renderHeaderIcon('slideId', 'header-icon menu-icon')} <span id="stub-player-show-slide-id-label">Show Slide ID: On</span></span>
66
+ <input type="checkbox" id="stub-player-show-slide-id" class="menu-toggle-input" checked aria-label="Show Slide ID">
67
+ <button id="stub-player-show-slide-id-toggle" class="config-toggle on" type="button" role="switch" aria-checked="true" aria-label="Show Slide ID"></button>
68
+ </div>` : ''}
69
+ <div class="menu-toggle-row" title="Skip navigation gating">
70
+ <span class="menu-toggle-label-wrap">${renderHeaderIcon('skipGating', 'header-icon menu-icon')} <span id="stub-player-skip-gating-label">Skip Gating: On</span></span>
71
+ <input type="checkbox" id="stub-player-skip-gating" class="menu-toggle-input" checked aria-label="Skip Gating">
72
+ <button id="stub-player-skip-gating-toggle" class="config-toggle on" type="button" role="switch" aria-checked="true" aria-label="Skip Gating"></button>
73
+ </div>
74
+ <button id="stub-player-reset-btn" class="menu-item danger-item" title="Reset progress">${renderHeaderIcon('reset', 'header-icon menu-icon')} Reset Progress</button>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ <button class="toggle-btn" id="stub-player-header-toggle" title="Minimize preview" aria-label="Minimize preview">${renderHeaderIcon('minimize')}</button>
79
+ </div>
80
+ <div id="stub-player-confirm-dialog" class="confirm-dialog">
81
+ <div class="confirm-content">
82
+ <h3 id="stub-player-confirm-title">Confirm</h3>
83
+ <p id="stub-player-confirm-message"></p>
84
+ <div class="confirm-actions">
85
+ <button id="stub-player-confirm-cancel">Cancel</button>
86
+ <button id="stub-player-confirm-ok" class="btn-primary">OK</button>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ `;
91
+ }
92
+
93
+ /**
94
+ * Initialize Header Bar Handlers
95
+ * @param {Object} callbacks
96
+ * @param {Function} callbacks.onToggle - (isCollapsed) => void
97
+ * @param {Function} callbacks.onDebug - () => void
98
+ * @param {Function} callbacks.onConfig - () => void
99
+ * @param {Function} callbacks.onContent - () => void
100
+ * @param {Function} callbacks.onInteract - () => void
101
+ * @param {Function} callbacks.onEdit - () => void
102
+ * @param {Function} callbacks.onReset - () => void
103
+ * @param {Function} callbacks.onSkipGating - (enabled) => void
104
+ * @param {Function} callbacks.onStatus - () => void
105
+ */
106
+ export function createHeaderBarHandlers(callbacks) {
107
+ const { onToggle, onDebug, onConfig, onContent, onInteract, onCatalog, onEdit, onReset, onSkipGating, onStatus } = callbacks;
108
+ const closeMoreMenu = () => {
109
+ document.getElementById('stub-player-more-menu')?.classList.remove('visible');
110
+ };
111
+ const updateSlideIdVisibility = (isVisible) => {
112
+ const header = document.getElementById('stub-player-header');
113
+ if (!header) return;
114
+ header.classList.toggle('hide-slide-id', !isVisible);
115
+ };
116
+
117
+ // Header Toggle
118
+ const toggleBtn = document.getElementById('stub-player-header-toggle');
119
+ if (toggleBtn) {
120
+ toggleBtn.addEventListener('click', () => {
121
+ const header = document.getElementById('stub-player-header');
122
+ const iframe = document.getElementById('stub-player-course-frame');
123
+ const moreMenu = document.getElementById('stub-player-more-menu');
124
+
125
+ header.classList.toggle('collapsed');
126
+ const isCollapsed = header.classList.contains('collapsed');
127
+ if (iframe) iframe.classList.toggle('header-collapsed', isCollapsed);
128
+ if (moreMenu) moreMenu.classList.remove('visible');
129
+ toggleBtn.innerHTML = isCollapsed
130
+ ? `${renderHeaderIcon('maximize')}`
131
+ : `${renderHeaderIcon('minimize')}`;
132
+ toggleBtn.title = isCollapsed ? 'Restore preview' : 'Minimize preview';
133
+ toggleBtn.setAttribute('aria-label', isCollapsed ? 'Restore preview' : 'Minimize preview');
134
+
135
+ if (onToggle) onToggle(isCollapsed);
136
+ });
137
+ }
138
+
139
+ // More Menu
140
+ const moreBtn = document.getElementById('stub-player-more-btn');
141
+ const moreMenu = document.getElementById('stub-player-more-menu');
142
+ if (moreBtn && moreMenu) {
143
+ moreBtn.addEventListener('click', (e) => {
144
+ e.stopPropagation();
145
+ moreMenu.classList.toggle('visible');
146
+ });
147
+
148
+ document.addEventListener('click', (e) => {
149
+ if (!moreMenu.contains(e.target) && !moreBtn.contains(e.target)) {
150
+ moreMenu.classList.remove('visible');
151
+ }
152
+ });
153
+ }
154
+
155
+ // Show Slide ID
156
+ const showSlideIdCheckbox = document.getElementById('stub-player-show-slide-id');
157
+ const showSlideIdToggle = document.getElementById('stub-player-show-slide-id-toggle');
158
+ const showSlideIdLabel = document.getElementById('stub-player-show-slide-id-label');
159
+ if (showSlideIdCheckbox) {
160
+ const persisted = (() => {
161
+ try {
162
+ const stored = localStorage.getItem(SHOW_SLIDE_ID_STORAGE_KEY);
163
+ return stored !== 'false';
164
+ } catch {
165
+ return true;
166
+ }
167
+ })();
168
+ showSlideIdCheckbox.checked = persisted;
169
+
170
+ const syncSlideIdUi = () => {
171
+ if (showSlideIdToggle) {
172
+ showSlideIdToggle.classList.toggle('on', showSlideIdCheckbox.checked);
173
+ showSlideIdToggle.setAttribute('aria-checked', showSlideIdCheckbox.checked ? 'true' : 'false');
174
+ }
175
+ if (showSlideIdLabel) {
176
+ showSlideIdLabel.textContent = `Show Slide ID: ${showSlideIdCheckbox.checked ? 'On' : 'Off'}`;
177
+ }
178
+ updateSlideIdVisibility(showSlideIdCheckbox.checked);
179
+ };
180
+
181
+ syncSlideIdUi();
182
+
183
+ if (showSlideIdToggle) {
184
+ const toggleShowSlideId = () => {
185
+ showSlideIdCheckbox.checked = !showSlideIdCheckbox.checked;
186
+ showSlideIdCheckbox.dispatchEvent(new Event('change', { bubbles: true }));
187
+ };
188
+ showSlideIdToggle.addEventListener('click', toggleShowSlideId);
189
+ showSlideIdToggle.addEventListener('keydown', (e) => {
190
+ if (e.key === 'Enter' || e.key === ' ') {
191
+ e.preventDefault();
192
+ toggleShowSlideId();
193
+ }
194
+ });
195
+ }
196
+
197
+ showSlideIdCheckbox.addEventListener('change', () => {
198
+ try {
199
+ localStorage.setItem(SHOW_SLIDE_ID_STORAGE_KEY, showSlideIdCheckbox.checked ? 'true' : 'false');
200
+ } catch {
201
+ // ignore storage failures
202
+ }
203
+ syncSlideIdUi();
204
+ });
205
+ }
206
+
207
+ // Skip Gating (persisted in localStorage)
208
+ const skipGatingCheckbox = document.getElementById('stub-player-skip-gating');
209
+ const skipGatingToggle = document.getElementById('stub-player-skip-gating-toggle');
210
+ const skipGatingLabel = document.getElementById('stub-player-skip-gating-label');
211
+ if (skipGatingCheckbox) {
212
+ // Restore persisted state (defaults to true / skip on)
213
+ const persistedSkip = (() => {
214
+ try {
215
+ const stored = localStorage.getItem(SKIP_GATING_STORAGE_KEY);
216
+ return stored !== 'false';
217
+ } catch {
218
+ return true;
219
+ }
220
+ })();
221
+ skipGatingCheckbox.checked = persistedSkip;
222
+
223
+ const syncSkipToggleUi = () => {
224
+ if (!skipGatingToggle) return;
225
+ skipGatingToggle.classList.toggle('on', skipGatingCheckbox.checked);
226
+ skipGatingToggle.setAttribute('aria-checked', skipGatingCheckbox.checked ? 'true' : 'false');
227
+ if (skipGatingLabel) {
228
+ skipGatingLabel.textContent = `Skip Gating: ${skipGatingCheckbox.checked ? 'On' : 'Off'}`;
229
+ }
230
+ };
231
+
232
+ syncSkipToggleUi();
233
+
234
+ if (skipGatingToggle) {
235
+ const toggleSkip = () => {
236
+ skipGatingCheckbox.checked = !skipGatingCheckbox.checked;
237
+ skipGatingCheckbox.dispatchEvent(new Event('change', { bubbles: true }));
238
+ };
239
+ skipGatingToggle.addEventListener('click', toggleSkip);
240
+ skipGatingToggle.addEventListener('keydown', (e) => {
241
+ if (e.key === 'Enter' || e.key === ' ') {
242
+ e.preventDefault();
243
+ toggleSkip();
244
+ }
245
+ });
246
+ }
247
+
248
+ skipGatingCheckbox.addEventListener('change', (e) => {
249
+ if (!e.target.checked) {
250
+ // Unchecking = enabling gating, show confirmation
251
+ showConfirmDialog({
252
+ title: 'Enable Gating?',
253
+ message: 'This will reset course progress and enforce navigation locks.',
254
+ confirmLabel: 'Confirm',
255
+ confirmClass: 'btn-primary',
256
+ onConfirm: () => {
257
+ try { localStorage.setItem(SKIP_GATING_STORAGE_KEY, 'false'); } catch {}
258
+ if (onSkipGating) onSkipGating(false);
259
+ syncSkipToggleUi();
260
+ },
261
+ onCancel: () => {
262
+ e.target.checked = true; // Revert
263
+ syncSkipToggleUi();
264
+ }
265
+ });
266
+ } else {
267
+ // Checking = skip gating
268
+ try { localStorage.setItem(SKIP_GATING_STORAGE_KEY, 'true'); } catch {}
269
+ if (onSkipGating) onSkipGating(true);
270
+ syncSkipToggleUi();
271
+ }
272
+ });
273
+ }
274
+
275
+ // Reset Button
276
+ const resetBtn = document.getElementById('stub-player-reset-btn');
277
+ if (resetBtn) {
278
+ resetBtn.addEventListener('click', () => {
279
+ closeMoreMenu();
280
+ showConfirmDialog({
281
+ title: 'Reset Progress?',
282
+ message: 'This will clear all course progress. This cannot be undone.',
283
+ confirmLabel: 'Reset',
284
+ confirmClass: 'btn-danger',
285
+ onConfirm: () => {
286
+ if (onReset) onReset();
287
+ }
288
+ });
289
+ });
290
+ }
291
+
292
+ // Panel Buttons
293
+ if (onDebug) document.getElementById('stub-player-debug-btn')?.addEventListener('click', onDebug);
294
+ if (onConfig) document.getElementById('stub-player-config-btn')?.addEventListener('click', () => { closeMoreMenu(); onConfig(); });
295
+ if (onContent) document.getElementById('stub-player-content-btn')?.addEventListener('click', onContent);
296
+ if (onInteract) document.getElementById('stub-player-interactions-btn')?.addEventListener('click', () => { closeMoreMenu(); onInteract(); });
297
+ if (onCatalog) document.getElementById('stub-player-catalog-btn')?.addEventListener('click', () => { closeMoreMenu(); onCatalog(); });
298
+ if (onEdit) document.getElementById('stub-player-edit-mode-btn')?.addEventListener('click', onEdit);
299
+ if (onStatus) document.getElementById('stub-player-status-btn')?.addEventListener('click', () => { closeMoreMenu(); onStatus(); });
300
+
301
+ // Setup Confirm Dialog Handlers
302
+ setupConfirmDialog();
303
+ }
304
+
305
+ // Dialog Logic
306
+ let pendingConfirmCallback = null;
307
+ let pendingCancelCallback = null;
308
+
309
+ function setupConfirmDialog() {
310
+ const okBtn = document.getElementById('stub-player-confirm-ok');
311
+ const cancelBtn = document.getElementById('stub-player-confirm-cancel');
312
+ const dialog = document.getElementById('stub-player-confirm-dialog');
313
+
314
+ if (okBtn) {
315
+ okBtn.addEventListener('click', () => {
316
+ dialog.classList.remove('visible');
317
+ if (pendingConfirmCallback) {
318
+ pendingConfirmCallback();
319
+ pendingConfirmCallback = null;
320
+ pendingCancelCallback = null;
321
+ }
322
+ });
323
+ }
324
+
325
+ if (cancelBtn) {
326
+ cancelBtn.addEventListener('click', () => {
327
+ dialog.classList.remove('visible');
328
+ if (pendingCancelCallback) {
329
+ pendingCancelCallback();
330
+ }
331
+ pendingConfirmCallback = null;
332
+ pendingCancelCallback = null;
333
+ });
334
+ }
335
+ }
336
+
337
+ export function showConfirmDialog({ title, message, confirmLabel = 'Confirm', confirmClass = 'btn-primary', onConfirm, onCancel }) {
338
+ document.getElementById('stub-player-confirm-title').textContent = title;
339
+ document.getElementById('stub-player-confirm-message').textContent = message;
340
+ const confirmBtn = document.getElementById('stub-player-confirm-ok');
341
+ confirmBtn.textContent = confirmLabel;
342
+ confirmBtn.className = confirmClass;
343
+ pendingConfirmCallback = onConfirm;
344
+ pendingCancelCallback = onCancel;
345
+ document.getElementById('stub-player-confirm-dialog').classList.add('visible');
346
+ }
347
+
348
+ export function updateSlideId(id) {
349
+ const badge = document.getElementById('stub-player-slide-id');
350
+ if (badge) {
351
+ badge.textContent = id;
352
+ badge.title = 'Current slide: ' + id;
353
+ }
354
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * stub-player/interaction-editor.js - Popup modal for editing interactions
3
+ *
4
+ * Provides a reusable popup editor that opens when clicking on interaction
5
+ * components in the preview iframe. Reuses renderEditForm from edit-utils.js.
6
+ */
7
+
8
+ import { renderEditForm, saveItemEdits } from './edit-utils.js';
9
+
10
+ let currentInteraction = null;
11
+ let onSaveCallback = null;
12
+
13
+ /**
14
+ * Generate the editor modal HTML
15
+ */
16
+ export function generateInteractionEditor() {
17
+ return `
18
+ <div id="stub-player-interaction-editor" class="interaction-editor-overlay">
19
+ <div class="interaction-editor-modal">
20
+ <div class="interaction-editor-header">
21
+ <div class="interaction-editor-title">
22
+ <span class="interaction-type-badge"></span>
23
+ <span class="interaction-id-text"></span>
24
+ </div>
25
+ <button class="interaction-editor-close" title="Close">&times;</button>
26
+ </div>
27
+ <div class="interaction-editor-body">
28
+ <!-- Form content will be injected here -->
29
+ </div>
30
+ <div class="interaction-editor-footer">
31
+ <button class="interaction-editor-cancel">Cancel</button>
32
+ <button class="interaction-editor-save btn-primary">💾 Save</button>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ `;
37
+ }
38
+
39
+ /**
40
+ * Initialize the interaction editor handlers
41
+ * @param {Object} options
42
+ * @param {Function} options.onSave - Callback after successful save (refresh content)
43
+ */
44
+ export function initInteractionEditor(options = {}) {
45
+ onSaveCallback = options.onSave || null;
46
+
47
+ const overlay = document.getElementById('stub-player-interaction-editor');
48
+ if (!overlay) return;
49
+
50
+ const closeBtn = overlay.querySelector('.interaction-editor-close');
51
+ const cancelBtn = overlay.querySelector('.interaction-editor-cancel');
52
+ const saveBtn = overlay.querySelector('.interaction-editor-save');
53
+
54
+ // Close handlers
55
+ closeBtn?.addEventListener('click', closeEditor);
56
+ cancelBtn?.addEventListener('click', closeEditor);
57
+
58
+ // Click outside to close
59
+ overlay.addEventListener('click', (e) => {
60
+ if (e.target === overlay) {
61
+ closeEditor();
62
+ }
63
+ });
64
+
65
+ // Escape key to close
66
+ document.addEventListener('keydown', (e) => {
67
+ if (e.key === 'Escape' && overlay.classList.contains('visible')) {
68
+ closeEditor();
69
+ }
70
+ });
71
+
72
+ // Save handler
73
+ saveBtn?.addEventListener('click', handleSave);
74
+ }
75
+
76
+
77
+
78
+ /**
79
+ * Open the editor for a specific interaction
80
+ * @param {Object} interaction - The interaction data object
81
+ * @param {string} slideId - The slide ID containing this interaction
82
+ */
83
+ export async function openEditor(interaction, slideId) {
84
+ if (!interaction || !interaction.id) {
85
+ console.warn('Cannot open editor: invalid interaction data');
86
+ return;
87
+ }
88
+
89
+ currentInteraction = { ...interaction, slideId };
90
+
91
+ const overlay = document.getElementById('stub-player-interaction-editor');
92
+ const body = overlay?.querySelector('.interaction-editor-body');
93
+ const typeBadge = overlay?.querySelector('.interaction-type-badge');
94
+ const idText = overlay?.querySelector('.interaction-id-text');
95
+
96
+ if (!overlay || !body) return;
97
+
98
+ // Set header info
99
+ if (typeBadge) typeBadge.textContent = interaction.type || 'unknown';
100
+ if (idText) idText.textContent = interaction.id;
101
+
102
+ // Use schema from interaction (provided by manifest)
103
+ const schema = interaction.schema || null;
104
+
105
+ body.innerHTML = renderEditForm(interaction, schema);
106
+
107
+ // Show the modal
108
+ overlay.classList.add('visible');
109
+
110
+ // Focus first input
111
+ const firstInput = body.querySelector('input, textarea, select');
112
+ if (firstInput) firstInput.focus();
113
+ }
114
+
115
+ /**
116
+ * Open editor by fetching interaction data from server
117
+ * @param {string} interactionId - The interaction ID
118
+ * @param {string} slideId - The slide ID
119
+ */
120
+ export async function openEditorById(interactionId, slideId) {
121
+ try {
122
+ // Fetch interactions from manifest
123
+ const response = await fetch('/_content-manifest.json');
124
+ if (!response.ok) {
125
+ console.error('Failed to fetch manifest');
126
+ return;
127
+ }
128
+
129
+ const manifest = await response.json();
130
+
131
+ // Collect all interactions from slides
132
+ let interaction = null;
133
+ for (const [sId, slideData] of Object.entries(manifest.slides || {})) {
134
+ const found = (slideData.interactions || []).find(i => i.id === interactionId);
135
+ if (found) {
136
+ interaction = { ...found, slideId: sId };
137
+ break;
138
+ }
139
+ }
140
+
141
+ // If not found in standalone interactions, check assessments
142
+ if (!interaction) {
143
+ for (const assessment of (manifest.assessments || [])) {
144
+ const question = (assessment.questions || []).find(q => q.id === interactionId);
145
+ if (question) {
146
+ interaction = { ...question, slideId: assessment.id };
147
+ break;
148
+ }
149
+ }
150
+ }
151
+
152
+ if (interaction) {
153
+ await openEditor(interaction, slideId || interaction.slideId);
154
+ } else {
155
+ console.warn(`Interaction "${interactionId}" not found`);
156
+ }
157
+ } catch (err) {
158
+ console.error('Failed to open editor:', err);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Close the editor modal
164
+ */
165
+ export function closeEditor() {
166
+ const overlay = document.getElementById('stub-player-interaction-editor');
167
+ if (overlay) {
168
+ overlay.classList.remove('visible');
169
+ }
170
+ currentInteraction = null;
171
+ }
172
+
173
+ /**
174
+ * Handle save button click
175
+ */
176
+ async function handleSave() {
177
+ if (!currentInteraction) return;
178
+
179
+ const overlay = document.getElementById('stub-player-interaction-editor');
180
+ const form = overlay?.querySelector('.edit-form');
181
+
182
+ if (!form) return;
183
+
184
+ const saveBtn = overlay.querySelector('.interaction-editor-save');
185
+ if (saveBtn) {
186
+ saveBtn.disabled = true;
187
+ saveBtn.textContent = 'Saving...';
188
+ }
189
+
190
+ try {
191
+ const success = await saveItemEdits(
192
+ '/__edit-interaction',
193
+ form,
194
+ currentInteraction.slideId,
195
+ currentInteraction.id
196
+ );
197
+
198
+ if (success) {
199
+ closeEditor();
200
+ if (onSaveCallback) {
201
+ onSaveCallback();
202
+ }
203
+ }
204
+ } finally {
205
+ if (saveBtn) {
206
+ saveBtn.disabled = false;
207
+ saveBtn.textContent = '💾 Save';
208
+ }
209
+ }
210
+ }