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,274 @@
1
+ /**
2
+ * @file embed-frame.js
3
+ * @description Sandboxed iframe component for embedding custom HTML/JS apps.
4
+ *
5
+ * Provides CSS isolation and JS sandboxing for custom interactive elements.
6
+ * Communicates with the parent frame via postMessage for state persistence
7
+ * and engagement tracking.
8
+ *
9
+ * Usage (Declarative):
10
+ * <div data-component="embed-frame"
11
+ * data-src="assets/widgets/my-app.html"
12
+ * data-embed-id="custom-widget"
13
+ * data-aspect-ratio="16/9">
14
+ * </div>
15
+ *
16
+ * PostMessage API (from embedded content):
17
+ * // Set a flag (triggers engagement re-evaluation)
18
+ * parent.postMessage({ type: 'coursecode:flag', key: 'widget-complete', value: true }, '*');
19
+ *
20
+ * // Request resize (auto-height mode)
21
+ * parent.postMessage({ type: 'coursecode:resize', height: 400 }, '*');
22
+ *
23
+ * // Log a message to the console (for debugging)
24
+ * parent.postMessage({ type: 'coursecode:log', level: 'info', message: 'Widget initialized' }, '*');
25
+ */
26
+
27
+ export const schema = {
28
+ type: 'embed-frame',
29
+ description: 'Sandboxed iframe for custom HTML/JS apps',
30
+ example: `<div data-component="embed-frame" data-src="widgets/my-app.html" data-embed-id="custom-widget" data-aspect-ratio="16/9">
31
+ <p style="color: #64748b; font-size: 0.875rem; font-style: italic;">📦 Sandboxed iframe renders dynamically — embeds custom HTML/JS with postMessage API.</p>
32
+ </div>`,
33
+ properties: {
34
+ src: { type: 'string', required: true, dataAttribute: 'data-src' },
35
+ embedId: { type: 'string', required: true, dataAttribute: 'data-embed-id' },
36
+ aspectRatio: { type: 'string', dataAttribute: 'data-aspect-ratio' }
37
+ },
38
+ structure: {
39
+ container: '[data-component="embed-frame"]',
40
+ children: {} // Content is dynamically rendered
41
+ }
42
+ };
43
+
44
+ export const metadata = {
45
+ category: 'ui-component',
46
+ cssFile: 'components/embed-frame.css',
47
+ engagementTracking: null,
48
+ emitsEvents: ['embed-frame:initialized', 'embed-frame:ready', 'embed-frame:flag']
49
+ };
50
+
51
+ import { logger } from '../../utilities/logger.js';
52
+ import { eventBus } from '../../core/event-bus.js';
53
+ import flagManager from '../../managers/flag-manager.js';
54
+
55
+ /**
56
+ * Initializes an embed-frame component.
57
+ * @param {HTMLElement} container - The container element with data-component="embed-frame"
58
+ */
59
+ export function init(container) {
60
+ if (!container) {
61
+ logger.fatal('initEmbedFrame: container not found.', { domain: 'ui', operation: 'initEmbedFrame' });
62
+ return;
63
+ }
64
+
65
+ const src = container.dataset.src;
66
+ const embedId = container.dataset.embedId;
67
+ const aspectRatio = container.dataset.aspectRatio || null;
68
+
69
+ if (!src) {
70
+ logger.fatal('initEmbedFrame: data-src attribute is required.', { domain: 'ui', operation: 'initEmbedFrame' });
71
+ return;
72
+ }
73
+
74
+ if (!embedId) {
75
+ logger.fatal('initEmbedFrame: data-embed-id attribute is required.', { domain: 'ui', operation: 'initEmbedFrame' });
76
+ return;
77
+ }
78
+
79
+ // Mark as initialized
80
+ if (container.dataset.embedInitialized === 'true') {
81
+ logger.debug('[EmbedFrame] Already initialized:', embedId);
82
+ return { destroy: () => { } };
83
+ }
84
+ container.dataset.embedInitialized = 'true';
85
+
86
+ // Build the iframe
87
+ const wrapper = document.createElement('div');
88
+ wrapper.className = 'embed-frame-wrapper';
89
+
90
+ if (aspectRatio) {
91
+ wrapper.style.aspectRatio = aspectRatio;
92
+ }
93
+
94
+ const iframe = document.createElement('iframe');
95
+ iframe.className = 'embed-frame-iframe';
96
+ iframe.src = _resolvePath(src);
97
+ // SECURITY NOTE: 'allow-same-origin' is required for postMessage to work reliably
98
+ // when the embedded widget is loaded from the same origin (local assets).
99
+ // Without it, the iframe gets an opaque origin ('null') which breaks postMessage
100
+ // in some browsers. The trade-off is that same-origin iframes CAN access the
101
+ // parent's DOM, cookies, and localStorage. This is acceptable because:
102
+ // 1. embed-frame is for author-controlled widgets, not untrusted user content
103
+ // 2. We validate event.source to ensure messages come from the correct iframe
104
+ // 3. For untrusted content, authors should use an external URL (cross-origin)
105
+ iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms');
106
+ iframe.setAttribute('loading', 'lazy');
107
+ iframe.setAttribute('title', `Embedded content: ${embedId}`);
108
+ iframe.setAttribute('data-testid', `embed-frame-${embedId}`);
109
+
110
+ // For auto-height mode (no aspect ratio)
111
+ if (!aspectRatio) {
112
+ wrapper.classList.add('embed-frame-auto-height');
113
+ }
114
+
115
+ wrapper.appendChild(iframe);
116
+ container.appendChild(wrapper);
117
+
118
+ // Message handler for postMessage communication
119
+ const messageHandler = (event) => {
120
+ // Validate the message came from this iframe
121
+ if (event.source !== iframe.contentWindow) {
122
+ return;
123
+ }
124
+
125
+ const data = event.data;
126
+ if (!data || typeof data !== 'object' || !data.type) {
127
+ return;
128
+ }
129
+
130
+ // Only handle coursecode: prefixed messages
131
+ if (!data.type.startsWith('coursecode:')) {
132
+ return;
133
+ }
134
+
135
+ const action = data.type.replace('coursecode:', '');
136
+
137
+ switch (action) {
138
+ case 'flag':
139
+ handleFlagMessage(data, embedId);
140
+ break;
141
+ case 'resize':
142
+ handleResizeMessage(data, wrapper, aspectRatio);
143
+ break;
144
+ case 'log':
145
+ handleLogMessage(data, embedId);
146
+ break;
147
+ case 'ready':
148
+ handleReadyMessage(embedId);
149
+ break;
150
+ default:
151
+ logger.warn(`[EmbedFrame] Unknown message type: ${data.type}`, { embedId });
152
+ }
153
+ };
154
+
155
+ window.addEventListener('message', messageHandler);
156
+
157
+ // Emit initialization event
158
+ eventBus.emit('embed-frame:initialized', { embedId, src });
159
+ logger.debug('[EmbedFrame] Initialized:', { embedId, src, aspectRatio });
160
+
161
+ return {
162
+ destroy: () => {
163
+ window.removeEventListener('message', messageHandler);
164
+ logger.debug('[EmbedFrame] Destroyed:', embedId);
165
+ }
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Handles flag messages from embedded content.
171
+ * @param {object} data - Message data with key and value
172
+ * @param {string} embedId - The embed ID for logging
173
+ */
174
+ function handleFlagMessage(data, embedId) {
175
+ const { key, value } = data;
176
+
177
+ if (typeof key !== 'string' || key.trim() === '') {
178
+ logger.error('[EmbedFrame] Invalid flag key:', { embedId, key });
179
+ return;
180
+ }
181
+
182
+ try {
183
+ flagManager.setFlag(key, value);
184
+ logger.debug('[EmbedFrame] Flag set via postMessage:', { embedId, key, value });
185
+ eventBus.emit('embed-frame:flag', { embedId, key, value });
186
+ } catch (error) {
187
+ logger.error('[EmbedFrame] Failed to set flag:', { embedId, key, error: error.message });
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Handles resize messages from embedded content (auto-height mode).
193
+ * @param {object} data - Message data with height
194
+ * @param {HTMLElement} wrapper - The wrapper element
195
+ * @param {string|null} aspectRatio - The configured aspect ratio
196
+ */
197
+ function handleResizeMessage(data, wrapper, aspectRatio) {
198
+ // Only allow resize in auto-height mode
199
+ if (aspectRatio) {
200
+ logger.warn('[EmbedFrame] Resize ignored - aspect ratio is fixed');
201
+ return;
202
+ }
203
+
204
+ const { height } = data;
205
+ if (typeof height !== 'number' || height < 50 || height > 5000) {
206
+ logger.warn('[EmbedFrame] Invalid resize height:', height);
207
+ return;
208
+ }
209
+
210
+ wrapper.style.height = `${height}px`;
211
+ logger.debug('[EmbedFrame] Resized to:', height);
212
+ }
213
+
214
+ /**
215
+ * Handles log messages from embedded content.
216
+ * @param {object} data - Message data with level and message
217
+ * @param {string} embedId - The embed ID for context
218
+ */
219
+ function handleLogMessage(data, embedId) {
220
+ const { level = 'info', message } = data;
221
+ const prefix = `[EmbedFrame:${embedId}]`;
222
+
223
+ switch (level) {
224
+ case 'debug':
225
+ logger.debug(prefix, message);
226
+ break;
227
+ case 'info':
228
+ logger.info(prefix, message);
229
+ break;
230
+ case 'warn':
231
+ logger.warn(prefix, message);
232
+ break;
233
+ case 'error':
234
+ logger.error(prefix, message);
235
+ break;
236
+ default:
237
+ logger.info(prefix, message);
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Handles ready messages from embedded content.
243
+ * @param {string} embedId - The embed ID
244
+ */
245
+ function handleReadyMessage(embedId) {
246
+ logger.debug('[EmbedFrame] Embed ready:', embedId);
247
+ eventBus.emit('embed-frame:ready', { embedId });
248
+ }
249
+
250
+ /**
251
+ * Resolves embed path relative to course/assets/.
252
+ * Follows the same pattern as audio-manager and video-player.
253
+ * @param {string} src - The source path
254
+ * @returns {string} Resolved path
255
+ */
256
+ function _resolvePath(src) {
257
+ // Already absolute URL or protocol-relative
258
+ if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('//')) {
259
+ return src;
260
+ }
261
+
262
+ // Already has leading slash (absolute path from root)
263
+ if (src.startsWith('/')) {
264
+ return src;
265
+ }
266
+
267
+ // Already uses ./ or ../ relative paths
268
+ if (src.startsWith('./') || src.startsWith('../')) {
269
+ return src;
270
+ }
271
+
272
+ // Otherwise, assume relative to course/assets/
273
+ return `./course/${src}`;
274
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Features Layout Pattern
3
+ *
4
+ * CSS-only component for highlighting 3-4 key features with icons.
5
+ */
6
+
7
+ export const schema = {
8
+ type: 'features',
9
+ description: 'Feature showcase with icons and centered layout',
10
+ example: `<div data-component="features">
11
+ <div class="feature-item"><div class="feature-icon">🚀</div><h3>Fast</h3><p>Lightning-fast performance for all operations.</p></div>
12
+ <div class="feature-item"><div class="feature-icon">🔒</div><h3>Secure</h3><p>Built with security best practices.</p></div>
13
+ <div class="feature-item"><div class="feature-icon">🎨</div><h3>Flexible</h3><p>Customize everything to your needs.</p></div>
14
+ </div>`,
15
+ properties: {},
16
+ structure: {
17
+ container: '[data-component="features"]',
18
+ children: {
19
+ item: { selector: '.feature-item', required: true, minItems: 1 },
20
+ icon: { selector: '.feature-icon', parent: '.feature-item' },
21
+ title: { selector: 'h3', parent: '.feature-item' }
22
+ }
23
+ }
24
+ };
25
+
26
+ export const metadata = {
27
+ category: 'ui-component',
28
+ cssOnly: true,
29
+ cssFile: 'components/features.css'
30
+ };
31
+
32
+ /** No-op initializer — CSS-only component, registered for consistency. */
33
+ export function init() {}
@@ -0,0 +1,230 @@
1
+ /**
2
+ * @file flip-card.js
3
+ * @description Flip card component with accessibility and engagement tracking.
4
+ *
5
+ * Usage:
6
+ * <div class="flip-card" data-component="flip-card" data-flip-card-id="card-1">
7
+ * <div class="flip-card-inner">
8
+ * <div class="flip-card-front">Front Content</div>
9
+ * <div class="flip-card-back">Back Content</div>
10
+ * </div>
11
+ * </div>
12
+ *
13
+ * For engagement tracking, use the viewAllFlipCards requirement:
14
+ * engagement: {
15
+ * required: true,
16
+ * requirements: [{ type: 'viewAllFlipCards' }]
17
+ * }
18
+ *
19
+ * Each flip card must have a unique data-flip-card-id attribute for tracking.
20
+ */
21
+
22
+ export const schema = {
23
+ type: 'flip-card',
24
+ description: 'Interactive flip card with front/back content',
25
+ example: `<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
26
+ <div class="flip-card" data-component="flip-card">
27
+ <div class="flip-card-inner">
28
+ <div class="flip-card-front"><span class="flip-card-icon">🎯</span><h3 class="flip-card-title">Click to Flip</h3><p class="text-sm text-muted">Front side</p></div>
29
+ <div class="flip-card-back"><h3 class="flip-card-title">Answer</h3><p class="flip-card-text">This is the back side content.</p></div>
30
+ </div>
31
+ </div>
32
+ <div class="flip-card" data-component="flip-card">
33
+ <div class="flip-card-inner">
34
+ <div class="flip-card-front"><span class="flip-card-icon">💡</span><h3 class="flip-card-title">Key Concept</h3><p class="text-sm text-muted">Click to reveal</p></div>
35
+ <div class="flip-card-back bg-secondary"><h3 class="flip-card-title">Definition</h3><p class="flip-card-text">The concept explained in detail.</p></div>
36
+ </div>
37
+ </div>
38
+ </div>`,
39
+ properties: {
40
+ flipCardId: { type: 'string', required: true, dataAttribute: 'data-flip-card-id' }
41
+ },
42
+ structure: {
43
+ container: '[data-component="flip-card"]',
44
+ children: {
45
+ inner: { selector: '.flip-card-inner', required: true },
46
+ front: { selector: '.flip-card-front', required: true },
47
+ back: { selector: '.flip-card-back', required: true }
48
+ }
49
+ }
50
+ };
51
+
52
+ export const metadata = {
53
+ category: 'ui-component',
54
+ cssFile: 'components/flip-cards.css',
55
+ engagementTracking: 'viewAllFlipCards',
56
+ emitsEvents: ['flipcard:flipped']
57
+ };
58
+
59
+ import engagementManager from '../../engagement/engagement-manager.js';
60
+ import * as NavigationState from '../../navigation/NavigationState.js';
61
+ import { announceToScreenReader } from './index.js';
62
+ import { logger } from '../../utilities/logger.js';
63
+
64
+ /**
65
+ * Initializes all flip card components within a container.
66
+ * Call this once per slide to register all flip cards for engagement tracking.
67
+ *
68
+ * @param {HTMLElement|string} root - Container element or selector
69
+ * @returns {Object} API for controlling flip cards
70
+ */
71
+ export function initFlipCards(root) {
72
+ const container = typeof root === 'string' ? document.querySelector(root) : root;
73
+ if (!container) {
74
+ logger.fatal('UIComponents.initFlipCards: container not found', { domain: 'ui', operation: 'initFlipCards' });
75
+ return;
76
+ }
77
+
78
+ const flipCards = Array.from(container.querySelectorAll('[data-component="flip-card"], .flip-card'));
79
+
80
+ if (flipCards.length === 0) {
81
+ logger.debug('[FlipCards] No flip cards found in container');
82
+ return { destroy: () => {} };
83
+ }
84
+
85
+ // Validate: All flip cards should have unique IDs for tracking
86
+ const cardIds = [];
87
+ const errors = [];
88
+
89
+ flipCards.forEach((card, index) => {
90
+ const cardId = card.dataset.flipCardId;
91
+ if (!cardId) {
92
+ errors.push(`Flip card ${index + 1} is missing data-flip-card-id attribute.`);
93
+ } else if (cardIds.includes(cardId)) {
94
+ errors.push(`Duplicate flip card ID: "${cardId}". Each flip card must have a unique ID.`);
95
+ } else {
96
+ cardIds.push(cardId);
97
+ }
98
+ });
99
+
100
+ if (errors.length > 0) {
101
+ logger.fatal(`UIComponents.initFlipCards: Invalid structure:\n${errors.join('\n')}`, { domain: 'ui', operation: 'initFlipCards' });
102
+ return;
103
+ }
104
+
105
+ // Register flip cards with engagement manager
106
+ const currentSlideId = NavigationState.getCurrentSlideId();
107
+ if (currentSlideId) {
108
+ engagementManager.registerFlipCards(currentSlideId, cardIds);
109
+ }
110
+
111
+ // Initialize each flip card
112
+ flipCards.forEach(card => {
113
+ init(card);
114
+ });
115
+
116
+ return {
117
+ /**
118
+ * Programmatically flip a card by ID
119
+ * @param {string} cardId - The flip card ID
120
+ * @param {boolean} [flipped=true] - Whether to flip to back (true) or front (false)
121
+ */
122
+ flipCard(cardId, flipped = true) {
123
+ const card = container.querySelector(`[data-flip-card-id="${cardId}"]`);
124
+ if (card) {
125
+ if (flipped) {
126
+ card.classList.add('is-flipped');
127
+ } else {
128
+ card.classList.remove('is-flipped');
129
+ }
130
+ card.setAttribute('aria-expanded', String(flipped));
131
+
132
+ // Track the flip for engagement
133
+ if (flipped) {
134
+ const slideId = NavigationState.getCurrentSlideId();
135
+ if (slideId) {
136
+ engagementManager.trackFlipCardView(slideId, cardId);
137
+ }
138
+ }
139
+ }
140
+ },
141
+
142
+ /**
143
+ * Get IDs of all flipped cards
144
+ * @returns {string[]} Array of flipped card IDs
145
+ */
146
+ getFlippedCards() {
147
+ return flipCards
148
+ .filter(card => card.classList.contains('is-flipped'))
149
+ .map(card => card.dataset.flipCardId);
150
+ },
151
+
152
+ /**
153
+ * Reset all cards to unflipped state
154
+ */
155
+ resetAll() {
156
+ flipCards.forEach(card => {
157
+ card.classList.remove('is-flipped');
158
+ card.setAttribute('aria-expanded', 'false');
159
+ });
160
+ },
161
+
162
+ /**
163
+ * Destroy event listeners
164
+ */
165
+ destroy() {
166
+ // Individual card cleanup is handled by init
167
+ // This is mainly for consistency with other component APIs
168
+ }
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Initializes a single flip card component.
174
+ * Ensures the card has a tabindex for keyboard accessibility and handles click/keyboard events.
175
+ * Note: Flip card registration for engagement tracking is handled by ui-initializer
176
+ * after all flip cards on the slide are initialized.
177
+ * @param {HTMLElement} element - The flip card container element.
178
+ */
179
+ export function init(element) {
180
+ if (!element) return;
181
+
182
+ const cardId = element.dataset.flipCardId;
183
+
184
+ // Ensure the card is keyboard focusable if not already set
185
+ if (!element.hasAttribute('tabindex')) {
186
+ element.setAttribute('tabindex', '0');
187
+ }
188
+
189
+ // Set initial ARIA attributes
190
+ if (!element.hasAttribute('aria-expanded')) {
191
+ element.setAttribute('aria-expanded', 'false');
192
+ }
193
+ if (!element.hasAttribute('role')) {
194
+ element.setAttribute('role', 'button');
195
+ }
196
+
197
+ // Toggle flip state
198
+ const toggleFlip = (e) => {
199
+ // Prevent default if it's a keydown event to avoid scrolling
200
+ if (e.type === 'keydown') {
201
+ e.preventDefault();
202
+ }
203
+ element.classList.toggle('is-flipped');
204
+
205
+ const isFlipped = element.classList.contains('is-flipped');
206
+ element.setAttribute('aria-expanded', String(isFlipped));
207
+
208
+ // Track flip for engagement when card is flipped to back
209
+ if (isFlipped && cardId) {
210
+ const slideId = NavigationState.getCurrentSlideId();
211
+ if (slideId) {
212
+ engagementManager.trackFlipCardView(slideId, cardId);
213
+ }
214
+ }
215
+
216
+ // Announce state change for screen readers
217
+ const action = isFlipped ? 'Card flipped to reveal back' : 'Card flipped to show front';
218
+ announceToScreenReader(action);
219
+ };
220
+
221
+ // Click listener
222
+ element.addEventListener('click', toggleFlip);
223
+
224
+ // Keyboard listener (Enter or Space)
225
+ element.addEventListener('keydown', (e) => {
226
+ if (e.key === 'Enter' || e.key === ' ') {
227
+ toggleFlip(e);
228
+ }
229
+ });
230
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @file form-validator.js
3
+ * @description Handles declarative form validation and submission feedback.
4
+ */
5
+
6
+ export const schema = {
7
+ type: 'form-validator',
8
+ description: 'Declarative form validation with feedback',
9
+ example: `<form data-component="form-validator" data-success-message="Thanks for your feedback!" data-error-message="Please complete all fields." style="display: flex; flex-direction: column; gap: 12px; max-width: 320px;">
10
+ <label style="font-weight: 500; font-size: 0.875rem;">Name
11
+ <input type="text" required placeholder="Your name" style="display: block; width: 100%; margin-top: 4px; padding: 6px 10px; border: 1px solid #cbd5e1; border-radius: 4px;">
12
+ </label>
13
+ <label style="font-weight: 500; font-size: 0.875rem;">Email
14
+ <input type="email" required placeholder="you@example.com" style="display: block; width: 100%; margin-top: 4px; padding: 6px 10px; border: 1px solid #cbd5e1; border-radius: 4px;">
15
+ </label>
16
+ <button type="submit" class="btn btn-primary">Submit</button>
17
+ </form>`,
18
+ properties: {
19
+ successMessage: { type: 'string', dataAttribute: 'data-success-message' },
20
+ errorMessage: { type: 'string', dataAttribute: 'data-error-message' },
21
+ closeModalOnSuccess: { type: 'boolean', dataAttribute: 'data-close-modal-on-success' }
22
+ },
23
+ structure: {
24
+ container: 'form[data-component="form-validator"]',
25
+ children: {}
26
+ }
27
+ };
28
+
29
+ export const metadata = {
30
+ category: 'ui-component',
31
+ cssFile: 'components/forms.css',
32
+ engagementTracking: null,
33
+ emitsEvents: ['form:success', 'form:error']
34
+ };
35
+
36
+ import { showNotification } from './notifications.js';
37
+
38
+ export function init(form) {
39
+ if (!form || form.tagName !== 'FORM') {
40
+ throw new Error('FormValidator: Element must be a <form> tag.');
41
+ }
42
+
43
+ const successMessage = form.dataset.successMessage || 'Form submitted successfully!';
44
+ const errorMessage = form.dataset.errorMessage || 'Please fill in all required fields.';
45
+
46
+ const handleSubmit = (event) => {
47
+ event.preventDefault();
48
+
49
+ if (form.checkValidity()) {
50
+ showNotification(successMessage, 'success');
51
+ form.reset();
52
+
53
+ // Dispatch success event for other components to listen to
54
+ form.dispatchEvent(new CustomEvent('form:success', { bubbles: true }));
55
+
56
+ // Check if we should close a parent modal
57
+ if (form.dataset.closeModalOnSuccess === 'true') {
58
+ const closeBtn = form.closest('.modal')?.querySelector('[data-action="close-modal"]');
59
+ if (closeBtn) closeBtn.click();
60
+ }
61
+ } else {
62
+ showNotification(errorMessage, 'error');
63
+
64
+ // Dispatch error event
65
+ form.dispatchEvent(new CustomEvent('form:error', { bubbles: true }));
66
+ }
67
+ };
68
+
69
+ form.addEventListener('submit', handleSubmit);
70
+
71
+ return {
72
+ destroy: () => {
73
+ form.removeEventListener('submit', handleSubmit);
74
+ }
75
+ };
76
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Hero Layout Pattern
3
+ *
4
+ * CSS-only component - no JavaScript behavior, just schema for discovery and validation.
5
+ * Full-width impactful intro slides with background support and overlays.
6
+ */
7
+
8
+ export const schema = {
9
+ type: 'hero',
10
+ description: 'Full-width hero section with background image support',
11
+ example: `<div data-component="hero" class="hero-gradient">
12
+ <div class="hero-content">
13
+ <span class="hero-badge">New Course</span>
14
+ <h2 class="hero-title">Welcome to the Course</h2>
15
+ <p class="hero-subtitle">Learn everything you need to know.</p>
16
+ <div class="hero-cta"><button class="btn btn-primary">Get Started</button></div>
17
+ </div>
18
+ </div>`,
19
+ properties: {
20
+ bgImage: {
21
+ type: 'string',
22
+ description: 'Background image URL (data-bg-image attribute)'
23
+ },
24
+ variant: {
25
+ type: 'string',
26
+ enum: ['default', 'overlay', 'overlay-light', 'dark', 'gradient', 'split'],
27
+ default: 'default',
28
+ description: 'Visual style variant (add as class, e.g., hero-overlay)'
29
+ }
30
+ },
31
+ structure: {
32
+ container: '[data-component="hero"]',
33
+ children: {
34
+ content: { selector: '.hero-content' },
35
+ title: { selector: '.hero-title, h1' },
36
+ subtitle: { selector: '.hero-subtitle' },
37
+ cta: { selector: '.hero-cta' }
38
+ }
39
+ }
40
+ };
41
+
42
+ export const metadata = {
43
+ category: 'ui-component',
44
+ cssOnly: true,
45
+ cssFile: 'components/hero.css'
46
+ };
47
+
48
+ /** No-op initializer — CSS-only component, registered for consistency. */
49
+ export function init() {}
@@ -0,0 +1,12 @@
1
+ // index.js — UI Components shared utilities
2
+
3
+ import accessibilityManager from '../../managers/accessibility-manager.js';
4
+
5
+ /**
6
+ * Announces a message to a screen reader.
7
+ * @param {string} message - The message to announce.
8
+ * @param {string} [priority='polite'] - The assertiveness level ('polite' or 'assertive').
9
+ */
10
+ export function announceToScreenReader(message, priority = 'polite') {
11
+ accessibilityManager.announce(message, priority);
12
+ }