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,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file modal.js
|
|
3
|
+
* @description Dynamic modal management system with audio support.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import * as Modal from './modal.js';
|
|
7
|
+
*
|
|
8
|
+
* // In main application entry point:
|
|
9
|
+
* Modal.setup();
|
|
10
|
+
*
|
|
11
|
+
* // To show a modal:
|
|
12
|
+
* Modal.show({
|
|
13
|
+
* title: 'My Title',
|
|
14
|
+
* body: '<p>My content.</p>',
|
|
15
|
+
* footer: '<button data-action="close-modal">Close</button>',
|
|
16
|
+
* config: { closeOnBackdrop: true, closeOnEscape: true },
|
|
17
|
+
* audio: {
|
|
18
|
+
* src: 'audio/modal-narration.mp3',
|
|
19
|
+
* autoplay: true,
|
|
20
|
+
* required: true, // Audio must complete before modal counts as viewed
|
|
21
|
+
* completionThreshold: 0.9 // 90% listened = complete
|
|
22
|
+
* },
|
|
23
|
+
* onOpen: () => console.log('Modal opened!'),
|
|
24
|
+
* onClose: () => console.log('Modal closed!'),
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* Declarative Audio:
|
|
28
|
+
* <button data-modal-trigger="my-modal"
|
|
29
|
+
* data-audio-src="audio/modal.mp3"
|
|
30
|
+
* data-audio-required="true"
|
|
31
|
+
* data-audio-threshold="0.9">
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { iconManager } from '../../utilities/icons.js';
|
|
35
|
+
import { announceToScreenReader } from './index.js';
|
|
36
|
+
import { trapFocus } from '../../utilities/utilities.js';
|
|
37
|
+
import audioManager from '../../managers/audio-manager.js';
|
|
38
|
+
import engagementManager from '../../engagement/engagement-manager.js';
|
|
39
|
+
import * as NavigationState from '../../navigation/NavigationState.js';
|
|
40
|
+
import * as AudioPlayer from './audio-player.js';
|
|
41
|
+
import { eventBus } from '../../core/event-bus.js';
|
|
42
|
+
import { logger } from '../../utilities/logger.js';
|
|
43
|
+
import { renderCompactPlayer } from './audio-player.js';
|
|
44
|
+
|
|
45
|
+
// Schema for validation, linting, and AI-assisted authoring
|
|
46
|
+
export const schema = {
|
|
47
|
+
type: 'modal-trigger',
|
|
48
|
+
description: 'Dynamic modal with audio support and focus trapping',
|
|
49
|
+
example: '<button data-component=\'modal-trigger\' data-title=\'Welcome\' data-body=\'<p>This modal supports rich content, audio narration, and focus trapping for accessibility.</p>\' class=\'btn btn-primary\'>Open Modal</button>',
|
|
50
|
+
properties: {
|
|
51
|
+
closeOnBackdrop: { type: 'boolean', default: true, description: 'Close when clicking backdrop' },
|
|
52
|
+
closeOnEscape: { type: 'boolean', default: true, description: 'Close on Escape key' },
|
|
53
|
+
hideCloseButton: { type: 'boolean', default: false, description: 'Hide the X close button' }
|
|
54
|
+
},
|
|
55
|
+
structure: {
|
|
56
|
+
trigger: '[data-modal-trigger], [data-component="modal-trigger"]',
|
|
57
|
+
modal: '#global-modal',
|
|
58
|
+
backdrop: '.modal-backdrop'
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const metadata = {
|
|
63
|
+
category: 'ui-component',
|
|
64
|
+
cssFile: 'components/modals.css',
|
|
65
|
+
engagementTracking: 'viewAllModals',
|
|
66
|
+
emitsEvents: ['modal:opened', 'modal:closed']
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let modalElement = null;
|
|
70
|
+
let modalTitle = null;
|
|
71
|
+
let modalBody = null;
|
|
72
|
+
let modalFooter = null;
|
|
73
|
+
let backdropElement = null;
|
|
74
|
+
|
|
75
|
+
let activeConfig = {};
|
|
76
|
+
let previousFocus = null;
|
|
77
|
+
let isInitialized = false;
|
|
78
|
+
let currentModalId = null; // Track current modal for audio completion
|
|
79
|
+
let audioCompletedHandler = null; // Audio completion event handler
|
|
80
|
+
let currentSlideId = null; // Track slide ID for engagement tracking
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Checks if the modal is currently visible.
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
function isVisible() {
|
|
87
|
+
return modalElement && modalElement.classList.contains('active');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Initializes the modal system. Must be called once on app startup.
|
|
92
|
+
*/
|
|
93
|
+
export function setup() {
|
|
94
|
+
if (isInitialized) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
modalElement = document.getElementById('global-modal');
|
|
99
|
+
backdropElement = document.querySelector('.modal-backdrop');
|
|
100
|
+
|
|
101
|
+
// Inject standard close icon
|
|
102
|
+
if (modalElement) {
|
|
103
|
+
const closeBtn = modalElement.querySelector('.modal-close');
|
|
104
|
+
if (closeBtn) {
|
|
105
|
+
closeBtn.innerHTML = iconManager.getIcon('x');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!modalElement || !backdropElement) {
|
|
110
|
+
logger.fatal('Modal elements (#global-modal, .modal-backdrop) not found in the DOM.', { domain: 'ui', operation: 'Modal.setup' });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
modalTitle = document.getElementById('global-modal-title');
|
|
115
|
+
modalBody = document.getElementById('global-modal-body');
|
|
116
|
+
modalFooter = document.getElementById('global-modal-footer');
|
|
117
|
+
|
|
118
|
+
// Close button handler (delegated to handle dynamic content)
|
|
119
|
+
modalElement.addEventListener('click', (event) => {
|
|
120
|
+
if (event.target.closest('[data-action="close-modal"]')) {
|
|
121
|
+
hide();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Backdrop click handler
|
|
126
|
+
backdropElement.addEventListener('click', () => {
|
|
127
|
+
if (activeConfig.closeOnBackdrop) {
|
|
128
|
+
hide();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Global Escape key handler
|
|
133
|
+
document.addEventListener('keydown', (e) => {
|
|
134
|
+
if (e.key === 'Escape' && isVisible() && activeConfig.closeOnEscape) {
|
|
135
|
+
hide();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
isInitialized = true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Initializes a single declarative modal trigger.
|
|
144
|
+
* @param {HTMLElement} trigger
|
|
145
|
+
*/
|
|
146
|
+
export function init(trigger) {
|
|
147
|
+
trigger.addEventListener('click', () => {
|
|
148
|
+
const title = trigger.dataset.title || 'Modal';
|
|
149
|
+
let body = trigger.dataset.body || '';
|
|
150
|
+
let footer = trigger.dataset.footer || '<button class="btn btn-secondary" data-action="close-modal">Close</button>';
|
|
151
|
+
|
|
152
|
+
// If body/footer starts with #, try to find element and use its HTML
|
|
153
|
+
// Handle both regular elements and <template> elements
|
|
154
|
+
if (body.startsWith('#')) {
|
|
155
|
+
const el = document.querySelector(body);
|
|
156
|
+
if (el) {
|
|
157
|
+
if (el.tagName === 'TEMPLATE') {
|
|
158
|
+
// For <template> elements, clone content and extract HTML
|
|
159
|
+
const tempDiv = document.createElement('div');
|
|
160
|
+
tempDiv.appendChild(el.content.cloneNode(true));
|
|
161
|
+
body = tempDiv.innerHTML;
|
|
162
|
+
} else {
|
|
163
|
+
body = el.innerHTML;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (footer.startsWith('#')) {
|
|
168
|
+
const el = document.querySelector(footer);
|
|
169
|
+
if (el) {
|
|
170
|
+
if (el.tagName === 'TEMPLATE') {
|
|
171
|
+
const tempDiv = document.createElement('div');
|
|
172
|
+
tempDiv.appendChild(el.content.cloneNode(true));
|
|
173
|
+
footer = tempDiv.innerHTML;
|
|
174
|
+
} else {
|
|
175
|
+
footer = el.innerHTML;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check for audio configuration on trigger
|
|
181
|
+
const audioSrc = trigger.dataset.audioSrc;
|
|
182
|
+
const audioConfig = audioSrc ? {
|
|
183
|
+
src: audioSrc,
|
|
184
|
+
autoplay: trigger.dataset.audioAutoplay === 'true',
|
|
185
|
+
required: trigger.dataset.audioRequired === 'true',
|
|
186
|
+
completionThreshold: parseFloat(trigger.dataset.audioThreshold) || 0.95
|
|
187
|
+
} : null;
|
|
188
|
+
|
|
189
|
+
// Get modal ID for tracking (from trigger id or generate one)
|
|
190
|
+
const modalId = trigger.dataset.modalId || trigger.id || `modal-${Date.now()}`;
|
|
191
|
+
|
|
192
|
+
show({
|
|
193
|
+
title,
|
|
194
|
+
body,
|
|
195
|
+
footer,
|
|
196
|
+
audio: audioConfig,
|
|
197
|
+
modalId,
|
|
198
|
+
config: {
|
|
199
|
+
closeOnBackdrop: trigger.dataset.closeOnBackdrop !== 'false',
|
|
200
|
+
closeOnEscape: trigger.dataset.closeOnEscape !== 'false'
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Shows the global modal with dynamic content.
|
|
208
|
+
* @param {object} options - The modal configuration.
|
|
209
|
+
* @param {string} options.title - The text for the modal title.
|
|
210
|
+
* @param {string} options.body - The HTML string for the modal body.
|
|
211
|
+
* @param {string} [options.footer] - The HTML string for the modal footer.
|
|
212
|
+
* @param {string} [options.modalId] - Unique identifier for this modal (for engagement tracking).
|
|
213
|
+
* @param {object} [options.config] - Behavior configuration.
|
|
214
|
+
* @param {boolean} [options.config.closeOnBackdrop=true] - If true, clicking the backdrop closes the modal.
|
|
215
|
+
* @param {boolean} [options.config.closeOnEscape=true] - If true, pressing Escape closes the modal.
|
|
216
|
+
* @param {boolean} [options.config.hideCloseButton=false] - If true, hides the modal's close (X) button.
|
|
217
|
+
* @param {object} [options.audio] - Audio configuration for modal narration.
|
|
218
|
+
* @param {string} options.audio.src - Audio file source path.
|
|
219
|
+
* @param {boolean} [options.audio.autoplay=true] - Whether to autoplay the audio.
|
|
220
|
+
* @param {boolean} [options.audio.required=false] - Whether audio must complete for modal engagement.
|
|
221
|
+
* @param {number} [options.audio.completionThreshold=0.95] - Percentage (0-1) for completion.
|
|
222
|
+
* @param {Function} [options.onOpen] - Callback executed when the modal opens.
|
|
223
|
+
* @param {Function} [options.onClose] - Callback executed when the modal closes.
|
|
224
|
+
*/
|
|
225
|
+
export async function show({ title, body, footer = '', modalId = null, config = {}, audio = null, onOpen, onClose }) {
|
|
226
|
+
if (!isInitialized) {
|
|
227
|
+
throw new Error('Modal system not initialized. Call setup() first.');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Generate modal ID if not provided
|
|
231
|
+
currentModalId = modalId || `modal-${Date.now()}`;
|
|
232
|
+
|
|
233
|
+
// Store config for this active session
|
|
234
|
+
activeConfig = {
|
|
235
|
+
closeOnBackdrop: config.closeOnBackdrop !== false,
|
|
236
|
+
closeOnEscape: config.closeOnEscape !== false,
|
|
237
|
+
hideCloseButton: config.hideCloseButton === true,
|
|
238
|
+
audio,
|
|
239
|
+
modalId: currentModalId,
|
|
240
|
+
onOpen,
|
|
241
|
+
onClose,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Show/hide the close button based on config
|
|
245
|
+
const closeBtn = modalElement.querySelector('.modal-close');
|
|
246
|
+
if (closeBtn) {
|
|
247
|
+
closeBtn.style.display = activeConfig.hideCloseButton ? 'none' : '';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Populate content
|
|
251
|
+
modalTitle.textContent = title;
|
|
252
|
+
modalBody.innerHTML = body;
|
|
253
|
+
|
|
254
|
+
// If audio is present, prepend compact audio player to footer
|
|
255
|
+
if (audio && audio.src) {
|
|
256
|
+
const compactAudioHtml = renderCompactPlayer();
|
|
257
|
+
modalFooter.innerHTML = compactAudioHtml + footer;
|
|
258
|
+
} else {
|
|
259
|
+
modalFooter.innerHTML = footer;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Save current focus
|
|
263
|
+
previousFocus = document.activeElement;
|
|
264
|
+
|
|
265
|
+
// Save the current slide ID (for engagement tracking)
|
|
266
|
+
currentSlideId = NavigationState.getCurrentSlideId();
|
|
267
|
+
|
|
268
|
+
// Track modal view for engagement (if this modal is registered for tracking)
|
|
269
|
+
if (currentSlideId && currentModalId) {
|
|
270
|
+
engagementManager.trackModalView(currentSlideId, currentModalId);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Handle audio: load modal audio
|
|
274
|
+
// Note: Due to singleton audio element, slide audio and modal audio are mutually exclusive
|
|
275
|
+
// (enforced by runtime-linter - a slide cannot have both)
|
|
276
|
+
if (audioManager.isReady() && audio && audio.src) {
|
|
277
|
+
const audioContextId = `modal-${currentModalId}`;
|
|
278
|
+
try {
|
|
279
|
+
await audioManager.load({
|
|
280
|
+
src: audio.src,
|
|
281
|
+
autoplay: audio.autoplay === true,
|
|
282
|
+
required: audio.required || false,
|
|
283
|
+
completionThreshold: audio.completionThreshold || 0.95
|
|
284
|
+
}, audioContextId, 'modal');
|
|
285
|
+
|
|
286
|
+
logger.debug(`[Modal] Loaded modal audio: ${audioContextId}`);
|
|
287
|
+
|
|
288
|
+
// Initialize event listeners AFTER audio is loaded
|
|
289
|
+
if (modalFooter) {
|
|
290
|
+
AudioPlayer.initAudioControlsInContainer(modalFooter);
|
|
291
|
+
}
|
|
292
|
+
} catch (err) {
|
|
293
|
+
logger.warn('[Modal] Failed to load modal audio:', err.message);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// If audio is required, listen for completion
|
|
297
|
+
if (audio.required) {
|
|
298
|
+
audioCompletedHandler = ({ contextId }) => {
|
|
299
|
+
if (contextId === audioContextId && currentSlideId) {
|
|
300
|
+
engagementManager.trackModalAudioComplete(currentSlideId, currentModalId);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
eventBus.on('audio:completed', audioCompletedHandler);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Show modal and backdrop
|
|
308
|
+
backdropElement.classList.add('active');
|
|
309
|
+
modalElement.classList.add('active');
|
|
310
|
+
modalElement.setAttribute('aria-hidden', 'false');
|
|
311
|
+
|
|
312
|
+
// Focus first focusable element
|
|
313
|
+
setTimeout(() => {
|
|
314
|
+
const focusable = modalElement.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
315
|
+
if (focusable.length > 0) {
|
|
316
|
+
focusable[0].focus();
|
|
317
|
+
}
|
|
318
|
+
}, 100);
|
|
319
|
+
|
|
320
|
+
// Setup focus trap
|
|
321
|
+
modalElement._focusTrapCleanup = trapFocus(modalElement);
|
|
322
|
+
|
|
323
|
+
if (activeConfig.onOpen) {
|
|
324
|
+
activeConfig.onOpen(modalElement);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
announceToScreenReader(`Modal opened: ${title}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Hides the global modal.
|
|
332
|
+
*/
|
|
333
|
+
export function hide() {
|
|
334
|
+
if (!isInitialized || !modalElement.classList.contains('active')) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
modalElement.classList.remove('active');
|
|
339
|
+
modalElement.setAttribute('aria-hidden', 'true');
|
|
340
|
+
backdropElement.classList.remove('active');
|
|
341
|
+
|
|
342
|
+
// Clean up focus trap
|
|
343
|
+
if (modalElement._focusTrapCleanup && typeof modalElement._focusTrapCleanup === 'function') {
|
|
344
|
+
modalElement._focusTrapCleanup();
|
|
345
|
+
modalElement._focusTrapCleanup = null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Clean up audio completion listener
|
|
349
|
+
if (audioCompletedHandler) {
|
|
350
|
+
eventBus.off('audio:completed', audioCompletedHandler);
|
|
351
|
+
audioCompletedHandler = null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Clean up audio state update listeners from modal footer
|
|
355
|
+
if (modalFooter._audioStateUpdateCleanup && typeof modalFooter._audioStateUpdateCleanup === 'function') {
|
|
356
|
+
modalFooter._audioStateUpdateCleanup();
|
|
357
|
+
modalFooter._audioStateUpdateCleanup = null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Handle audio: unload modal audio
|
|
361
|
+
// Note: No slide audio to restore (they're mutually exclusive, enforced by runtime-linter)
|
|
362
|
+
if (audioManager.isReady() && activeConfig.audio) {
|
|
363
|
+
audioManager.unload();
|
|
364
|
+
logger.debug('[Modal] Unloaded modal audio');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Restore focus
|
|
368
|
+
if (previousFocus) {
|
|
369
|
+
previousFocus.focus();
|
|
370
|
+
previousFocus = null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Fire onClose callback
|
|
374
|
+
if (activeConfig.onClose) {
|
|
375
|
+
activeConfig.onClose(modalElement);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Clear content and config for next use
|
|
379
|
+
modalTitle.textContent = '';
|
|
380
|
+
modalBody.innerHTML = '';
|
|
381
|
+
modalFooter.innerHTML = '';
|
|
382
|
+
currentModalId = null;
|
|
383
|
+
activeConfig = {};
|
|
384
|
+
|
|
385
|
+
announceToScreenReader('Modal closed');
|
|
386
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file notifications.js
|
|
3
|
+
* @description Notification system with event delegation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const schema = {
|
|
7
|
+
type: 'notification-trigger',
|
|
8
|
+
description: 'Declarative notification trigger',
|
|
9
|
+
example: `<button data-action="show-notification" data-type="success" data-message="Your progress has been saved!" class="btn btn-primary" style="margin-right: 8px;">Success</button>
|
|
10
|
+
<button data-action="show-notification" data-type="warning" data-message="You have unsaved changes." class="btn btn-secondary" style="margin-right: 8px;">Warning</button>
|
|
11
|
+
<button data-action="show-notification" data-type="error" data-message="Connection lost. Please try again." class="btn btn-secondary">Error</button>`,
|
|
12
|
+
properties: {
|
|
13
|
+
type: { type: 'string', enum: ['info', 'success', 'warning', 'error'], default: 'info', dataAttribute: 'data-type' },
|
|
14
|
+
message: { type: 'string', required: true, dataAttribute: 'data-message' }
|
|
15
|
+
},
|
|
16
|
+
structure: {
|
|
17
|
+
container: '[data-action="show-notification"]',
|
|
18
|
+
children: {}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const metadata = {
|
|
23
|
+
category: 'ui-component',
|
|
24
|
+
cssFile: 'components/notifications.css',
|
|
25
|
+
engagementTracking: null,
|
|
26
|
+
emitsEvents: []
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
import { announceToScreenReader } from './index.js';
|
|
30
|
+
import { logger } from '../../utilities/logger.js';
|
|
31
|
+
|
|
32
|
+
let notificationContainer = null;
|
|
33
|
+
let notificationId = 0;
|
|
34
|
+
let initialized = false;
|
|
35
|
+
|
|
36
|
+
export function setup() {
|
|
37
|
+
if (initialized) return;
|
|
38
|
+
|
|
39
|
+
notificationContainer = document.getElementById('notification-container');
|
|
40
|
+
if (!notificationContainer) {
|
|
41
|
+
logger.fatal('Notification container with ID "notification-container" not found in DOM.', { domain: 'ui', operation: 'Notifications.setup' });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Add a single, permanent delegated click listener
|
|
46
|
+
notificationContainer.addEventListener('click', (event) => {
|
|
47
|
+
const closeButton = event.target.closest('[data-action="dismiss-notification"]');
|
|
48
|
+
if (closeButton) {
|
|
49
|
+
const notification = closeButton.closest('.notification');
|
|
50
|
+
if (notification) {
|
|
51
|
+
dismissNotification(notification.id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
initialized = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Initializes declarative notification triggers in a container using event delegation.
|
|
61
|
+
* @param {HTMLElement} container
|
|
62
|
+
*/
|
|
63
|
+
export function init(container) {
|
|
64
|
+
// Use event delegation to handle dynamically rendered content
|
|
65
|
+
container.addEventListener('click', (event) => {
|
|
66
|
+
const trigger = event.target.closest('[data-action="show-notification"]');
|
|
67
|
+
if (trigger) {
|
|
68
|
+
const type = trigger.dataset.type || 'info';
|
|
69
|
+
const message = trigger.dataset.message || 'Notification';
|
|
70
|
+
showNotification(message, type);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function showNotification(message, type = 'info', duration = 5000, options = {}) {
|
|
76
|
+
if (!initialized) {
|
|
77
|
+
setup();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!message || typeof message !== 'string') {
|
|
81
|
+
throw new Error('Notification message must be a non-empty string');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const id = `notification-${++notificationId}`;
|
|
85
|
+
const notification = document.createElement('div');
|
|
86
|
+
|
|
87
|
+
notification.id = id;
|
|
88
|
+
notification.className = `notification notification-${type}`;
|
|
89
|
+
notification.setAttribute('role', type === 'error' ? 'alert' : 'status');
|
|
90
|
+
notification.setAttribute('data-testid', `notification-${type}`);
|
|
91
|
+
|
|
92
|
+
const dismissible = options.dismissible !== false;
|
|
93
|
+
const closeButton = dismissible ? `
|
|
94
|
+
<button class="notification-close" data-action="dismiss-notification" aria-label="Close notification" data-testid="notification-close">×</button>
|
|
95
|
+
` : '';
|
|
96
|
+
|
|
97
|
+
notification.innerHTML = `
|
|
98
|
+
<span class="notification-message">${message}</span>
|
|
99
|
+
${closeButton}
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
notificationContainer.appendChild(notification);
|
|
103
|
+
|
|
104
|
+
if (duration > 0) {
|
|
105
|
+
setTimeout(() => dismissNotification(id), duration);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
announceToScreenReader(message, type === 'error' ? 'assertive' : 'polite');
|
|
109
|
+
|
|
110
|
+
return id;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function dismissNotification(id) {
|
|
114
|
+
const notification = document.getElementById(id);
|
|
115
|
+
if (!notification) return;
|
|
116
|
+
|
|
117
|
+
notification.style.animation = 'notificationSlideOut 0.3s ease-out forwards';
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
if (notification.parentNode) {
|
|
120
|
+
notification.parentNode.removeChild(notification);
|
|
121
|
+
}
|
|
122
|
+
}, 300);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function clearAllNotifications() {
|
|
126
|
+
if (!notificationContainer) return;
|
|
127
|
+
const notifications = notificationContainer.querySelectorAll('.notification');
|
|
128
|
+
notifications.forEach(notification => dismissNotification(notification.id));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function showSuccess(message, duration = 4000, options = {}) {
|
|
132
|
+
return showNotification(message, 'success', duration, options);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function showWarning(message, duration = 6000, options = {}) {
|
|
136
|
+
return showNotification(message, 'warning', duration, options);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function showError(message, duration = 8000, options = {}) {
|
|
140
|
+
return showNotification(message, 'error', duration, options);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function showInfo(message, duration = 5000, options = {}) {
|
|
144
|
+
return showNotification(message, 'info', duration, options);
|
|
145
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file progress.js
|
|
3
|
+
* @description Handles progress bar components.
|
|
4
|
+
*
|
|
5
|
+
* Usage (Declarative):
|
|
6
|
+
* <div class="progress-bar" data-component="progress" id="my-progress" data-initial-value="25">
|
|
7
|
+
* <div class="progress-bar-fill"></div>
|
|
8
|
+
* <span class="progress-bar-text">25%</span>
|
|
9
|
+
* </div>
|
|
10
|
+
*
|
|
11
|
+
* Usage (Imperative):
|
|
12
|
+
* import { updateProgress } from '...';
|
|
13
|
+
* updateProgress('my-progress', 50);
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const schema = {
|
|
17
|
+
type: 'progress',
|
|
18
|
+
description: 'Animated progress bar with percentage display',
|
|
19
|
+
example: `<div class="progress-bar" data-component="progress" id="preview-progress" data-initial-value="65">
|
|
20
|
+
<div class="progress-bar-fill" style="width: 65%"></div>
|
|
21
|
+
<span class="progress-bar-text">65%</span>
|
|
22
|
+
</div>`,
|
|
23
|
+
properties: {
|
|
24
|
+
initialValue: { type: 'number', default: 0, dataAttribute: 'data-initial-value' }
|
|
25
|
+
},
|
|
26
|
+
structure: {
|
|
27
|
+
container: '[data-component="progress"]',
|
|
28
|
+
children: {
|
|
29
|
+
fill: { selector: '.progress-bar-fill', required: true },
|
|
30
|
+
text: { selector: '.progress-bar-text', required: false }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const metadata = {
|
|
36
|
+
category: 'ui-component',
|
|
37
|
+
cssFile: 'components/engagement.css',
|
|
38
|
+
engagementTracking: null,
|
|
39
|
+
emitsEvents: []
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sets the value of a progress bar.
|
|
44
|
+
* @param {HTMLElement|string} elementOrId - The progress bar container element or its ID.
|
|
45
|
+
* @param {number} value - The progress value (0-100).
|
|
46
|
+
*/
|
|
47
|
+
import { logger } from '../../utilities/logger.js';
|
|
48
|
+
|
|
49
|
+
export function updateProgress(elementOrId, value) {
|
|
50
|
+
const element = typeof elementOrId === 'string' ? document.getElementById(elementOrId) : elementOrId;
|
|
51
|
+
if (!element) {
|
|
52
|
+
logger.error('updateProgress: Element not found.', elementOrId);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const fill = element.querySelector('.progress-bar-fill');
|
|
57
|
+
const text = element.querySelector('.progress-bar-text');
|
|
58
|
+
const clampedValue = Math.max(0, Math.min(100, value));
|
|
59
|
+
|
|
60
|
+
element.style.setProperty('--progress-percent', `${clampedValue}%`);
|
|
61
|
+
element.setAttribute('aria-valuenow', clampedValue);
|
|
62
|
+
|
|
63
|
+
if (fill) {
|
|
64
|
+
fill.style.width = `${clampedValue}%`;
|
|
65
|
+
}
|
|
66
|
+
if (text) {
|
|
67
|
+
text.textContent = `${Math.round(clampedValue)}%`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Initializes a progress bar component, setting its initial value.
|
|
73
|
+
* @param {HTMLElement} element - The progress bar container element.
|
|
74
|
+
*/
|
|
75
|
+
export function init(element) {
|
|
76
|
+
if (!element) {
|
|
77
|
+
logger.fatal('initProgress: element not found.', { domain: 'ui', operation: 'initProgress' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const initialValue = parseFloat(element.dataset.initialValue) || 0;
|
|
82
|
+
updateProgress(element, initialValue);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
update: (value) => updateProgress(element, value),
|
|
86
|
+
destroy: () => {} // No listeners to remove
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quote/Testimonial Layout Pattern
|
|
3
|
+
*
|
|
4
|
+
* CSS-only component for customer quotes, testimonials, citations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const schema = {
|
|
8
|
+
type: 'quote',
|
|
9
|
+
description: 'Quote/testimonial display with attribution',
|
|
10
|
+
example: `<div data-component="quote">
|
|
11
|
+
<p class="quote-text">"This framework has transformed how we create training content. It's intuitive, powerful, and makes our courses look professional."</p>
|
|
12
|
+
<div class="quote-attribution"><span class="quote-author">Jane Smith</span><span class="quote-role">Director of Training</span></div>
|
|
13
|
+
</div>`,
|
|
14
|
+
properties: {
|
|
15
|
+
variant: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
enum: ['default', 'card', 'accent', 'featured', 'dark'],
|
|
18
|
+
default: 'default',
|
|
19
|
+
description: 'Visual style variant (add as class, e.g., quote-card)'
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
structure: {
|
|
23
|
+
container: '[data-component="quote"]',
|
|
24
|
+
children: {
|
|
25
|
+
text: { selector: '.quote-text', required: true },
|
|
26
|
+
attribution: { selector: '.quote-attribution' },
|
|
27
|
+
avatar: { selector: '.quote-avatar' },
|
|
28
|
+
author: { selector: '.quote-author' },
|
|
29
|
+
role: { selector: '.quote-role' }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const metadata = {
|
|
35
|
+
category: 'ui-component',
|
|
36
|
+
cssOnly: true,
|
|
37
|
+
cssFile: 'components/quote.css'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** No-op initializer — CSS-only component, registered for consistency. */
|
|
41
|
+
export function init() {}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stats/Metrics Layout Pattern
|
|
3
|
+
*
|
|
4
|
+
* CSS-only component for displaying important numbers/statistics.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const schema = {
|
|
8
|
+
type: 'stats',
|
|
9
|
+
description: 'Statistics/metrics display with large numbers',
|
|
10
|
+
example: `<div data-component="stats">
|
|
11
|
+
<div class="stat"><span class="stat-value">150+</span><span class="stat-label">Active Courses</span></div>
|
|
12
|
+
<div class="stat"><span class="stat-value">98%</span><span class="stat-label">Completion Rate</span></div>
|
|
13
|
+
<div class="stat"><span class="stat-value">4.9</span><span class="stat-label">Average Rating</span></div>
|
|
14
|
+
</div>`,
|
|
15
|
+
properties: {},
|
|
16
|
+
structure: {
|
|
17
|
+
container: '[data-component="stats"]',
|
|
18
|
+
children: {
|
|
19
|
+
stat: { selector: '.stat', required: true, minItems: 1 },
|
|
20
|
+
value: { selector: '.stat-value', parent: '.stat', required: true },
|
|
21
|
+
label: { selector: '.stat-label', parent: '.stat' }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const metadata = {
|
|
27
|
+
category: 'ui-component',
|
|
28
|
+
cssOnly: true,
|
|
29
|
+
cssFile: 'components/stats.css'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** No-op initializer — CSS-only component, registered for consistency. */
|
|
33
|
+
export function init() {}
|