coursecode 0.1.41 → 0.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/framework/css/02-layout.css +4 -4
  2. package/framework/css/components/audio-player.css +1 -1
  3. package/framework/css/components/footer.css +2 -2
  4. package/framework/css/design-tokens.css +17 -10
  5. package/framework/css/layouts/base.css +8 -0
  6. package/framework/css/layouts/focused.css +3 -3
  7. package/framework/css/layouts/presentation.css +1 -1
  8. package/framework/css/responsive-structure.css +10 -2
  9. package/framework/index.html +2 -2
  10. package/framework/js/core/event-bus.js +1 -1
  11. package/framework/js/drivers/scorm-2004-driver.js +12 -14
  12. package/framework/js/utilities/view-manager.js +35 -4
  13. package/framework/js/vendor/pipwerks.js +4 -8
  14. package/framework/scripts/generate-narration.js +11 -3
  15. package/lib/stub-player/config-panel.js +1 -1
  16. package/package.json +1 -1
  17. package/template/.narration-cache.json +6 -0
  18. package/template/course/assets/audio/example-ui-showcase--compact-player.mp3 +0 -0
  19. package/template/course/assets/audio/example-ui-showcase--demo-modal.mp3 +0 -0
  20. package/template/course/assets/audio/example-ui-showcase--full-player.mp3 +0 -0
  21. package/template/course/assets/audio/example-welcome.mp3 +0 -0
  22. package/template/course/course-config.js +5 -0
  23. package/template/course/slides/example-ui-showcase.js +6 -6
  24. package/template/course/slides/example-welcome.js +10 -0
  25. package/template/course/theme.css +11 -0
  26. package/template/course/assets/audio/example-intro.mp3 +0 -0
  27. package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
  28. package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
  29. package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
@@ -31,7 +31,7 @@
31
31
 
32
32
  /* App Layout - Fixed Frame Structure */
33
33
  #app {
34
- max-width: 1400px;
34
+ max-width: var(--course-shell-max-width);
35
35
  margin: 0 auto;
36
36
  background: var(--bg-surface);
37
37
  box-shadow: var(--shadow-md);
@@ -325,19 +325,19 @@ main#content>section {
325
325
  while max-width constrains the upper bound. */
326
326
  .content-narrow {
327
327
  width: 100%;
328
- max-width: 700px;
328
+ max-width: var(--content-width-narrow);
329
329
  margin: 0 auto;
330
330
  }
331
331
 
332
332
  .content-medium {
333
333
  width: 100%;
334
- max-width: 900px;
334
+ max-width: var(--content-width-medium);
335
335
  margin: 0 auto;
336
336
  }
337
337
 
338
338
  .content-wide {
339
339
  width: 100%;
340
- max-width: 1200px;
340
+ max-width: var(--content-width-wide);
341
341
  margin: 0 auto;
342
342
  }
343
343
 
@@ -413,7 +413,7 @@
413
413
  [data-layout="focused"] #audio-player {
414
414
  position: fixed;
415
415
  bottom: var(--space-4);
416
- left: var(--space-4);
416
+ left: var(--course-edge-inset);
417
417
  right: auto;
418
418
  margin: 0;
419
419
  z-index: var(--z-fixed);
@@ -185,7 +185,7 @@ footer[data-footer-layout="floating"] .nav-controls {
185
185
  [data-layout="focused"] footer {
186
186
  position: fixed;
187
187
  bottom: var(--space-4);
188
- right: var(--space-4);
188
+ right: var(--course-edge-inset);
189
189
  left: auto;
190
190
  width: auto;
191
191
  background: var(--layout-floating-nav-bg);
@@ -247,7 +247,7 @@ footer[data-footer-layout="floating"] .nav-controls {
247
247
  display: flex;
248
248
  align-items: center;
249
249
  justify-content: space-between;
250
- padding: 0 calc(var(--space-4) + env(safe-area-inset-left, 0px)) 0 calc(var(--space-4) + env(safe-area-inset-right, 0px));
250
+ padding: 0 calc(var(--course-edge-inset) + env(safe-area-inset-left, 0px)) 0 calc(var(--course-edge-inset) + env(safe-area-inset-right, 0px));
251
251
  pointer-events: none;
252
252
  }
253
253
 
@@ -492,16 +492,23 @@
492
492
  --btn-transition: background-color var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-interactive), transform var(--transition-interactive), opacity var(--transition-fast), filter var(--transition-fast);
493
493
  --btn-hover-translate-y: var(--motion-lift-sm); /* Shared hover lift for button variants */
494
494
 
495
- /* Layout spacing defaults - overridden by responsive breakpoint classes */
496
- --content-padding: var(--space-10);
497
- --section-padding: var(--space-8);
498
- --header-padding-x: var(--space-6);
499
- --header-padding-y: var(--space-3);
500
- --header-height: 72px;
501
- --footer-padding-x: var(--space-6);
502
- --footer-padding-y: var(--space-3);
503
- --nav-padding-x: var(--space-6);
504
- --nav-padding-y: var(--space-2);
495
+ /* Layout spacing defaults - overridden by responsive breakpoint classes */
496
+ --course-shell-max-width: 1400px;
497
+ --course-shell-max-width-large: 1440px;
498
+ --course-edge-base: var(--space-4);
499
+ --content-width-narrow: 700px;
500
+ --content-width-medium: 900px;
501
+ --content-width-wide: 1200px;
502
+ --focused-content-max-width: 1000px;
503
+ --content-padding: var(--space-10);
504
+ --section-padding: var(--space-8);
505
+ --header-padding-x: var(--space-6);
506
+ --header-padding-y: var(--space-3);
507
+ --header-height: 72px;
508
+ --footer-padding-x: var(--space-6);
509
+ --footer-padding-y: var(--space-3);
510
+ --nav-padding-x: var(--space-6);
511
+ --nav-padding-y: var(--space-2);
505
512
 
506
513
  /* Input System - Unified styling */
507
514
  --input-padding-y: var(--space-3);
@@ -21,6 +21,14 @@
21
21
  --layout-floating-nav-bg: var(--footer-floating-bg);
22
22
  --layout-floating-nav-border: 1px solid var(--footer-floating-border);
23
23
  --layout-floating-nav-shadow: var(--footer-floating-shadow);
24
+
25
+ /* Edge inset for floating UI (audio player, footer pill, FAB toggles,
26
+ edge nav arrows). On wide viewports, add the centered app gutter before
27
+ applying the normal in-frame edge padding. */
28
+ --course-edge-inset: max(
29
+ var(--course-edge-base),
30
+ calc(((100vw - var(--course-shell-max-width)) / 2) + var(--course-edge-base))
31
+ );
24
32
  }
25
33
 
26
34
  /* Default layout is traditional if not specified */
@@ -60,7 +60,7 @@
60
60
  --header-border: var(--nav-edge-border);
61
61
  position: fixed;
62
62
  top: var(--space-4);
63
- left: var(--space-4);
63
+ left: var(--course-edge-inset);
64
64
  z-index: var(--z-fixed);
65
65
  width: var(--nav-edge-control-size);
66
66
  height: var(--nav-edge-control-size);
@@ -103,7 +103,7 @@
103
103
  }
104
104
 
105
105
  [data-layout="focused"] main#content>section {
106
- max-width: 1000px;
106
+ max-width: var(--focused-content-max-width);
107
107
  width: 100%;
108
108
  border: none;
109
109
  box-shadow: none;
@@ -112,7 +112,7 @@
112
112
 
113
113
  /* Slide container centered */
114
114
  [data-layout="focused"] #slide-container {
115
- max-width: 1000px;
115
+ max-width: var(--focused-content-max-width);
116
116
  margin: 0 auto;
117
117
  }
118
118
 
@@ -63,7 +63,7 @@
63
63
  --header-border: var(--nav-edge-border);
64
64
  position: fixed;
65
65
  top: calc(var(--space-4) + env(safe-area-inset-top, 0px));
66
- left: calc(var(--space-4) + env(safe-area-inset-left, 0px));
66
+ left: calc(var(--course-edge-inset) + env(safe-area-inset-left, 0px));
67
67
  z-index: var(--z-fixed);
68
68
  width: var(--nav-edge-control-size);
69
69
  height: var(--nav-edge-control-size);
@@ -17,21 +17,29 @@
17
17
  ============================================================================ */
18
18
 
19
19
  html.bp-min-large-desktop #app {
20
- max-width: 1440px;
20
+ max-width: var(--course-shell-max-width);
21
21
  height: 100%;
22
22
  margin: 0 auto;
23
23
  }
24
24
 
25
+ html.bp-min-large-desktop {
26
+ --course-shell-max-width: var(--course-shell-max-width-large);
27
+ }
28
+
25
29
  /* ============================================================================
26
30
  DESKTOP (<= 1439px)
27
31
  ============================================================================ */
28
32
 
29
33
  html.bp-max-desktop #app {
30
34
  margin: 0;
31
- max-width: none;
35
+ max-width: var(--course-shell-max-width);
32
36
  height: 100%;
33
37
  }
34
38
 
39
+ html.bp-max-desktop {
40
+ --course-shell-max-width: 100vw;
41
+ }
42
+
35
43
  /* ============================================================================
36
44
  TABLET PORTRAIT (<= 1023px)
37
45
  ============================================================================ */
@@ -7,7 +7,7 @@
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1" />
8
8
  <meta name="description" id="page-description" content="" />
9
9
  <link rel="stylesheet" href="css/framework.css" />
10
- <link rel="stylesheet" href="../course/theme.css" />
10
+ <link rel="stylesheet" href="../template/course/theme.css" />
11
11
  <!-- Skip link is styled via framework.css for accessibility -->
12
12
  </head>
13
13
 
@@ -172,4 +172,4 @@
172
172
  <script type="module" src="./js/main.js"></script>
173
173
  </body>
174
174
 
175
- </html>
175
+ </html>
@@ -120,7 +120,7 @@ class EventBus {
120
120
  // suppress to prevent infinite cascade
121
121
  if (isErrorEvent) {
122
122
  if (this._emittingError) {
123
- console.warn(`[EventBus] Suppressed recursive error event: ${event}`);
123
+ logger.warn(`[EventBus] Suppressed recursive error event: ${event}`);
124
124
  return false;
125
125
  }
126
126
  this._emittingError = true;
@@ -507,8 +507,7 @@ export class Scorm2004Driver extends ScormDriverBase {
507
507
  const value = this._scorm.get(key);
508
508
  const errorCode = this._scorm.debug.getCode();
509
509
 
510
- // Error 403 = "Data Model Element Value Not Initialized" - expected for optional fields
511
- if (errorCode === 403) {
510
+ if (this._isOptionalReadError(errorCode)) {
512
511
  return null;
513
512
  }
514
513
 
@@ -561,18 +560,12 @@ export class Scorm2004Driver extends ScormDriverBase {
561
560
  * Populates the CMI cache at init time. Single LMS read pass.
562
561
  */
563
562
  _populateCache() {
564
- // Helper: read a CMI value via strict _getValue, but tolerate error 403
565
- // ("Data Model Element Value Not Initialized") which strict LMSes like
566
- // SCORM Cloud return for unset elements on a fresh session.
567
- // Any other SCORM error still throws through _getValue's normal path.
563
+ // Startup cache hydration reads LMS state opportunistically. Some LMS
564
+ // adapters report supported-but-empty or unavailable optional fields as
565
+ // SCORM errors, so use the optional read path here and keep strict reads
566
+ // for required operations.
568
567
  const getOrDefault = (key, fallback) => {
569
- try {
570
- return this._getValue(key) || fallback;
571
- } catch (e) {
572
- const code = this._scorm.debug.getCode();
573
- if (code === 403) return fallback;
574
- throw e;
575
- }
568
+ return this._getValueOptional(key) || fallback;
576
569
  };
577
570
 
578
571
  // Read-only scalars (may be uninitialized on first launch)
@@ -712,6 +705,11 @@ export class Scorm2004Driver extends ScormDriverBase {
712
705
  throw new Error(msg);
713
706
  }
714
707
 
708
+ _isOptionalReadError(code) {
709
+ const numericCode = Number(code);
710
+ return numericCode === 401 || numericCode === 403;
711
+ }
712
+
715
713
  // --- Recovery Mode Helpers ---
716
714
 
717
715
  _getValueRecovered(key) {
@@ -739,7 +737,7 @@ export class Scorm2004Driver extends ScormDriverBase {
739
737
  const value = api.GetValue(key);
740
738
  const errCode = api.GetLastError ? parseInt(api.GetLastError(), 10) : 0;
741
739
 
742
- if (errCode === 403) {
740
+ if (this._isOptionalReadError(errCode)) {
743
741
  return null;
744
742
  }
745
743
 
@@ -111,12 +111,25 @@ export function createViewManager(container, scope = 'local') {
111
111
  return element;
112
112
  }
113
113
 
114
- // Check for per-slide override via data-content-width attribute
115
- const dataAttrWidth = element.getAttribute('data-content-width');
116
- const slideConfigWidth = dataAttrWidth;
114
+ const contentWidthValues = new Set(['narrow', 'medium', 'wide', 'full']);
115
+
116
+ // Check for per-slide override via data-content-width attribute. Slide
117
+ // modules commonly return a neutral container with the authored slide as
118
+ // its only child, so support the root and that first slide element.
119
+ const slideConfigWidth = getContentWidthOverride(element);
120
+ if (slideConfigWidth && !contentWidthValues.has(slideConfigWidth)) {
121
+ logger.warn(`[ViewManager] Ignoring invalid data-content-width="${slideConfigWidth}". Expected narrow, medium, wide, or full.`);
122
+ }
123
+
124
+ const globalConfigWidth = courseConfig?.slideDefaults?.contentWidth;
125
+ if (!slideConfigWidth && globalConfigWidth && !contentWidthValues.has(globalConfigWidth)) {
126
+ logger.warn(`[ViewManager] Ignoring invalid slideDefaults.contentWidth="${globalConfigWidth}". Expected narrow, medium, wide, or full.`);
127
+ }
117
128
 
118
129
  // Determine which width to use: per-slide override > global config > no wrapping
119
- const configWidth = slideConfigWidth || courseConfig?.slideDefaults?.contentWidth;
130
+ const configWidth = contentWidthValues.has(slideConfigWidth)
131
+ ? slideConfigWidth
132
+ : (contentWidthValues.has(globalConfigWidth) ? globalConfigWidth : null);
120
133
 
121
134
  if (!configWidth) {
122
135
  // No wrapping configured
@@ -136,6 +149,24 @@ export function createViewManager(container, scope = 'local') {
136
149
  return wrapper;
137
150
  }
138
151
 
152
+ /**
153
+ * Gets a per-slide content width override from the rendered root or from
154
+ * the single authored slide element inside a neutral wrapper.
155
+ * @param {HTMLElement} element - The rendered element
156
+ * @returns {string|null} The requested width, if present
157
+ * @private
158
+ */
159
+ function getContentWidthOverride(element) {
160
+ const rootOverride = element.getAttribute('data-content-width');
161
+ if (rootOverride) return rootOverride;
162
+
163
+ if (element.children?.length === 1) {
164
+ return element.firstElementChild?.getAttribute('data-content-width');
165
+ }
166
+
167
+ return null;
168
+ }
169
+
139
170
  /**
140
171
  * Checks if an element or its children already have a content-width class.
141
172
  * @param {HTMLElement} element - The element to check
@@ -31,13 +31,9 @@ further modified by Philip Hutchison
31
31
  if (typeof define === 'function' && define.amd) {
32
32
  // AMD. Register as an anonymous module.
33
33
  define([], factory);
34
- } else if (typeof module === 'object' && module.exports) {
35
- // Node. Does not work with strict CommonJS, but
36
- // only CommonJS-like environments that support module.exports,
37
- // like Node.
38
- module.exports = factory();
39
34
  } else {
40
- // Browser globals (root is window)
35
+ // Browser/global object. CourseCode imports this vendored copy as ESM,
36
+ // so avoid CommonJS export branches that modern bundlers warn about.
41
37
  root.pipwerks = factory();
42
38
  }
43
39
  }(typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : this, function () {
@@ -927,5 +923,5 @@ further modified by Philip Hutchison
927
923
  // ESM Export for Vite/modern bundlers
928
924
  // Added to enable ES module imports while keeping original UMD intact
929
925
  // =============================================================================
930
- export default (typeof window !== 'undefined' && window.pipwerks) || (typeof global !== 'undefined' && global.pipwerks);
931
- export const SCORM = (typeof window !== 'undefined' && window.pipwerks?.SCORM) || (typeof global !== 'undefined' && global.pipwerks?.SCORM);
926
+ export default globalThis.pipwerks;
927
+ export const SCORM = globalThis.pipwerks?.SCORM;
@@ -43,15 +43,22 @@ import {
43
43
 
44
44
  const __filename = fileURLToPath(import.meta.url);
45
45
  const __dirname = path.dirname(__filename);
46
- // __dirname = framework/scripts, go up two levels to reach scorm_template
46
+ // __dirname = framework/scripts, go up two levels to reach the course project
47
+ // root. Real authored courses use ./course; this framework repo keeps its
48
+ // development fixture at ./template/course.
47
49
  const SCORM_TEMPLATE_DIR = path.resolve(__dirname, '../..');
48
50
  const ROOT_DIR = path.resolve(SCORM_TEMPLATE_DIR, '..');
49
- const COURSE_DIR = path.join(SCORM_TEMPLATE_DIR, 'course');
51
+ const DEFAULT_COURSE_DIR = path.join(SCORM_TEMPLATE_DIR, 'course');
52
+ const FRAMEWORK_DEV_COURSE_DIR = path.join(SCORM_TEMPLATE_DIR, 'template', 'course');
53
+ const COURSE_DIR = fs.existsSync(DEFAULT_COURSE_DIR)
54
+ ? DEFAULT_COURSE_DIR
55
+ : (fs.existsSync(FRAMEWORK_DEV_COURSE_DIR) ? FRAMEWORK_DEV_COURSE_DIR : DEFAULT_COURSE_DIR);
56
+ const COURSE_ROOT_DIR = path.dirname(COURSE_DIR);
50
57
  const ASSETS_DIR = path.join(COURSE_DIR, 'assets');
51
58
  const AUDIO_DIR = path.join(ASSETS_DIR, 'audio');
52
59
 
53
60
  const SLIDES_DIR = path.join(COURSE_DIR, 'slides');
54
- const CACHE_FILE = path.join(SCORM_TEMPLATE_DIR, '.narration-cache.json');
61
+ const CACHE_FILE = path.join(COURSE_ROOT_DIR, '.narration-cache.json');
55
62
 
56
63
  // Parse command line arguments
57
64
  const args = process.argv.slice(2);
@@ -73,6 +80,7 @@ const REBUILD_CACHE = args.includes('--rebuild-cache');
73
80
  function loadEnv() {
74
81
  const searchPaths = [
75
82
  path.join(process.cwd(), '.env'), // Current working directory (most common)
83
+ path.join(COURSE_ROOT_DIR, '.env'), // Course project or framework dev template
76
84
  path.join(SCORM_TEMPLATE_DIR, '.env'), // Template directory
77
85
  path.join(ROOT_DIR, '.env') // Root directory
78
86
  ];
@@ -161,7 +161,7 @@ export function createConfigPanelHandlers(context) {
161
161
  <select data-path="slideDefaults.contentWidth" ${['focused', 'presentation', 'canvas'].includes(configData.layout) ? 'disabled' : ''}>
162
162
  ${widths.map(w => `<option value="${w}" ${configData.slideDefaults?.contentWidth === w ? 'selected' : ''}>${w}</option>`).join('')}
163
163
  </select>
164
- ${['focused', 'presentation', 'canvas'].includes(configData.layout) ? `<span class="config-override-hint" title="${configData.layout === 'focused' ? 'Focused layout uses 1000px width' : configData.layout === 'canvas' ? 'Canvas layout has no framework chrome' : 'Presentation layout uses full viewport'}">override</span>` : ''}
164
+ ${['focused', 'presentation', 'canvas'].includes(configData.layout) ? `<span class="config-override-hint" title="${configData.layout === 'focused' ? 'Focused layout uses --focused-content-max-width' : configData.layout === 'canvas' ? 'Canvas layout has no framework chrome' : 'Presentation layout uses full viewport'}">override</span>` : ''}
165
165
  </div>
166
166
  </div>
167
167
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coursecode",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "description": "Multi-format course authoring framework with CLI tools (SCORM 2004, SCORM 1.2, cmi5, LTI 1.3)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,6 @@
1
+ {
2
+ "@slides/example-welcome.js": "91d353d330166293491f0c34ef8c4f6a",
3
+ "@slides/example-ui-showcase.js#demo-modal": "4f7f4e4b846ec06f86a664ae809192aa",
4
+ "@slides/example-ui-showcase.js#full-player": "4349b331452acab4b97f46d9655cb006",
5
+ "@slides/example-ui-showcase.js#compact-player": "9ab41cdd6b1be5248fb446cee91cd0a2"
6
+ }
@@ -241,6 +241,11 @@ export const courseConfig = {
241
241
  engagement: {
242
242
  required: false
243
243
  },
244
+ audio: {
245
+ src: '@slides/example-welcome.js',
246
+ autoplay: false,
247
+ completionThreshold: 0.95
248
+ },
244
249
  navigation: {
245
250
  sequential: true,
246
251
  controls: {
@@ -833,7 +833,7 @@ export const slide = {
833
833
  data-title="Demo Modal"
834
834
  data-body="#demo-modal-body"
835
835
  data-footer="#demo-modal-footer"
836
- data-audio-src="audio/example-ui-showcase--demo-modal.mp3"
836
+ data-audio-src="@slides/example-ui-showcase.js#demo-modal"
837
837
  data-audio-required="false">
838
838
  Launch Modal with Audio
839
839
  </button>
@@ -1497,7 +1497,7 @@ export const slide = {
1497
1497
  <h4 class="font-bold mb-2">Full Size Player</h4>
1498
1498
  <p class="text-sm mb-3">Complete controls with progress bar, time display, and all playback options.</p>
1499
1499
  <div class="mt-2" data-component="audio-player"
1500
- data-audio-src="audio/example-ui-showcase--full-player.mp3"
1500
+ data-audio-src="@slides/example-ui-showcase.js#full-player"
1501
1501
  data-audio-id="full-player-demo">
1502
1502
  </div>
1503
1503
  </div>
@@ -1507,7 +1507,7 @@ export const slide = {
1507
1507
  <h4 class="font-bold mb-2">Compact Player</h4>
1508
1508
  <p class="text-sm mb-3">Minimal controls (play/pause, restart, mute) - same style used in modals.</p>
1509
1509
  <div class="mt-2" data-component="audio-player"
1510
- data-audio-src="audio/example-ui-showcase--compact-player.mp3"
1510
+ data-audio-src="@slides/example-ui-showcase.js#compact-player"
1511
1511
  data-audio-id="compact-player-demo"
1512
1512
  data-audio-compact="true">
1513
1513
  </div>
@@ -1765,9 +1765,9 @@ export const slide = {
1765
1765
  * - course/assets/audio/example-ui-showcase--compact-player.mp3
1766
1766
  *
1767
1767
  * Reference in slide HTML:
1768
- * - Modal audio: data-audio-src="audio/example-ui-showcase--demo-modal.mp3"
1769
- * - Full player: data-audio-src="audio/example-ui-showcase--full-player.mp3"
1770
- * - Compact player: data-audio-src="audio/example-ui-showcase--compact-player.mp3"
1768
+ * - Modal audio: data-audio-src="@slides/example-ui-showcase.js#demo-modal"
1769
+ * - Full player: data-audio-src="@slides/example-ui-showcase.js#full-player"
1770
+ * - Compact player: data-audio-src="@slides/example-ui-showcase.js#compact-player"
1771
1771
  */
1772
1772
  export const narration = {
1773
1773
  'demo-modal': `
@@ -121,3 +121,13 @@ export const slide = {
121
121
  return container;
122
122
  }
123
123
  };
124
+
125
+ export const narration = `
126
+ Welcome to CourseCode.
127
+
128
+ This opening slide introduces the template course and the main workflow it demonstrates.
129
+
130
+ CourseCode helps training teams, instructional designers, and subject matter experts build interactive learning experiences with AI assistance.
131
+
132
+ Use this course to explore the authoring workflow, preview tools, reusable components, theming options, and LMS-ready publishing process.
133
+ `;
@@ -109,6 +109,17 @@
109
109
  Adjust height, padding, and width of structural UI areas.
110
110
  */
111
111
 
112
+ /* Course shell sizing */
113
+ /* --course-shell-max-width: 1400px; */ /* Outer app frame max width */
114
+ /* --course-shell-max-width-large: 1440px; */ /* Outer app frame max width on large desktop */
115
+ /* --course-edge-base: 1rem; */ /* Floating UI inset within the app frame */
116
+
117
+ /* Slide content widths */
118
+ /* --content-width-narrow: 700px; */ /* .content-narrow max width */
119
+ /* --content-width-medium: 900px; */ /* .content-medium max width */
120
+ /* --content-width-wide: 1200px; */ /* .content-wide max width */
121
+ /* --focused-content-max-width: 1000px; */ /* Focused layout viewport-fit content cap */
122
+
112
123
  /* Header sizing */
113
124
  /* --header-height: 72px; */ /* Header bar height */
114
125
  /* --header-padding-x: 1.5rem; */ /* Header horizontal padding */