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,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
|
+
}
|