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.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/bin/cli.js +331 -0
- package/framework/assets/logo-coursecode-black.svg +14 -0
- package/framework/assets/logo-coursecode-white.svg +14 -0
- package/framework/assets/logo-coursecode.svg +14 -0
- package/framework/css/01-base.css +160 -0
- package/framework/css/02-layout.css +499 -0
- package/framework/css/accessibility.css +834 -0
- package/framework/css/components/accordions.css +710 -0
- package/framework/css/components/assessments.css +520 -0
- package/framework/css/components/audio-player.css +570 -0
- package/framework/css/components/badges.css +80 -0
- package/framework/css/components/breadcrumbs.css +87 -0
- package/framework/css/components/buttons.css +707 -0
- package/framework/css/components/callouts.css +1280 -0
- package/framework/css/components/cards.css +475 -0
- package/framework/css/components/carousel.css +193 -0
- package/framework/css/components/checkbox-group.css +123 -0
- package/framework/css/components/checklist.css +203 -0
- package/framework/css/components/collapse.css +96 -0
- package/framework/css/components/comparison.css +33 -0
- package/framework/css/components/content-image.css +36 -0
- package/framework/css/components/document-gallery.css +425 -0
- package/framework/css/components/dropdown.css +115 -0
- package/framework/css/components/embed-frame.css +142 -0
- package/framework/css/components/engagement.css +412 -0
- package/framework/css/components/features.css +35 -0
- package/framework/css/components/flip-cards.css +253 -0
- package/framework/css/components/footer.css +353 -0
- package/framework/css/components/forms.css +294 -0
- package/framework/css/components/hero.css +216 -0
- package/framework/css/components/images.css +528 -0
- package/framework/css/components/interactive-timeline.css +274 -0
- package/framework/css/components/intro-cards.css +30 -0
- package/framework/css/components/lightbox.css +666 -0
- package/framework/css/components/loading.css +65 -0
- package/framework/css/components/modals.css +235 -0
- package/framework/css/components/notifications.css +107 -0
- package/framework/css/components/quote.css +150 -0
- package/framework/css/components/sidebar.css +684 -0
- package/framework/css/components/slide-header.css +52 -0
- package/framework/css/components/spinner.css +62 -0
- package/framework/css/components/stats.css +44 -0
- package/framework/css/components/steps.css +232 -0
- package/framework/css/components/tables.css +90 -0
- package/framework/css/components/tabs.css +347 -0
- package/framework/css/components/timeline.css +154 -0
- package/framework/css/components/toggle.css +95 -0
- package/framework/css/components/tooltip.css +226 -0
- package/framework/css/components/video-player.css +438 -0
- package/framework/css/design-tokens.css +707 -0
- package/framework/css/framework.css +86 -0
- package/framework/css/interactions/accessibility.css +75 -0
- package/framework/css/interactions/base.css +92 -0
- package/framework/css/interactions/drag-drop.css +295 -0
- package/framework/css/interactions/fill-in-the-blank.css +236 -0
- package/framework/css/interactions/hotspots.css +69 -0
- package/framework/css/interactions/index.css +45 -0
- package/framework/css/interactions/interactive-image.css +359 -0
- package/framework/css/interactions/likert.css +126 -0
- package/framework/css/interactions/matching.css +354 -0
- package/framework/css/interactions/numeric-input.css +78 -0
- package/framework/css/interactions/sequencing.css +378 -0
- package/framework/css/interactions/true-false.css +177 -0
- package/framework/css/layouts/article.css +258 -0
- package/framework/css/layouts/base.css +30 -0
- package/framework/css/layouts/canvas.css +38 -0
- package/framework/css/layouts/focused.css +236 -0
- package/framework/css/layouts/index.css +29 -0
- package/framework/css/layouts/presentation.css +191 -0
- package/framework/css/layouts/traditional.css +52 -0
- package/framework/css/responsive.css +439 -0
- package/framework/css/utilities/accessibility-utils.css +59 -0
- package/framework/css/utilities/animations.css +419 -0
- package/framework/css/utilities/borders.css +72 -0
- package/framework/css/utilities/colors.css +76 -0
- package/framework/css/utilities/container.css +46 -0
- package/framework/css/utilities/decorative.css +442 -0
- package/framework/css/utilities/display.css +257 -0
- package/framework/css/utilities/flexbox.css +80 -0
- package/framework/css/utilities/grid.css +69 -0
- package/framework/css/utilities/icons.css +534 -0
- package/framework/css/utilities/lists.css +190 -0
- package/framework/css/utilities/spacing.css +167 -0
- package/framework/css/utilities/tables.css +81 -0
- package/framework/css/utilities/typography.css +159 -0
- package/framework/css/utilities/visibility.css +117 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
- package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
- package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
- package/framework/docs/DATA_MODEL.md +409 -0
- package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
- package/framework/docs/USER_GUIDE.md +583 -0
- package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
- package/framework/docs/examples/cloudflare-data-worker.js +102 -0
- package/framework/docs/examples/cloudflare-error-worker.js +228 -0
- package/framework/index.html +175 -0
- package/framework/js/app/AppActions.js +410 -0
- package/framework/js/app/AppState.js +225 -0
- package/framework/js/app/AppUI.js +616 -0
- package/framework/js/assessment/AssessmentActions.js +615 -0
- package/framework/js/assessment/AssessmentFactory.js +471 -0
- package/framework/js/assessment/AssessmentState.js +322 -0
- package/framework/js/assessment/AssessmentUI.js +451 -0
- package/framework/js/automation/api-engagement.js +196 -0
- package/framework/js/automation/api-interactions.js +167 -0
- package/framework/js/automation/api.js +242 -0
- package/framework/js/automation/index.js +41 -0
- package/framework/js/components/interactions/drag-drop.js +884 -0
- package/framework/js/components/interactions/fill-in.js +535 -0
- package/framework/js/components/interactions/hotspot.js +702 -0
- package/framework/js/components/interactions/interaction-base.js +511 -0
- package/framework/js/components/interactions/likert.js +301 -0
- package/framework/js/components/interactions/matching.js +699 -0
- package/framework/js/components/interactions/multiple-choice.js +377 -0
- package/framework/js/components/interactions/numeric.js +271 -0
- package/framework/js/components/interactions/sequencing.js +423 -0
- package/framework/js/components/interactions/true-false.js +241 -0
- package/framework/js/components/ui-components/accordion.js +442 -0
- package/framework/js/components/ui-components/alert.js +88 -0
- package/framework/js/components/ui-components/audio-player.js +1193 -0
- package/framework/js/components/ui-components/callout.js +121 -0
- package/framework/js/components/ui-components/carousel.js +145 -0
- package/framework/js/components/ui-components/checkbox-group.js +87 -0
- package/framework/js/components/ui-components/checklist.js +40 -0
- package/framework/js/components/ui-components/collapse.js +114 -0
- package/framework/js/components/ui-components/comparison.js +30 -0
- package/framework/js/components/ui-components/conditional-display.js +150 -0
- package/framework/js/components/ui-components/content-image.js +41 -0
- package/framework/js/components/ui-components/dropdown.js +262 -0
- package/framework/js/components/ui-components/embed-frame.js +274 -0
- package/framework/js/components/ui-components/features.js +33 -0
- package/framework/js/components/ui-components/flip-card.js +230 -0
- package/framework/js/components/ui-components/form-validator.js +76 -0
- package/framework/js/components/ui-components/hero.js +49 -0
- package/framework/js/components/ui-components/index.js +12 -0
- package/framework/js/components/ui-components/interactive-image.js +235 -0
- package/framework/js/components/ui-components/interactive-timeline.js +285 -0
- package/framework/js/components/ui-components/intro-cards.js +35 -0
- package/framework/js/components/ui-components/lightbox.js +652 -0
- package/framework/js/components/ui-components/modal.js +386 -0
- package/framework/js/components/ui-components/notifications.js +145 -0
- package/framework/js/components/ui-components/progress.js +88 -0
- package/framework/js/components/ui-components/quote.js +41 -0
- package/framework/js/components/ui-components/stats.js +33 -0
- package/framework/js/components/ui-components/steps.js +41 -0
- package/framework/js/components/ui-components/tabs.js +255 -0
- package/framework/js/components/ui-components/timeline.js +42 -0
- package/framework/js/components/ui-components/toggle-group.js +73 -0
- package/framework/js/components/ui-components/tooltip.js +458 -0
- package/framework/js/components/ui-components/value-display.js +133 -0
- package/framework/js/components/ui-components/video-player.js +686 -0
- package/framework/js/core/component-catalog.js +121 -0
- package/framework/js/core/event-bus.js +178 -0
- package/framework/js/core/interaction-catalog.js +149 -0
- package/framework/js/dev/runtime-linter.js +1725 -0
- package/framework/js/drivers/cmi5-driver.js +768 -0
- package/framework/js/drivers/driver-factory.js +77 -0
- package/framework/js/drivers/driver-interface.js +110 -0
- package/framework/js/drivers/http-driver-base.js +241 -0
- package/framework/js/drivers/lti-driver.js +508 -0
- package/framework/js/drivers/proxy-driver.js +444 -0
- package/framework/js/drivers/scorm-12-driver.js +560 -0
- package/framework/js/drivers/scorm-2004-driver.js +775 -0
- package/framework/js/drivers/scorm-driver-base.js +112 -0
- package/framework/js/engagement/engagement-manager.js +404 -0
- package/framework/js/engagement/engagement-progress.js +191 -0
- package/framework/js/engagement/engagement-trackers.js +215 -0
- package/framework/js/engagement/requirement-strategies.js +268 -0
- package/framework/js/main.js +727 -0
- package/framework/js/managers/accessibility-manager.js +499 -0
- package/framework/js/managers/assessment-manager.js +230 -0
- package/framework/js/managers/audio-manager.js +944 -0
- package/framework/js/managers/comment-manager.js +88 -0
- package/framework/js/managers/flag-manager.js +86 -0
- package/framework/js/managers/interaction-manager.js +254 -0
- package/framework/js/managers/interaction-registry.js +96 -0
- package/framework/js/managers/objective-manager.js +423 -0
- package/framework/js/managers/score-manager.js +441 -0
- package/framework/js/managers/video-manager.js +536 -0
- package/framework/js/navigation/Breadcrumbs.js +234 -0
- package/framework/js/navigation/NavigationActions.js +1132 -0
- package/framework/js/navigation/NavigationState.js +276 -0
- package/framework/js/navigation/NavigationUI.js +574 -0
- package/framework/js/navigation/document-gallery.js +357 -0
- package/framework/js/navigation/navigation-helpers.js +175 -0
- package/framework/js/navigation/navigation-validators.js +174 -0
- package/framework/js/state/index.js +8 -0
- package/framework/js/state/lms-connection.js +482 -0
- package/framework/js/state/lms-error-utils.js +58 -0
- package/framework/js/state/state-commits.js +200 -0
- package/framework/js/state/state-domains.js +86 -0
- package/framework/js/state/state-manager.js +502 -0
- package/framework/js/state/state-validation.js +311 -0
- package/framework/js/state/transaction-log.js +41 -0
- package/framework/js/state/xapi-statement-service.js +325 -0
- package/framework/js/utilities/access-control.js +99 -0
- package/framework/js/utilities/breakpoint-manager.js +315 -0
- package/framework/js/utilities/canvas-slide.js +35 -0
- package/framework/js/utilities/conditional-display.js +388 -0
- package/framework/js/utilities/course-channel.js +214 -0
- package/framework/js/utilities/course-helpers.js +420 -0
- package/framework/js/utilities/data-reporter.js +273 -0
- package/framework/js/utilities/error-reporter.js +313 -0
- package/framework/js/utilities/hotspot-helper.js +341 -0
- package/framework/js/utilities/icons.js +348 -0
- package/framework/js/utilities/logger.js +92 -0
- package/framework/js/utilities/markdown-renderer.js +45 -0
- package/framework/js/utilities/scroll-tracker.js +68 -0
- package/framework/js/utilities/ui-initializer.js +146 -0
- package/framework/js/utilities/utilities.js +293 -0
- package/framework/js/utilities/view-manager.js +227 -0
- package/framework/js/validation/html-validators.js +422 -0
- package/framework/js/validation/scorm-validators.js +438 -0
- package/framework/js/vendor/pipwerks.js +931 -0
- package/framework/scripts/generate-narration.js +629 -0
- package/framework/scripts/tts-providers/azure-provider.js +178 -0
- package/framework/scripts/tts-providers/base-provider.js +81 -0
- package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
- package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
- package/framework/scripts/tts-providers/google-provider.js +272 -0
- package/framework/scripts/tts-providers/index.js +158 -0
- package/framework/scripts/tts-providers/openai-provider.js +143 -0
- package/framework/version.json +63 -0
- package/lib/authoring-api.js +919 -0
- package/lib/build-linter.js +450 -0
- package/lib/build-packaging.js +186 -0
- package/lib/build.js +88 -0
- package/lib/cloud.js +691 -0
- package/lib/convert.js +341 -0
- package/lib/course-parser.js +936 -0
- package/lib/course-writer.js +258 -0
- package/lib/create.js +248 -0
- package/lib/css-index.js +237 -0
- package/lib/dev.js +51 -0
- package/lib/export-content.js +1246 -0
- package/lib/headless-browser.js +413 -0
- package/lib/import.js +377 -0
- package/lib/index.js +80 -0
- package/lib/info.js +79 -0
- package/lib/interaction-formatters.js +568 -0
- package/lib/manifest/cmi5-manifest.js +63 -0
- package/lib/manifest/lti-tool-config.js +53 -0
- package/lib/manifest/manifest-factory.js +99 -0
- package/lib/manifest/scorm-12-manifest.js +61 -0
- package/lib/manifest/scorm-2004-manifest.js +94 -0
- package/lib/manifest/scorm-proxy-manifest.js +104 -0
- package/lib/manifest-parser.js +96 -0
- package/lib/mcp-prompts.js +753 -0
- package/lib/mcp-server.js +316 -0
- package/lib/narration.js +53 -0
- package/lib/pdf-structure.js +142 -0
- package/lib/preview-export.js +231 -0
- package/lib/preview-routes-api.js +662 -0
- package/lib/preview-routes-editing.js +159 -0
- package/lib/preview-routes-lms.js +230 -0
- package/lib/preview-server.js +564 -0
- package/lib/project-utils.js +269 -0
- package/lib/proxy-templates/proxy.html +68 -0
- package/lib/proxy-templates/scorm-bridge.js +112 -0
- package/lib/scaffold.js +193 -0
- package/lib/schema-extractor.js +361 -0
- package/lib/slide-source-editor.js +586 -0
- package/lib/stub-player/app-viewer.js +195 -0
- package/lib/stub-player/app.js +370 -0
- package/lib/stub-player/catalog-panel.js +312 -0
- package/lib/stub-player/config-panel.js +1303 -0
- package/lib/stub-player/content-generator.js +586 -0
- package/lib/stub-player/content-viewer.js +173 -0
- package/lib/stub-player/debug-panel.js +420 -0
- package/lib/stub-player/edit-mode.js +922 -0
- package/lib/stub-player/edit-utils.js +400 -0
- package/lib/stub-player/header-bar.js +354 -0
- package/lib/stub-player/interaction-editor.js +210 -0
- package/lib/stub-player/interactions-panel.js +565 -0
- package/lib/stub-player/lms-api.js +1094 -0
- package/lib/stub-player/login-screen.js +74 -0
- package/lib/stub-player/outline-mode.js +689 -0
- package/lib/stub-player/styles/_assessments-panel.css +245 -0
- package/lib/stub-player/styles/_base.css +89 -0
- package/lib/stub-player/styles/_catalog-icons.css +96 -0
- package/lib/stub-player/styles/_catalog-panel.css +291 -0
- package/lib/stub-player/styles/_config-panel.css +636 -0
- package/lib/stub-player/styles/_content-viewer.css +834 -0
- package/lib/stub-player/styles/_debug-panel.css +576 -0
- package/lib/stub-player/styles/_edit-mode.css +128 -0
- package/lib/stub-player/styles/_header-bar.css +343 -0
- package/lib/stub-player/styles/_interaction-editor.css +140 -0
- package/lib/stub-player/styles/_interactions-panel.css +1038 -0
- package/lib/stub-player/styles/_login-screen.css +102 -0
- package/lib/stub-player/styles/_outline-mode.css +752 -0
- package/lib/stub-player/styles.css +15 -0
- package/lib/stub-player.js +160 -0
- package/lib/test-data-reporting.js +176 -0
- package/lib/test-error-reporting.js +146 -0
- package/lib/token.js +86 -0
- package/lib/upgrade.js +257 -0
- package/lib/validation-rules.js +517 -0
- package/lib/vite-plugin-content-discovery.js +296 -0
- package/package.json +108 -0
- package/schemas/XMLSchema.dtd +402 -0
- package/schemas/adlcp_v1p3.xsd +111 -0
- package/schemas/adlnav_v1p3.xsd +61 -0
- package/schemas/adlseq_v1p3.xsd +93 -0
- package/schemas/common/anyElement.xsd +27 -0
- package/schemas/common/dataTypes.xsd +138 -0
- package/schemas/common/elementNames.xsd +767 -0
- package/schemas/common/elementTypes.xsd +786 -0
- package/schemas/common/rootElement.xsd +31 -0
- package/schemas/common/vocabTypes.xsd +345 -0
- package/schemas/common/vocabValues.xsd +257 -0
- package/schemas/datatypes.dtd +203 -0
- package/schemas/ims_xml.xsd +35 -0
- package/schemas/imscp_v1p1.xsd +368 -0
- package/schemas/imsss_v1p0.xsd +67 -0
- package/schemas/imsss_v1p0auxresource.xsd +19 -0
- package/schemas/imsss_v1p0control.xsd +20 -0
- package/schemas/imsss_v1p0delivery.xsd +17 -0
- package/schemas/imsss_v1p0limit.xsd +47 -0
- package/schemas/imsss_v1p0objective.xsd +67 -0
- package/schemas/imsss_v1p0random.xsd +16 -0
- package/schemas/imsss_v1p0rollup.xsd +46 -0
- package/schemas/imsss_v1p0seqrule.xsd +108 -0
- package/schemas/imsss_v1p0util.xsd +94 -0
- package/schemas/license.txt +17 -0
- package/schemas/lom.xsd +102 -0
- package/schemas/lomCustom.xsd +62 -0
- package/schemas/lomLoose.xsd +62 -0
- package/schemas/lomStrict.xsd +62 -0
- package/schemas/xml.xsd +81 -0
- package/template/.env.example +92 -0
- package/template/course/assets/audio/example-intro.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
- package/template/course/assets/docs/example_md_1.md +39 -0
- package/template/course/assets/docs/example_md_2.md +41 -0
- package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
- package/template/course/assets/docs/example_pdf_2.pdf +0 -0
- package/template/course/assets/images/course-architecture.svg +36 -0
- package/template/course/assets/images/logo.svg +14 -0
- package/template/course/assets/widgets/counter-demo.html +190 -0
- package/template/course/assets/widgets/gravity-painter.html +384 -0
- package/template/course/course-config.js +539 -0
- package/template/course/icons.js +19 -0
- package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
- package/template/course/slides/example-course-structure.js +138 -0
- package/template/course/slides/example-final-exam.js +144 -0
- package/template/course/slides/example-finishing.js +127 -0
- package/template/course/slides/example-interactions-showcase.js +615 -0
- package/template/course/slides/example-preview-tour.js +129 -0
- package/template/course/slides/example-remedial.js +143 -0
- package/template/course/slides/example-summary.js +103 -0
- package/template/course/slides/example-ui-showcase.js +1805 -0
- package/template/course/slides/example-welcome.js +123 -0
- package/template/course/slides/example-workflow.js +140 -0
- package/template/course/theme.css +165 -0
- package/template/eslint.config.js +47 -0
- package/template/package.json +28 -0
- package/template/vite.config.js +339 -0
|
@@ -0,0 +1,1088 @@
|
|
|
1
|
+
# SCORM Framework Development Guide
|
|
2
|
+
|
|
3
|
+
> **Intended Audience: AI Agents** — This document is a machine-readable reference for AI agents developing the framework itself. For human-readable documentation, see `USER_GUIDE.md`.
|
|
4
|
+
|
|
5
|
+
**Related Docs:**
|
|
6
|
+
- `COURSE_AUTHORING_GUIDE.md` - For course authors (not framework devs)
|
|
7
|
+
|
|
8
|
+
- `DATA_MODEL.md` - Complete learner data schemas and storage architecture
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Development Commands (Framework Repo)
|
|
13
|
+
|
|
14
|
+
The framework source repo has a different structure than course projects created with `coursecode create`. Use these commands from the repo root:
|
|
15
|
+
|
|
16
|
+
| Command | Purpose | Output |
|
|
17
|
+
|---------|---------|--------|
|
|
18
|
+
| `npm run dev` | Build watch only (no server) | `dist/` |
|
|
19
|
+
| `npm run preview` | **Stub LMS player + build watch + live reload** | `dist/` + server on :4173 |
|
|
20
|
+
| `npm run preview:scorm12` | Preview with SCORM 1.2 format | `dist/` + server on :4173 |
|
|
21
|
+
| `npm run preview:cmi5` | Preview with cmi5 format | `dist/` + server on :4173 |
|
|
22
|
+
| `npm run build` | One-time development build | `dist/` |
|
|
23
|
+
| `npm run build:scorm2004` | Build SCORM 2004 package | `dist/` |
|
|
24
|
+
| `npm run build:scorm12` | Build SCORM 1.2 package | `dist/` |
|
|
25
|
+
| `npm run build:cmi5` | Build cmi5 package | `dist/` |
|
|
26
|
+
| `npm run build:lti` | Build LTI 1.3 package | `dist/` |
|
|
27
|
+
|
|
28
|
+
### Quick Start for Framework Development
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Start the preview server with stub LMS
|
|
32
|
+
npm run preview
|
|
33
|
+
|
|
34
|
+
# Opens http://localhost:4173 with:
|
|
35
|
+
# - Stub SCORM API (localStorage persistence)
|
|
36
|
+
# - Live reload on file changes
|
|
37
|
+
# - Debug panel for API inspection with LMS Compatibility Warnings
|
|
38
|
+
# - Content viewer for course review
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### LMS Compatibility Warnings
|
|
42
|
+
The preview server (stub player) includes an advanced diagnostic system that monitors API usage and data limits to detect potential issues before deployment to a real LMS.
|
|
43
|
+
|
|
44
|
+
It flags issues with color-coded badges in the debug panel:
|
|
45
|
+
- **Red Error Badge**: API misuse (e.g., calling `GetValue` before `Initialize`, `Terminate` errors)
|
|
46
|
+
- **Yellow Warning Badge**: Potential compatibility issues (e.g., suspend data exceeding 4KB/64KB limits)
|
|
47
|
+
|
|
48
|
+
Check the **Errors** tab in the debug panel to see detailed logs and remediation steps.
|
|
49
|
+
|
|
50
|
+
### Multi-Format Support
|
|
51
|
+
|
|
52
|
+
The framework supports multiple LMS formats:
|
|
53
|
+
|
|
54
|
+
| Format | Description | Limit |
|
|
55
|
+
|--------|-------------|-------|
|
|
56
|
+
| `cmi5` | cmi5/xAPI (default) | Unlimited |
|
|
57
|
+
| `scorm2004` | SCORM 2004 4th Edition | 64KB suspend_data |
|
|
58
|
+
| `scorm1.2` | SCORM 1.2 (legacy LMS) | 4KB suspend_data (Strict Diet Mode found in `scorm-12-driver.js`) |
|
|
59
|
+
| `lti` | LTI 1.3 (remote-hosted, JWT launch) | Unlimited (host-dependent) |
|
|
60
|
+
|
|
61
|
+
> **Note on SCORM 1.2 Strict Diet Mode:** To stay within the 4KB limit, this mode only persists interaction responses for the *current* slide. If a user navigates away and returns, their previous answers on other slides may not be restored visually, although their completion status/score for assessments is always preserved.
|
|
62
|
+
|
|
63
|
+
**Set format in `course-config.js`** (for local CLI builds):
|
|
64
|
+
```javascript
|
|
65
|
+
export const courseConfig = {
|
|
66
|
+
// format: 'cmi5', // Default is 'cmi5'. Options: 'scorm2004' | 'scorm1.2' | 'lti'
|
|
67
|
+
// ... rest of config
|
|
68
|
+
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Or override via CLI:**
|
|
72
|
+
```bash
|
|
73
|
+
# Environment variable
|
|
74
|
+
LMS_FORMAT=scorm1.2 npm run preview
|
|
75
|
+
|
|
76
|
+
# Build script
|
|
77
|
+
npm run build:scorm12
|
|
78
|
+
npm run build:lti
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> **Cloud courses ignore this setting.** When a course is deployed to CourseCode Cloud, the format in `course-config.js` is irrelevant — the cloud uses the universal build to generate ZIPs for any format on demand by re-stamping the meta tag and generating the appropriate manifest. Authors never need to choose a format for cloud-deployed courses.
|
|
82
|
+
|
|
83
|
+
### Universal Build Architecture
|
|
84
|
+
|
|
85
|
+
The framework produces a **universal build** — a single `dist/` output that contains all LMS drivers as lazy-loaded chunks. The active format is determined at **runtime**, not build time. This enables:
|
|
86
|
+
|
|
87
|
+
- **One build, any format**: `dist/` works with cmi5, SCORM 2004, SCORM 1.2, or LTI without rebuilding
|
|
88
|
+
- **Cloud-ready**: Platforms can serve any format from a single upload by re-stamping a meta tag
|
|
89
|
+
- **No user code execution**: Format-specific ZIPs can be assembled using only framework utilities (no Vite, no course JS)
|
|
90
|
+
|
|
91
|
+
#### How Runtime Format Detection Works
|
|
92
|
+
|
|
93
|
+
`lms-connection.js` resolves the active format via a priority chain:
|
|
94
|
+
|
|
95
|
+
| Priority | Source | Purpose |
|
|
96
|
+
|----------|--------|---------|
|
|
97
|
+
| 1 | `<meta name="lms-format">` tag in HTML | Primary — stamped at build time, re-stampable by cloud/CI |
|
|
98
|
+
| 2 | `import.meta.env.LMS_FORMAT` (Vite define) | Fallback for preview server / dev builds |
|
|
99
|
+
| 3 | `'cmi5'` | Default |
|
|
100
|
+
|
|
101
|
+
The meta tag is injected into `dist/index.html` during the post-build step. Its value comes from `course-config.js` (or the `LMS_FORMAT` env var override). Any platform can re-stamp it with a simple HTML string replacement — no build tools needed.
|
|
102
|
+
|
|
103
|
+
#### Driver Bundling
|
|
104
|
+
|
|
105
|
+
All drivers use `await import()` in `driver-factory.js`. Vite emits each as a separate lazy chunk:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
dist/assets/
|
|
109
|
+
main.js ← Framework entry (shared across all formats)
|
|
110
|
+
scorm-2004-driver.js ← Lazy chunk, only fetched if format = 'scorm2004'
|
|
111
|
+
scorm-12-driver.js ← Lazy chunk, only fetched if format = 'scorm1.2'
|
|
112
|
+
cmi5-driver.js ← Lazy chunk, only fetched if format = 'cmi5'
|
|
113
|
+
lti-driver.js ← Lazy chunk, only fetched if format = 'lti'
|
|
114
|
+
proxy-driver.js ← Lazy chunk, only fetched if format = '*-proxy'
|
|
115
|
+
jose.js ← Lazy chunk, only fetched by lti-driver.js
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The browser only downloads the one chunk matching the meta tag. Unused driver chunks sit on disk (~20-30 KB each) and are never requested.
|
|
119
|
+
|
|
120
|
+
#### Build Outputs by Scenario
|
|
121
|
+
|
|
122
|
+
| Command | Output | Contains |
|
|
123
|
+
|---------|--------|---------|
|
|
124
|
+
| `coursecode build` | `dist/` | Universal build + format manifest + meta tag stamped |
|
|
125
|
+
| `coursecode build` (with `PACKAGE=true`) | `dist/` + ZIP | Same + format-specific ZIP for LMS upload |
|
|
126
|
+
| `coursecode preview --export` | `course-preview/` | Copy of `dist/` wrapped in stub player (for Netlify/GitHub Pages) |
|
|
127
|
+
| `coursecode deploy` | Uploads `dist/` | Cloud hosts universal build, assembles format ZIPs on demand |
|
|
128
|
+
|
|
129
|
+
The ZIP never includes preview/stub player assets. Preview is a separate concern (see below).
|
|
130
|
+
|
|
131
|
+
#### Re-Stamping for Different Formats
|
|
132
|
+
|
|
133
|
+
`lib/build-packaging.js` exports `stampFormatInHtml(htmlPath, format)` for re-stamping the meta tag:
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
import { stampFormatInHtml } from 'coursecode/build-packaging';
|
|
137
|
+
import { generateManifest } from 'coursecode/manifest';
|
|
138
|
+
|
|
139
|
+
// Re-stamp dist/index.html for a different format
|
|
140
|
+
stampFormatInHtml('/path/to/dist/index.html', 'scorm2004');
|
|
141
|
+
|
|
142
|
+
// Generate the format-specific manifest
|
|
143
|
+
const { filename, content } = generateManifest('scorm2004', config, files, options);
|
|
144
|
+
fs.writeFileSync(`/path/to/dist/${filename}`, content);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Both are pure Node utilities
|
|
148
|
+
|
|
149
|
+
#### Key Files
|
|
150
|
+
|
|
151
|
+
| File | Purpose |
|
|
152
|
+
|------|---------|
|
|
153
|
+
| `framework/js/state/lms-connection.js` | `getLMSFormat()` — runtime priority chain (meta → env → default) |
|
|
154
|
+
| `framework/js/drivers/driver-factory.js` | Dynamic `import()` switch — loads one driver at runtime |
|
|
155
|
+
| `lib/build-packaging.js` | `stampFormatInHtml()` — meta tag re-stamping utility |
|
|
156
|
+
| `lib/manifest/manifest-factory.js` | `generateManifest()` — generates format-specific manifests |
|
|
157
|
+
| Both `vite.config.js` files | Post-build `closeBundle` hook stamps meta tag into `dist/index.html` |
|
|
158
|
+
|
|
159
|
+
### cmi5 xAPI Features
|
|
160
|
+
|
|
161
|
+
When using cmi5 format, the framework automatically sends rich xAPI statements:
|
|
162
|
+
|
|
163
|
+
| Event | xAPI Verb | Data |
|
|
164
|
+
|-------|-----------|------|
|
|
165
|
+
| Slide navigation | `experienced` | Slide ID, time spent |
|
|
166
|
+
| Interaction answered | `answered` | Response, result, duration |
|
|
167
|
+
| Objective updated | `completed`/`passed`/`failed` | Objective ID, score |
|
|
168
|
+
| Assessment submitted | `completed` | Score, pass/fail, duration, attempt# |
|
|
169
|
+
|
|
170
|
+
**LMS Launch Data:** cmi5 exposes `masteryScore` and `moveOn` from LMS launch parameters. If the LMS sets `masteryScore`, it overrides the course's configured `passingScore` for assessments.
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
// Access launch data programmatically (cmi5 only)
|
|
174
|
+
const launchData = stateManager.getLaunchData();
|
|
175
|
+
// Returns: { moveOn, masteryScore, launchMode, activityId, registration }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### External Hosting (CDN Deployment)
|
|
179
|
+
|
|
180
|
+
For hot-fixable courses, the framework supports **external hosting**: course content lives on a CDN while a minimal proxy package is uploaded to the LMS.
|
|
181
|
+
|
|
182
|
+
| Format | Description | Use Case |
|
|
183
|
+
|--------|-------------|----------|
|
|
184
|
+
| `scorm1.2-proxy` | SCORM 1.2 proxy | Legacy LMS, CDN hosting |
|
|
185
|
+
| `scorm2004-proxy` | SCORM 2004 proxy | Modern SCORM LMS, CDN hosting |
|
|
186
|
+
| `cmi5-remote` | cmi5 with absolute AU URL | Modern LMS/LRS, CDN hosting |
|
|
187
|
+
|
|
188
|
+
**Configuration:**
|
|
189
|
+
```javascript
|
|
190
|
+
// course-config.js
|
|
191
|
+
export const courseConfig = {
|
|
192
|
+
format: 'scorm1.2-proxy',
|
|
193
|
+
externalUrl: 'https://cdn.example.com/my-course', // Required
|
|
194
|
+
accessControl: { // Required for proxy/remote formats
|
|
195
|
+
clients: {
|
|
196
|
+
'acme-corp': { token: 'abc123' },
|
|
197
|
+
'globex': { token: 'def456' }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Generate tokens:**
|
|
204
|
+
```bash
|
|
205
|
+
coursecode token # Generate random token
|
|
206
|
+
coursecode token --add acme-corp # Add client to config
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Build outputs:**
|
|
210
|
+
- `dist/` — Deploy to CDN
|
|
211
|
+
- `*_acme-corp_proxy.zip`, `*_globex_proxy.zip` — One package per client
|
|
212
|
+
|
|
213
|
+
**Access Control:**
|
|
214
|
+
- Tokens are injected into URLs: `https://cdn.example.com/my-course?clientId=acme-corp&token=abc123`
|
|
215
|
+
- Runtime validation in `access-control.js` blocks unauthorized access
|
|
216
|
+
- To disable a client: remove from config → redeploy CDN
|
|
217
|
+
|
|
218
|
+
**Architecture:**
|
|
219
|
+
- **Proxy formats**: LMS loads a lightweight proxy (proxy.html + bridge) that iframes the CDN-hosted course. The bridge relays `postMessage` calls to the LMS SCORM API via pipwerks.
|
|
220
|
+
- **cmi5-remote**: The cmi5 manifest's AU URL points directly to the CDN. Course communicates with LRS via xAPI (no iframe/bridge).
|
|
221
|
+
|
|
222
|
+
**Key files:**
|
|
223
|
+
| File | Purpose |
|
|
224
|
+
|------|---------|
|
|
225
|
+
| `framework/js/drivers/proxy-driver.js` | Course-side postMessage LMSDriver |
|
|
226
|
+
| `framework/js/utilities/access-control.js` | Token validation + unauthorized screen |
|
|
227
|
+
| `lib/proxy-templates/proxy.html` | Proxy package entry point |
|
|
228
|
+
| `lib/proxy-templates/scorm-bridge.js` | postMessage ↔ pipwerks bridge |
|
|
229
|
+
| `lib/token.js` | CLI token generator |
|
|
230
|
+
|
|
231
|
+
**Preview mode**: Proxy/remote suffixes are stripped (e.g., `scorm1.2-proxy` → `scorm1.2`) so the stub LMS works normally.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Development Roles & Testing Tools
|
|
236
|
+
|
|
237
|
+
| Role | Testing Tool | Why |
|
|
238
|
+
|------|--------------|-----|
|
|
239
|
+
| **Framework Developers** | `npm run preview` (stub server + example course) | Test framework changes against `template/course/` with full LMS simulation |
|
|
240
|
+
| **Course Authors** | `coursecode preview` (stub server) | Fast iteration on content, CSS, gating, interactions |
|
|
241
|
+
|
|
242
|
+
**Framework developers** modify code in `framework/` that directly interfaces with the LMS via drivers. The preview server with the example course in `template/course/` provides a complete testing environment for validating driver behavior, state management, and component functionality.
|
|
243
|
+
|
|
244
|
+
**Course authors** only edit `course/` files and never touch LMS APIs directly. The framework abstracts all persistence, so the stub server's simple key-value storage is sufficient for testing content, styling, engagement tracking, and assessment logic.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## AI Agent Rules
|
|
249
|
+
|
|
250
|
+
**Development & Testing Environment:**
|
|
251
|
+
- Use `npm run preview` for framework development with stub LMS
|
|
252
|
+
- **CRITICAL: Watch Vite Build Warnings!** Errors like "X is not exported by Y" mean code WILL fail at runtime with "(void 0) is not a function". Always check build output for export/import mismatches.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Golden Rules
|
|
257
|
+
|
|
258
|
+
1. **NEVER call `window.doSetValue/doGetValue` directly** - use `stateManager` methods only
|
|
259
|
+
2. **ALWAYS use `stateManager`** - single source of truth for persistence
|
|
260
|
+
3. **TIERED ERROR HANDLING** - Tier 1 (contract violations) always throw; Tier 2 (runtime/init) use `logger.fatal()` → throws in DEV, degrades in PROD
|
|
261
|
+
4. **NEVER use `console.*` directly** - use `logger.*` instead (enforced by build linter)
|
|
262
|
+
5. **State-UI-Actions pattern** - for complex components
|
|
263
|
+
6. **ES Modules** - `import/export`, `const/let` (no `var`)
|
|
264
|
+
7. **Standardized errors** - ALWAYS emit `{ domain, operation, message, stack, context }` on events ending in `:error`
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Directory Structure
|
|
269
|
+
|
|
270
|
+
| Path | Purpose |
|
|
271
|
+
|------|---------|
|
|
272
|
+
| `framework/js/app/` | Global lifecycle, UI (modals, notifications), state |
|
|
273
|
+
| `framework/js/core/` | Core services: `EventBus`, `runtime` |
|
|
274
|
+
| `framework/js/drivers/` | LMS format drivers: SCORM 2004, SCORM 1.2, cmi5, LTI |
|
|
275
|
+
| `framework/js/vendor/` | Third-party libs (`pipwerks.js`) |
|
|
276
|
+
| `framework/js/state/` | State management facade, LMS connection, xAPI service (see `DATA_MODEL.md`) |
|
|
277
|
+
| `framework/js/managers/` | Feature managers: objectives, interactions, engagement, accessibility, etc. |
|
|
278
|
+
| `framework/js/components/` | Reusable UI (tabs, dropdowns) & interactions (MCQ, drag-drop) |
|
|
279
|
+
| `framework/js/navigation/` | Navigation: menu, buttons, document gallery |
|
|
280
|
+
| `framework/js/utilities/` | Helpers (incl. `validation-helpers.js` for SCORM 2004 4E validation) |
|
|
281
|
+
| `framework/js/dev/` | Dev-only code (linter, automation API) - tree-shaken in prod |
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Architecture Patterns
|
|
286
|
+
|
|
287
|
+
### State-UI-Actions Pattern
|
|
288
|
+
|
|
289
|
+
Separates data, presentation, logic (used in `app/`, `navigation/`):
|
|
290
|
+
|
|
291
|
+
- **State (`*-State.js`)**: In-memory data container. No DOM, no `stateManager` access
|
|
292
|
+
- **UI (`*-UI.js`)**: DOM manipulation only. No internal state
|
|
293
|
+
- **Actions (`*-Actions.js`)**: Orchestrates: handles input → updates State → directs UI → calls managers
|
|
294
|
+
|
|
295
|
+
### ViewManager
|
|
296
|
+
|
|
297
|
+
- **Single ViewManager** in `main.js` controls slide navigation
|
|
298
|
+
- **No caching** - views render fresh each `showView()` to prevent stale data
|
|
299
|
+
- Slide signature: `render(root, context)` where `root` is framework-provided container
|
|
300
|
+
- Components with sub-views (assessments) create own ViewManager
|
|
301
|
+
|
|
302
|
+
### Event Delegation
|
|
303
|
+
|
|
304
|
+
- Use `data-action="action-name"` attributes
|
|
305
|
+
- Single listener on container delegates based on attribute
|
|
306
|
+
- Works with ViewManager's dynamic rendering
|
|
307
|
+
|
|
308
|
+
### Course Writer (`lib/course-writer.js`)
|
|
309
|
+
|
|
310
|
+
Unified write operations for course data. All edits go through one endpoint:
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
POST /__write
|
|
314
|
+
{ "target": "config", "id": "navigation.sidebar.enabled", "value": true }
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
| Target | What it edits | `id` format |
|
|
318
|
+
|--------|---------------|-------------|
|
|
319
|
+
| `config` | `course-config.js` properties | Dot-notation path |
|
|
320
|
+
| `slide` | Slide config in structure array | `slideId` |
|
|
321
|
+
| `objective` | Objective in objectives array | `objectiveId` |
|
|
322
|
+
| `gating` | Gating conditions for slide | `slideId` |
|
|
323
|
+
|
|
324
|
+
**Process**: Import config as object → modify property at path → serialize → write back. No regex.
|
|
325
|
+
|
|
326
|
+
### PowerPoint Import (`lib/import.js`)
|
|
327
|
+
|
|
328
|
+
Converts `.pptx` to a CourseCode project. Two paths for slide image acquisition:
|
|
329
|
+
|
|
330
|
+
1. **Auto-export (macOS):** AppleScript drives Microsoft PowerPoint to export each slide as PNG → temp directory → copied to `course/assets/slides/`
|
|
331
|
+
2. **Manual (`--slides-dir`):** User points at pre-exported images (any platform, no PowerPoint needed)
|
|
332
|
+
|
|
333
|
+
After images are acquired, the module:
|
|
334
|
+
- Extracts text via `node-pptx-parser` → `course/references/converted/`
|
|
335
|
+
- Scaffolds project via `create.js`
|
|
336
|
+
- Removes template slides, generates `slide-XX.html` files (each an `<img class="img-contain">`)
|
|
337
|
+
- Writes `course-config.js` with `layout: 'presentation'`, no engagement tracking
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Key Managers (Internals)
|
|
342
|
+
|
|
343
|
+
> For complete domain schemas, storage architecture, and data flow, see [`DATA_MODEL.md`](./DATA_MODEL.md).
|
|
344
|
+
|
|
345
|
+
| Manager | Location | Purpose |
|
|
346
|
+
|---------|----------|--------|
|
|
347
|
+
| `stateManager` | `state/` | **Sole public API** for all LMS and state operations |
|
|
348
|
+
| `assessmentManager` | `managers/` | Graded assessments: question banks, randomization, scoring |
|
|
349
|
+
| `engagementManager` | `managers/` | Tracks content interaction (tabs, accordions, scroll, time) |
|
|
350
|
+
| `objectiveManager` | `managers/` | Learning objectives (CMI-backed domain) |
|
|
351
|
+
| `interactionManager` | `managers/` | LMS interaction reporting (CMI-backed, append-only) |
|
|
352
|
+
| `accessibilityManager` | `managers/` | A11y preferences persisted via `stateManager` |
|
|
353
|
+
| `flagManager` | `managers/` | Arbitrary key-value flags in suspend data |
|
|
354
|
+
| `audioManager` | `managers/` | Audio playback: position persistence, completion tracking |
|
|
355
|
+
| `videoManager` | `managers/` | Video playback: position persistence, completion tracking |
|
|
356
|
+
| `scoreManager` | `managers/` | Course-level scoring (dynamically loaded) |
|
|
357
|
+
| `commentManager` | `managers/` | End-of-course comments/ratings |
|
|
358
|
+
| `breakpointManager` | `app/` | Responsive breakpoints, `.bp-*` classes |
|
|
359
|
+
| `navigationState` | `navigation/` | Current slide, visited slides |
|
|
360
|
+
|
|
361
|
+
### stateManager: The Sole LMS Gateway
|
|
362
|
+
|
|
363
|
+
**stateManager** (from `state/index.js`) is the single entry point for all LMS communication. It composes:
|
|
364
|
+
|
|
365
|
+
| Internal Module | Responsibility |
|
|
366
|
+
|----------------|----------------|
|
|
367
|
+
| `lms-connection.js` | Driver lifecycle, connection init/terminate, keep-alive |
|
|
368
|
+
| `xapi-statement-service.js` | Bridges events to xAPI statements (cmi5 only) |
|
|
369
|
+
| `state-domains.js` | Domain CRUD with append-only semantics |
|
|
370
|
+
| `state-commits.js` | Auto-batched commit scheduling (500ms debounce) |
|
|
371
|
+
| `state-validation.js` | State hydration, migration, validation |
|
|
372
|
+
| `transaction-log.js` | Ring buffer for debugging |
|
|
373
|
+
|
|
374
|
+
> **Compression:** `cmi.suspend_data` is automatically compressed via `lz-string` (UTF16) in the driver layer. This is transparent to all consumers.
|
|
375
|
+
|
|
376
|
+
#### Domain API
|
|
377
|
+
|
|
378
|
+
All state access uses the domain pattern:
|
|
379
|
+
|
|
380
|
+
```javascript
|
|
381
|
+
// Read/write domain state — stateManager routes to appropriate storage
|
|
382
|
+
stateManager.getDomainState('objectives'); // → cmi.objectives.* (SCORM) or suspend_data (cmi5/LTI)
|
|
383
|
+
stateManager.getDomainState('navigation'); // → suspend_data
|
|
384
|
+
stateManager.setDomainState('objectives', data);
|
|
385
|
+
stateManager.setDomainState('flags', data);
|
|
386
|
+
|
|
387
|
+
// Semantic LMS methods — no raw CMI access
|
|
388
|
+
stateManager.reportScore({ raw: 85, scaled: 0.85, min: 0, max: 100 });
|
|
389
|
+
stateManager.reportCompletion('completed');
|
|
390
|
+
stateManager.setBookmark('slide-03');
|
|
391
|
+
stateManager.flush(); // Commit now, don't wait for debounce
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
> **Rule:** Never bypass `stateManager` for LMS access. Never import `lms-connection.js` directly.
|
|
395
|
+
|
|
396
|
+
#### Write Batching
|
|
397
|
+
|
|
398
|
+
LMS writes are auto-batched with a **500ms debounce**. Rapid `setDomainState` calls combine into a single commit. **Critical actions** (exit, terminate) automatically flush pending writes before proceeding.
|
|
399
|
+
|
|
400
|
+
### flagManager Events
|
|
401
|
+
|
|
402
|
+
```javascript
|
|
403
|
+
eventBus.on('flag:updated', ({ key, value }) => { /* handle */ });
|
|
404
|
+
eventBus.on('flag:removed', ({ key }) => { /* handle */ });
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### State Validation (Course Updates)
|
|
408
|
+
|
|
409
|
+
Handles LMS data mismatches when course structure changes after learners have started:
|
|
410
|
+
|
|
411
|
+
| Behavior | Dev Mode | Prod Mode |
|
|
412
|
+
|----------|----------|----------|
|
|
413
|
+
| Invalid slide in `cmi.location` | Throws error | Reverts to slide 0 |
|
|
414
|
+
| Missing assessment questions | Throws error | Filters out missing |
|
|
415
|
+
| Orphaned engagement/navigation data | Warns | Silently removes |
|
|
416
|
+
| Schema version newer (downgrade) | Throws error | Resets to fresh state |
|
|
417
|
+
| Schema version older (upgrade) | Runs migrations | Runs migrations |
|
|
418
|
+
|
|
419
|
+
**Setup:** `stateManager.setCourseValidationConfig(config)` called in `main.js` before `initialize()`.
|
|
420
|
+
|
|
421
|
+
**Event:** `state:recovered` emitted when prod mode gracefully recovers.
|
|
422
|
+
|
|
423
|
+
**Schema versioning:** `STATE_SCHEMA_VERSION` constant in `state-validation.js`. Increment when state structure changes incompatibly.
|
|
424
|
+
|
|
425
|
+
### Schema Migrations
|
|
426
|
+
|
|
427
|
+
When incrementing `STATE_SCHEMA_VERSION`, add a migration function in `STATE_MIGRATIONS`:
|
|
428
|
+
|
|
429
|
+
```javascript
|
|
430
|
+
const STATE_MIGRATIONS = {
|
|
431
|
+
2: (state) => {
|
|
432
|
+
// Migrate from v1 → v2: rename 'oldDomain' to 'newDomain'
|
|
433
|
+
if (state.oldDomain) {
|
|
434
|
+
state.newDomain = state.oldDomain;
|
|
435
|
+
delete state.oldDomain;
|
|
436
|
+
}
|
|
437
|
+
return state;
|
|
438
|
+
},
|
|
439
|
+
3: (state) => {
|
|
440
|
+
// Migrate from v2 → v3: restructure nested data
|
|
441
|
+
// ...
|
|
442
|
+
return state;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**When to add migrations:**
|
|
448
|
+
- Renaming domains (`navigation` → `nav`)
|
|
449
|
+
- Restructuring nested data paths
|
|
450
|
+
- Changing data types (array → object)
|
|
451
|
+
- Adding required fields that code expects
|
|
452
|
+
|
|
453
|
+
**When NOT needed (validation handles):**
|
|
454
|
+
- Adding optional fields or new domains
|
|
455
|
+
- Adding new slide/interaction types
|
|
456
|
+
|
|
457
|
+
Migrations run sequentially (v1→v2→v3), so each only handles one version jump.
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Logging & Error Handling
|
|
462
|
+
|
|
463
|
+
**Two complementary systems - use BOTH:**
|
|
464
|
+
|
|
465
|
+
### Logger (Observability)
|
|
466
|
+
|
|
467
|
+
```javascript
|
|
468
|
+
logger.debug('Initializing slide', { slideId: 'intro' }); // Dev only
|
|
469
|
+
logger.info('User action recorded'); // Dev only
|
|
470
|
+
logger.warn('Deprecated feature used'); // Dev + Prod
|
|
471
|
+
logger.error('Operation failed', error); // Dev + Prod
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Global as `logger` or `window.logger` (no import needed). Auto-filtered by environment.
|
|
475
|
+
|
|
476
|
+
### Event Bus (Error Communication)
|
|
477
|
+
|
|
478
|
+
**Tiered Error Strategy:**
|
|
479
|
+
|
|
480
|
+
| Tier | When | Behavior | Example |
|
|
481
|
+
|------|------|----------|---------|
|
|
482
|
+
| **Tier 1** | Contract violations, programming errors | Always `throw` | Wrong parameter types, API misuse, double-init |
|
|
483
|
+
| **Tier 2** | Runtime/init failures (missing DOM, bad config) | `logger.fatal()` → throws in DEV, logs+degrades in PROD | Missing container element, invalid data attributes |
|
|
484
|
+
| **Tier 3** | Existing dual-mode functions | Logger + eventBus only | Driver warnings, state persistence failures |
|
|
485
|
+
|
|
486
|
+
**`logger.fatal(message, context)`** — Tier 2 handler in `framework/js/utilities/logger.js`:
|
|
487
|
+
|
|
488
|
+
```javascript
|
|
489
|
+
if (!container) {
|
|
490
|
+
logger.fatal('initTabs: container not found', { domain: 'ui', operation: 'initTabs' });
|
|
491
|
+
return; // Required — exit the function after calling logger.fatal
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
- **DEV**: throws `Error` with formatted message for immediate visibility
|
|
496
|
+
- **PROD**: calls `logger.warn()` for graceful degradation
|
|
497
|
+
|
|
498
|
+
All error events follow the standardized shape:
|
|
499
|
+
|
|
500
|
+
```javascript
|
|
501
|
+
{ domain, operation, message, stack?, context? }
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**When to catch:** Only to show user notification or cleanup, then MUST re-throw:
|
|
505
|
+
|
|
506
|
+
```javascript
|
|
507
|
+
try {
|
|
508
|
+
objectiveManager.setCompletionStatus('obj', 'completed');
|
|
509
|
+
} catch (error) {
|
|
510
|
+
showNotification('Failed to save', 'error');
|
|
511
|
+
throw error; // Required - no silent failures
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### External Communications
|
|
516
|
+
|
|
517
|
+
Three optional, config-driven utilities for outbound communication. All in `framework/js/utilities/`, initialized in `main.js`. Each activates when its `endpoint` is configured — otherwise silently skips.
|
|
518
|
+
|
|
519
|
+
| Utility | Config Key | Transport | Events |
|
|
520
|
+
|---------|-----------|-----------|--------|
|
|
521
|
+
| `error-reporter.js` | `environment.errorReporting` | POST per error (60s dedup) | `*:error` (14 event types) |
|
|
522
|
+
| `data-reporter.js` | `environment.dataReporting` | Batched POST + `sendBeacon` on unload | `assessment:submitted`, `objective:updated`, `interaction:recorded` |
|
|
523
|
+
| `course-channel.js` | `environment.channel` | POST to send, SSE to receive | `channel:message`, `channel:connected`, `channel:disconnected` |
|
|
524
|
+
|
|
525
|
+
**Error Reporter** — Subscribes to all `*:error` events, deduplicates by domain+operation+message (60s window), POSTs to endpoint. Optional `enableUserReports: true` adds "Report Issue" to settings menu. `submitUserReport()` for programmatic user reports.
|
|
526
|
+
|
|
527
|
+
**Data Reporter** — Queues assessment/objective/interaction records, flushes on batch size (default 10) or timer (default 30s). `sendBeacon` fallback on page unload.
|
|
528
|
+
|
|
529
|
+
**Course Channel** — Generic pub/sub pipe. `sendChannelMessage(data)` POSTs any JSON to `endpoint/channelId`. SSE listener on same URL bridges incoming messages to EventBus. Exponential backoff reconnect (1s → 30s cap). Content-agnostic — the relay is a dumb fan-out router.
|
|
530
|
+
|
|
531
|
+
```javascript
|
|
532
|
+
// Config (all optional)
|
|
533
|
+
environment: {
|
|
534
|
+
errorReporting: { endpoint: '...', apiKey: '...', includeContext: true, enableUserReports: true },
|
|
535
|
+
dataReporting: { endpoint: '...', apiKey: '...', batchSize: 10, flushInterval: 30000 },
|
|
536
|
+
channel: { endpoint: '...', apiKey: '...', channelId: 'session-123' }
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
**Authentication** — All reporters support an optional `apiKey` field. When set, it's sent as `Authorization: Bearer <apiKey>` on `fetch()` calls. For `sendBeacon` (page unload), `fetch()` with `keepalive: true` is used instead since `sendBeacon` doesn't support custom headers. For SSE (`EventSource`), the token is passed as a `?token=` URL parameter.
|
|
541
|
+
|
|
542
|
+
**Example backends:** `framework/docs/examples/cloudflare-{error-worker,data-worker,channel-relay}.js`
|
|
543
|
+
|
|
544
|
+
> **Local dev:** Error and data reporters are automatically disabled during watch builds (`coursecode preview`, `coursecode dev`, `npm run dev`). The CLI sets `VITE_COURSECODE_LOCAL=true` in the Vite build env, which the reporters check at init. Production builds (`coursecode build`) do not set this flag.
|
|
545
|
+
|
|
546
|
+
## Dual-Context Architecture
|
|
547
|
+
|
|
548
|
+
This codebase serves two roles:
|
|
549
|
+
|
|
550
|
+
1. **Framework source repo** — where the framework itself is developed (`vite.framework-dev.config.js`, `template/course/` as test content)
|
|
551
|
+
2. **Course project template** — `template/` is what course authors get when they create a new project (`template/vite.config.js`, their own `course/`)
|
|
552
|
+
|
|
553
|
+
| File | Used by | Purpose |
|
|
554
|
+
|------|---------|---------|
|
|
555
|
+
| `vite.framework-dev.config.js` | Framework developers (this repo) | Builds from `template/course/`, references `lib/` directly |
|
|
556
|
+
| `template/vite.config.js` | Course authors (their project) | Builds from `course/`, imports from `coursecode` package |
|
|
557
|
+
### Preview Architecture
|
|
558
|
+
|
|
559
|
+
Preview is **not** part of the build output. It is platform infrastructure, served separately from `dist/`:
|
|
560
|
+
|
|
561
|
+
| Scenario | Who provides preview? | Where preview lives |
|
|
562
|
+
|----------|----------------------|--------------------|
|
|
563
|
+
| `coursecode preview` (local) | `preview-server.js` | In-memory, never written to disk |
|
|
564
|
+
| `coursecode preview --export` | `preview-export.js` | Separate `course-preview/` directory (copies `dist/` + wraps in stub player) |
|
|
565
|
+
| CourseCode Cloud | Cloud platform | Cloud hosts its own stub player, wraps any uploaded `dist/` |
|
|
566
|
+
| Self-hosted CDN (proxy) | N/A | User tests locally with `coursecode preview`, CDN serves `dist/` only |
|
|
567
|
+
|
|
568
|
+
**Key principle:** `dist/` never contains preview/stub player assets. The `--export` flag produces a separate `course-preview/` folder for static hosting (Netlify, GitHub Pages). The stub player is generic — it wraps any course's `index.html` in an iframe with fake LMS APIs.
|
|
569
|
+
|
|
570
|
+
### Cloud Integration Architecture
|
|
571
|
+
|
|
572
|
+
The universal build enables cloud platforms to assemble format-specific outputs **without running any framework build tools or user-uploaded code**:
|
|
573
|
+
|
|
574
|
+
```
|
|
575
|
+
User: coursecode build → uploads dist/
|
|
576
|
+
│
|
|
577
|
+
┌────────────────────┼─────────────────────┐
|
|
578
|
+
▼ ▼ ▼
|
|
579
|
+
Cloud Preview Cloud ZIP (SCORM 2004) Cloud ZIP (SCORM 1.2)
|
|
580
|
+
┌────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
|
581
|
+
│ Cloud's own │ │ Copy dist/ │ │ Copy dist/ │
|
|
582
|
+
│ stub player │ │ stampFormatInHtml│ │ stampFormatInHtml│
|
|
583
|
+
│ iframes the │ │ generateManifest │ │ generateManifest │
|
|
584
|
+
│ uploaded │ │ → ZIP │ │ → ZIP │
|
|
585
|
+
│ dist/ │ └──────────────────┘ └──────────────────┘
|
|
586
|
+
└────────────┘
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
**Cloud dependencies:** The cloud app imports `stampFormatInHtml` and `generateManifest` directly from the `coursecode` npm package. These are pure Node utilities — no Vite, no dynamic imports of user code, no `eval`. All inputs (title, version, file list) come from scanning the uploaded `dist/` or the cloud's own database.
|
|
590
|
+
|
|
591
|
+
**Security boundary:** The cloud never executes `course-config.js` or any user-authored JavaScript. The meta tag and manifest are the only format-specific artifacts, and both are generated from trusted framework source code.
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## Course Validation
|
|
595
|
+
|
|
596
|
+
Two linters validate courses at different stages, each in a different environment:
|
|
597
|
+
|
|
598
|
+
### Runtime Linter (`framework/js/dev/runtime-linter.js`)
|
|
599
|
+
|
|
600
|
+
Runs **in the browser** during preview when `import.meta.env.DEV === true`. Called from `main.js` before course initialization.
|
|
601
|
+
|
|
602
|
+
**What it checks (needs a real DOM):**
|
|
603
|
+
- Renders each slide to detached DOM, validates engagement requirements match content
|
|
604
|
+
- Visual layout: contrast ratios, touch target sizes, nested cards, heading hierarchy
|
|
605
|
+
- Component structure validation against schemas
|
|
606
|
+
- Assessment config validation
|
|
607
|
+
- Audio configuration conflicts (slide audio vs modal audio)
|
|
608
|
+
|
|
609
|
+
**On failure:** Halts initialization with formatted error showing slide ID and fix needed.
|
|
610
|
+
|
|
611
|
+
**Production:** Tree-shaken out — never included in production builds.
|
|
612
|
+
|
|
613
|
+
### Build Linter (`lib/build-linter.js`)
|
|
614
|
+
|
|
615
|
+
Runs **in Node.js** during build (via `vite.framework-dev.config.js` `closeBundle` hook) and via MCP/CLI.
|
|
616
|
+
|
|
617
|
+
**What it checks (no DOM needed):**
|
|
618
|
+
- Course config validation (structure, objectives, gating conditions)
|
|
619
|
+
- CSS class validation via PostCSS — flags hallucinated class names
|
|
620
|
+
- Schema-driven requirement validation from source templates
|
|
621
|
+
- Duplicate interaction ID detection
|
|
622
|
+
|
|
623
|
+
**Invoked by:**
|
|
624
|
+
- `npm run build` — automatically in `closeBundle` hook
|
|
625
|
+
- `coursecode lint` CLI command
|
|
626
|
+
- MCP `coursecode_lint` tool
|
|
627
|
+
|
|
628
|
+
**Errors fail the build; warnings print but don't block.**
|
|
629
|
+
|
|
630
|
+
### Shared Rules (`lib/validation-rules.js`)
|
|
631
|
+
|
|
632
|
+
Pure validation functions used by **both** linters. No environment-specific code (no DOM, no `fs`). Includes assessment validation, engagement validation, and result formatting.
|
|
633
|
+
|
|
634
|
+
## Engagement Tracking (Implementation)
|
|
635
|
+
|
|
636
|
+
All slides MUST have `engagement` config in `course-config.js`.
|
|
637
|
+
|
|
638
|
+
### Requirement Types
|
|
639
|
+
|
|
640
|
+
| Type | Required Props | Validates Against |
|
|
641
|
+
|------|----------------|-------------------|
|
|
642
|
+
| `viewAllTabs` | - | `[data-component="tabs"]` |
|
|
643
|
+
| `viewAllPanels` | - | `[data-component="accordion"]` |
|
|
644
|
+
| `viewAllFlipCards` | - | `[data-flip-card-id]` |
|
|
645
|
+
| `viewAllTimelineEvents` | - | `[data-component="interactive-timeline"]` |
|
|
646
|
+
| `viewAllHotspots` | - | `[data-component="interactive-image"]` |
|
|
647
|
+
| `viewAllModals` | - | `[data-component="modal-trigger"]` with `data-modal-id` |
|
|
648
|
+
| `interactionComplete` | `interactionId`, optional `label` | `[data-interaction-id]` |
|
|
649
|
+
| `allInteractionsComplete` | - | Any `[data-interaction-id]` |
|
|
650
|
+
| `scrollDepth` | `percentage` | Scroll event tracking |
|
|
651
|
+
| `timeOnSlide` | `minSeconds` | Session timer |
|
|
652
|
+
| `slideAudioComplete` | - | Slide audio config |
|
|
653
|
+
| `audioComplete` | `audioId` | `[data-audio-id]` |
|
|
654
|
+
| `modalAudioComplete` | `modalId` | Modal audio config |
|
|
655
|
+
| `flag` | `key`, optional `equals` | flagManager |
|
|
656
|
+
| `allFlags` | `flags` array | flagManager |
|
|
657
|
+
|
|
658
|
+
### Tooltip Labels
|
|
659
|
+
|
|
660
|
+
- **`message`** (engagement-level): Full override of tooltip text
|
|
661
|
+
- **`label`** (requirement-level): Custom name in "Complete: X" format (auto-formats from ID if omitted)
|
|
662
|
+
|
|
663
|
+
### Progress Indicator
|
|
664
|
+
|
|
665
|
+
When `engagement.required` is `true`, the circular progress indicator displays in the nav footer by default. Set `showIndicator: false` to hide it.
|
|
666
|
+
|
|
667
|
+
Custom indicators use `EngagementManager` API:
|
|
668
|
+
```javascript
|
|
669
|
+
const progress = engagementManager.getProgress(slideId);
|
|
670
|
+
// Returns: { percentage: 75, items: [{type, label, complete}, ...] }
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## Declarative UI Components
|
|
676
|
+
|
|
677
|
+
Components auto-initialize via `data-component` attributes. Registration in `framework/js/components/`.
|
|
678
|
+
|
|
679
|
+
| Component | Attribute | Events Emitted |
|
|
680
|
+
|-----------|-----------|----------------|
|
|
681
|
+
| Tabs | `data-component="tabs"` | `tab:selected` |
|
|
682
|
+
| Accordion | `data-component="accordion"` | `accordion:panel-opened` |
|
|
683
|
+
| Carousel | `data-component="carousel"` | `carousel:slide-changed` |
|
|
684
|
+
| Collapse | `data-component="collapse"` | `collapse:toggle` |
|
|
685
|
+
| Dropdown | `data-component="dropdown"` | `dropdown:change` |
|
|
686
|
+
| Modal | `data-component="modal-trigger"` | `modal:opened`, `modal:closed` |
|
|
687
|
+
| Flip Card | `data-component="flip-card"` | `flipcard:flipped` |
|
|
688
|
+
| Interactive Timeline | `data-component="interactive-timeline"` | `timeline:event-viewed` |
|
|
689
|
+
| Toggle Group | `data-component="toggle-group"` | `toggle:change` |
|
|
690
|
+
| Checkbox Group | `data-component="checkbox-group"` | `checkbox-group:change` |
|
|
691
|
+
| Audio Player | `data-component="audio-player"` | `audio:complete` |
|
|
692
|
+
| Video Player | `data-component="video-player"` | `video:complete` |
|
|
693
|
+
|
|
694
|
+
### Component Catalog
|
|
695
|
+
|
|
696
|
+
Components and layout patterns are managed via `component-catalog.js`, which mirrors the interaction catalog pattern:
|
|
697
|
+
|
|
698
|
+
```javascript
|
|
699
|
+
// Auto-discovery via import.meta.glob
|
|
700
|
+
// framework/js/components/ui-components/*.js → All built-in components (tabs, accordion, carousel, modal, hero, steps, timeline, etc.)
|
|
701
|
+
// course/components/*.js → Custom course components
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
**Schema Pattern** - Every component exports a schema for validation:
|
|
705
|
+
|
|
706
|
+
```javascript
|
|
707
|
+
// Example: framework/js/components/ui-components/tabs.js
|
|
708
|
+
export const schema = {
|
|
709
|
+
type: 'tabs',
|
|
710
|
+
description: 'Accessible tab interface with keyboard navigation',
|
|
711
|
+
properties: {
|
|
712
|
+
activeClass: { type: 'string', default: 'active' }
|
|
713
|
+
},
|
|
714
|
+
structure: {
|
|
715
|
+
container: '[data-component="tabs"]',
|
|
716
|
+
children: {
|
|
717
|
+
button: { selector: '[data-action="select-tab"]', required: true, minItems: 1 },
|
|
718
|
+
panel: { selector: '.tab-content', required: true }
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
export const metadata = {
|
|
724
|
+
category: 'ui-component',
|
|
725
|
+
engagementTracking: 'viewAllTabs',
|
|
726
|
+
emitsEvents: ['tab:selected']
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
export function initTabs(container) { /* ... */ }
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
**Linter Validation** - The runtime linter validates component structure against schemas during preview.
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
## Automation API
|
|
737
|
+
|
|
738
|
+
**Access**: `window.CourseCodeAutomation` (dev mode only, tree-shaken in prod)
|
|
739
|
+
|
|
740
|
+
**Enable** in `course/course-config.js`:
|
|
741
|
+
```javascript
|
|
742
|
+
environment: {
|
|
743
|
+
automation: { enabled: true, disableBeforeUnloadGuard: true, exposeCorrectAnswers: true }
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
**Requirements**: `import.meta.env.MODE !== 'production'` AND `automation.enabled === true`
|
|
748
|
+
|
|
749
|
+
### API Methods
|
|
750
|
+
|
|
751
|
+
| Category | Method | Returns/Purpose |
|
|
752
|
+
|----------|--------|-----------------|
|
|
753
|
+
| **Discovery** | `listInteractions()` | `[{id, type, registeredAt}, ...]` |
|
|
754
|
+
| | `getInteractionMetadata(id)` | `{id, type, registeredAt}` |
|
|
755
|
+
| **State Access** | `getResponse(id)` | Current response (format varies by type) |
|
|
756
|
+
| | `getCorrectResponse(id)` | Correct answer (needs `exposeCorrectAnswers`) |
|
|
757
|
+
| **State Mutation** | `setResponse(id, response)` | Sets response |
|
|
758
|
+
| **Evaluation** | `checkAnswer(id)` | `{correct, score, feedback, ...}` |
|
|
759
|
+
| | `checkSlideAnswers(slideId?)` | `[{interactionId, type, evaluation}, ...]` |
|
|
760
|
+
| **Navigation** | `getCourseStructure()` | Structure array from config |
|
|
761
|
+
| | `getCurrentSlide()` | Current slide ID or null |
|
|
762
|
+
| | `goToSlide(slideId, context?)` | Navigate with optional context |
|
|
763
|
+
| **Engagement** | `getEngagementState()` | `{complete, tracked, requirements}` |
|
|
764
|
+
| | `getEngagementProgress()` | `{percentage, items: [{label, complete, type}]}` |
|
|
765
|
+
| | `markTabViewed(tabId)` | Manually track tab (testing) |
|
|
766
|
+
| | `setScrollDepth(percentage)` | Simulate scroll (testing) |
|
|
767
|
+
| | `resetEngagement()` | Reset tracking for current slide |
|
|
768
|
+
| **Flags** | `getFlag(key)` | Get flag value |
|
|
769
|
+
| | `setFlag(key, value)` | Set flag (triggers engagement re-eval) |
|
|
770
|
+
| | `getAllFlags()` | All flags as object |
|
|
771
|
+
| | `removeFlag(key)` | Remove flag |
|
|
772
|
+
| **Audio** | `getAudioState()` | `{currentSrc, position, isPlaying, isMuted, duration}` |
|
|
773
|
+
| | `hasAudio()` | Check if audio loaded |
|
|
774
|
+
| | `playAudio()` / `pauseAudio()` / `toggleAudio()` | Playback control |
|
|
775
|
+
| | `restartAudio()` | Restart from beginning |
|
|
776
|
+
| | `seekAudio(seconds)` / `seekAudioToPercentage(pct)` | Seek |
|
|
777
|
+
| | `toggleAudioMute()` / `setAudioMuted(bool)` | Mute control |
|
|
778
|
+
| | `getAudioProgress()` | Playback percentage |
|
|
779
|
+
| **Observability** | `getAutomationTrace()` | `[{timestamp, action, ...}, ...]` |
|
|
780
|
+
| | `clearAutomationTrace()` | Clears trace log |
|
|
781
|
+
| | `getVersion()` | `{api, phase, features}` |
|
|
782
|
+
|
|
783
|
+
### Response Formats by Interaction Type
|
|
784
|
+
|
|
785
|
+
| Type | Format |
|
|
786
|
+
|------|--------|
|
|
787
|
+
| Multiple Choice | `'a'`, `'b'`, `'c'` |
|
|
788
|
+
| True/False | `true` / `false` |
|
|
789
|
+
| Fill-in-Blank | `{blankId: 'answer'}` |
|
|
790
|
+
| Drag-Drop | `{itemId: zoneId}` |
|
|
791
|
+
| Numeric | `1.5` |
|
|
792
|
+
| Sequencing | `['id1', 'id2', 'id3']` |
|
|
793
|
+
| Likert | `{questionId: 'value'}` |
|
|
794
|
+
|
|
795
|
+
### data-testid Attributes
|
|
796
|
+
|
|
797
|
+
**Nav**: `nav-prev`, `nav-next`, `nav-exit`, `nav-menu-toggle`, `nav-menu-item-{slideId}`, `nav-section-{sectionId}`
|
|
798
|
+
|
|
799
|
+
**Interactions**: `{id}-check-answer`, `{id}-reset`, `{id}-controls`, `{id}-feedback`, `{id}-choice-{index}`, `{id}-blank-{index}`, `{id}-input`, `{id}-drag-item-{itemId}`, `{id}-drop-zone-{zoneId}`
|
|
800
|
+
|
|
801
|
+
**Assessments**: `assessment-start`, `assessment-nav-{prev|next}`, `assessment-submit`, `assessment-retake`, `assessment-review-question-{index}`
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
## MCP Integration (AI Agent Control)
|
|
808
|
+
|
|
809
|
+
The MCP server runs a **persistent headless Chrome** internally via `puppeteer-core`. All runtime tools execute directly in this headless browser — agents never need to open a browser.
|
|
810
|
+
|
|
811
|
+
### Preview Server Ownership
|
|
812
|
+
|
|
813
|
+
The MCP does **not** start or manage the preview server. The preview must be running before using runtime tools:
|
|
814
|
+
|
|
815
|
+
- **Human**: run `coursecode preview` (or `npm run preview`) in a terminal
|
|
816
|
+
- **AI agent**: use `run_command` to execute `npm run preview`
|
|
817
|
+
|
|
818
|
+
If the preview is not running, runtime tools fail fast with a clear error message.
|
|
819
|
+
|
|
820
|
+
### Setup
|
|
821
|
+
|
|
822
|
+
1. Start the preview server externally (see above)
|
|
823
|
+
2. Add to IDE MCP config:
|
|
824
|
+
|
|
825
|
+
```json
|
|
826
|
+
{ "mcpServers": { "coursecode": { "command": "coursecode", "args": ["mcp"] } } }
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
### How the Headless Browser Works
|
|
830
|
+
|
|
831
|
+
- On the **first runtime tool call**, the MCP launches headless Chrome and connects to the already-running preview server
|
|
832
|
+
- The browser auto-reconnects when Vite rebuilds (file changes trigger SSE reload)
|
|
833
|
+
- All tool calls execute instantly via `page.evaluate()` — no manual waits needed
|
|
834
|
+
|
|
835
|
+
### Runtime Tools (require preview server)
|
|
836
|
+
|
|
837
|
+
| Tool | Purpose | Returns |
|
|
838
|
+
|------|---------|--------|
|
|
839
|
+
| `coursecode_state` | Full course snapshot | `{slide, structure, interactions, engagement, lmsState, errors}` |
|
|
840
|
+
| `coursecode_navigate` | Go to slide by ID | `{slideId}` → new state |
|
|
841
|
+
| `coursecode_interact` | Set response + evaluate | `{interactionId, response}` → `{correct, score, feedback}` |
|
|
842
|
+
| `coursecode_screenshot` | Visual capture (PNG) | Optional `slideId` to navigate first, `fullPage` for scroll capture |
|
|
843
|
+
| `coursecode_reset` | Clear learner state | `{hard?: boolean}` → reload |
|
|
844
|
+
|
|
845
|
+
### Navigation API
|
|
846
|
+
|
|
847
|
+
Use MCP tools for all course interaction — never use external browser tools:
|
|
848
|
+
|
|
849
|
+
- `coursecode_state` → get all slide IDs, current position, interactions
|
|
850
|
+
- `coursecode_navigate(slideId)` → instant slide navigation
|
|
851
|
+
- `coursecode_screenshot(slideId)` → navigate + capture in one call
|
|
852
|
+
- `coursecode_interact(id, response)` → answer + evaluate in one call
|
|
853
|
+
|
|
854
|
+
### Architecture
|
|
855
|
+
|
|
856
|
+
```
|
|
857
|
+
MCP Server (IDE) ──puppeteer──▶ Headless Chrome ──HTTP──▶ Preview Server
|
|
858
|
+
│
|
|
859
|
+
└── Course iframe (CourseCodeAutomation API)
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
- **Preview not running?** → Tools return clear error: "Start preview server first"
|
|
863
|
+
- **Chrome not found?** → Install Google Chrome or set `CHROME_PATH` env var
|
|
864
|
+
|
|
865
|
+
---
|
|
866
|
+
|
|
867
|
+
## Audio Manager (Internals)
|
|
868
|
+
|
|
869
|
+
Singleton at `framework/js/managers/audio-manager.js`. Manages single audio element.
|
|
870
|
+
|
|
871
|
+
### Key Behaviors
|
|
872
|
+
|
|
873
|
+
- **Position Persistence**: Saves position when leaving slide, restores on return
|
|
874
|
+
- **Completion Tracking**: Tracks max position reached (handles seeks/replays)
|
|
875
|
+
- **Single Instance**: Only one audio plays at a time (slide vs modal vs standalone)
|
|
876
|
+
- **Mute Preference**: Mute state persists across session
|
|
877
|
+
- **Auto-Pause**: Audio pauses on navigation away or modal close
|
|
878
|
+
|
|
879
|
+
### API
|
|
880
|
+
|
|
881
|
+
```javascript
|
|
882
|
+
const { audioManager } = CourseCode;
|
|
883
|
+
|
|
884
|
+
audioManager.play();
|
|
885
|
+
audioManager.pause();
|
|
886
|
+
audioManager.togglePlayPause();
|
|
887
|
+
audioManager.restart();
|
|
888
|
+
audioManager.seek(30); // seconds
|
|
889
|
+
audioManager.seekToPercentage(50);
|
|
890
|
+
audioManager.toggleMute();
|
|
891
|
+
audioManager.getState(); // Full state object
|
|
892
|
+
audioManager.hasAudio();
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
### Footer Controls
|
|
896
|
+
|
|
897
|
+
- **Slide audio**: Full controls (play/pause, restart, progress bar, mute, time)
|
|
898
|
+
- **Modal audio**: Compact controls (play/pause, restart, mute only)
|
|
899
|
+
- **Standalone**: Full controls inline
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
## Assessment Manager (Internals)
|
|
904
|
+
|
|
905
|
+
`framework/js/managers/assessment-manager.js`
|
|
906
|
+
|
|
907
|
+
### Features
|
|
908
|
+
|
|
909
|
+
- **Question Banks**: Select N questions from categorized banks
|
|
910
|
+
- **Randomization**: Shuffle questions and/or re-randomize on retake
|
|
911
|
+
- **Progressive Intervention**: Show remedial content after N failures, restart course after M
|
|
912
|
+
- **Auto-Linked Objectives**: Updates objective on submission based on score
|
|
913
|
+
- **Unanswered Handling**: Confirmation modal or immediate submit (configurable)
|
|
914
|
+
|
|
915
|
+
### Creation
|
|
916
|
+
|
|
917
|
+
```javascript
|
|
918
|
+
const { AssessmentManager } = CourseCode;
|
|
919
|
+
|
|
920
|
+
const assessment = AssessmentManager.createAssessment(
|
|
921
|
+
{ ...config, questions }, // OR questionBanks for random selection
|
|
922
|
+
overrides
|
|
923
|
+
);
|
|
924
|
+
assessment.render(container);
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
### Events
|
|
928
|
+
|
|
929
|
+
```javascript
|
|
930
|
+
eventBus.on('assessment:submitted', ({ id, score, passed }) => { });
|
|
931
|
+
eventBus.on('assessment:retake', ({ id, attemptNumber }) => { });
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
---
|
|
935
|
+
|
|
936
|
+
## CSS Architecture
|
|
937
|
+
|
|
938
|
+
| File | Purpose |
|
|
939
|
+
|------|---------|
|
|
940
|
+
| `design-tokens.css` | CSS variables (colors, spacing, typography) |
|
|
941
|
+
| `01-base.css` | HTML resets, base typography |
|
|
942
|
+
| `02-layout.css` | Content width, stacks, columns, splits |
|
|
943
|
+
| `components/*.css` | Individual UI component styles (cards, hero, tabs, steps, timeline, etc.) |
|
|
944
|
+
| `interactions/*.css` | Interaction-specific styles |
|
|
945
|
+
| `utilities/*.css` | Utility classes (spacing, display, flex) |
|
|
946
|
+
| `framework.css` | Main import file, orchestrates all modules |
|
|
947
|
+
|
|
948
|
+
### Layout System
|
|
949
|
+
|
|
950
|
+
CSS-only layouts controlled via `data-layout` attribute on `<html>`. Files in `framework/css/layouts/`:
|
|
951
|
+
|
|
952
|
+
| File | Purpose |
|
|
953
|
+
|------|---------|
|
|
954
|
+
| `base.css` | Layout tokens, default structure |
|
|
955
|
+
| `traditional.css` | Full header, sidebar toggle |
|
|
956
|
+
| `article.css` | **Default**: Minimal header, centered content, floating pill nav |
|
|
957
|
+
| `focused.css` | Hidden header, centered content, floating pill nav |
|
|
958
|
+
| `presentation.css` | Full viewport, edge arrow navigation |
|
|
959
|
+
| `canvas.css` | Zero framework CSS — all styles reverted to browser defaults, author BYOs via `theme.css` |
|
|
960
|
+
|
|
961
|
+
> **Note:** Canvas only strips CSS (`all: revert` on `#slide-container`). All JS infrastructure — navigation, gating, interactions, engagement tracking, and LMS drivers — remains fully functional.
|
|
962
|
+
|
|
963
|
+
Set via `layout` in `course-config.js`. The `main.js` automatically applies the attribute from config.
|
|
964
|
+
|
|
965
|
+
### Auto-Wrapping
|
|
966
|
+
|
|
967
|
+
Slides are automatically wrapped with content width class (default: `.content-medium`).
|
|
968
|
+
|
|
969
|
+
Override per-slide with `data-content-width` attribute or globally via `slideDefaults.contentWidth` in `course-config.js`.
|
|
970
|
+
### Document Gallery
|
|
971
|
+
|
|
972
|
+
`framework/js/navigation/document-gallery.js` — Collapsible sidebar gallery for reference documents.
|
|
973
|
+
|
|
974
|
+
**Build pipeline:** `vite-plugin-content-discovery.js` scans `course/assets/docs/` and generates `_gallery-manifest.json` at build time. In dev mode, `preview-server.js` generates the manifest dynamically.
|
|
975
|
+
|
|
976
|
+
**Key files:**
|
|
977
|
+
|
|
978
|
+
| File | Purpose |
|
|
979
|
+
|------|---------|
|
|
980
|
+
| `framework/js/navigation/document-gallery.js` | Fetches manifest, renders thumbnails, expand/collapse logic |
|
|
981
|
+
| `framework/css/components/document-gallery.css` | 2-column grid, thumbnail variants |
|
|
982
|
+
| `lib/vite-plugin-content-discovery.js` | `generateGalleryManifest()` — build-time discovery |
|
|
983
|
+
|
|
984
|
+
**Behavior:** Expanding gallery collapses nav menu (inverse toggle). Gallery resets to collapsed on sidebar `transitionend` close. Configured via `navigation.documentGallery` in `course-config.js`.
|
|
985
|
+
|
|
986
|
+
---
|
|
987
|
+
|
|
988
|
+
## Icons
|
|
989
|
+
|
|
990
|
+
`framework/js/utilities/icons.js` provides a centralized icon registry. Icons are rendered as SVG strings with no inline width/height - sizing is controlled via CSS classes.
|
|
991
|
+
|
|
992
|
+
### IconManager API
|
|
993
|
+
|
|
994
|
+
```javascript
|
|
995
|
+
const { iconManager } = CourseCode;
|
|
996
|
+
|
|
997
|
+
// Basic usage
|
|
998
|
+
iconManager.getIcon('menu'); // Returns SVG string
|
|
999
|
+
|
|
1000
|
+
// With options
|
|
1001
|
+
iconManager.getIcon('check', {
|
|
1002
|
+
size: 'lg', // 'xs'|'sm'|'md'|'lg'|'xl'|'2xl'|'3xl' or px: 12|16|20|24|32|48|64
|
|
1003
|
+
class: 'icon-success', // Additional CSS classes
|
|
1004
|
+
strokeWidth: 2, // SVG stroke width (default: 2)
|
|
1005
|
+
color: 'currentColor' // Stroke color (default: currentColor)
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
// Register custom icons
|
|
1009
|
+
iconManager.register('custom-icon', '<path d="..." />');
|
|
1010
|
+
iconManager.registerAll({ icon1: '...', icon2: '...' });
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
### Extending via Course
|
|
1014
|
+
|
|
1015
|
+
Course authors add custom icons in `course/icons.js`:
|
|
1016
|
+
|
|
1017
|
+
```javascript
|
|
1018
|
+
export const customIcons = {
|
|
1019
|
+
'rocket': '<path d="..." />'
|
|
1020
|
+
};
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
These are automatically registered and available via `iconManager.getIcon('rocket')`.
|
|
1024
|
+
|
|
1025
|
+
---
|
|
1026
|
+
|
|
1027
|
+
## Breakpoint Manager
|
|
1028
|
+
|
|
1029
|
+
`framework/js/utilities/breakpoint-manager.js` - Dynamically applies responsive CSS classes to `<html>`.
|
|
1030
|
+
|
|
1031
|
+
### Breakpoints
|
|
1032
|
+
|
|
1033
|
+
| Class | Condition | Width |
|
|
1034
|
+
|-------|-----------|-------|
|
|
1035
|
+
| `.bp-min-large-desktop` | ≥1440px | Large screens |
|
|
1036
|
+
| `.bp-max-desktop` | ≤1439px | Desktop and below |
|
|
1037
|
+
| `.bp-max-tablet-landscape` | ≤1199px | Tablet landscape |
|
|
1038
|
+
| `.bp-max-tablet-portrait` | ≤1023px | Tablet portrait |
|
|
1039
|
+
| `.bp-max-mobile-landscape` | ≤767px | Mobile landscape |
|
|
1040
|
+
| `.bp-max-mobile-portrait` | ≤479px | Mobile portrait |
|
|
1041
|
+
|
|
1042
|
+
Multiple classes cascade (e.g., at 600px: `.bp-max-desktop`, `.bp-max-tablet-landscape`, `.bp-max-tablet-portrait`, `.bp-max-mobile-landscape` all applied).
|
|
1043
|
+
|
|
1044
|
+
### API
|
|
1045
|
+
|
|
1046
|
+
```javascript
|
|
1047
|
+
import { breakpointManager } from './utilities/breakpoint-manager.js';
|
|
1048
|
+
|
|
1049
|
+
breakpointManager.getCurrentBreakpoint(); // 'tablet-portrait'
|
|
1050
|
+
breakpointManager.isMobile(); // true if ≤767px
|
|
1051
|
+
breakpointManager.isTablet(); // true if 768-1199px
|
|
1052
|
+
breakpointManager.isDesktop(); // true if ≥1200px
|
|
1053
|
+
breakpointManager.isAtMost('tablet-portrait'); // true if ≤1023px
|
|
1054
|
+
breakpointManager.onChange((newBp, oldBp) => { /* handle */ });
|
|
1055
|
+
breakpointManager.refresh(); // Force re-evaluation
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
Exposed globally via `CourseCode.breakpointManager`.
|
|
1059
|
+
|
|
1060
|
+
---
|
|
1061
|
+
|
|
1062
|
+
## Adding New Interaction Types
|
|
1063
|
+
|
|
1064
|
+
> **Course Authors:** See `course/interactions/PLUGIN_GUIDE.md`. Steps below are for framework developers.
|
|
1065
|
+
|
|
1066
|
+
1. Create file in `framework/js/components/interactions/`
|
|
1067
|
+
2. Export `create(container, config)`, `metadata`, and `schema`
|
|
1068
|
+
3. Register with `catalogInteraction()` in `core/interaction-catalog.js` (auto-discovered via `import.meta.glob`)
|
|
1069
|
+
4. Add CSS to `framework/css/interactions/`
|
|
1070
|
+
5. Update automation API if needed for testing support
|
|
1071
|
+
6. Add to `COURSE_AUTHORING_GUIDE.md` if has author-facing classes
|
|
1072
|
+
|
|
1073
|
+
### Required Contract
|
|
1074
|
+
|
|
1075
|
+
```javascript
|
|
1076
|
+
export function create(container, config) {
|
|
1077
|
+
// config must have: id, (type-specific props)
|
|
1078
|
+
// Must set data-interaction-id on root element
|
|
1079
|
+
// Must emit events for state changes
|
|
1080
|
+
// Must support getResponse() / setResponse() for automation
|
|
1081
|
+
return {
|
|
1082
|
+
getResponse: () => currentResponse,
|
|
1083
|
+
setResponse: (val) => { /* update UI and state */ },
|
|
1084
|
+
checkAnswer: () => ({ correct, score, feedback }),
|
|
1085
|
+
reset: () => { /* clear to initial state */ }
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
```
|