coursecode 0.1.5 → 0.1.7

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/bin/cli.js CHANGED
@@ -322,6 +322,7 @@ program
322
322
  .description('Build and deploy course to CourseCode Cloud')
323
323
  .option('--preview', 'Deploy as preview (expires in 7 days)')
324
324
  .option('--password', 'Password-protect preview (interactive prompt)')
325
+ .option('-m, --message <message>', 'Deploy reason (e.g. "Fixed accessibility issues")')
325
326
  .option('--local', 'Use local Cloud instance (http://localhost:3000)')
326
327
  .action(async (options) => {
327
328
  const { deploy, setLocalMode } = await import('../lib/cloud.js');
@@ -298,7 +298,14 @@ main#content {
298
298
  position: relative;
299
299
  flex: 1;
300
300
  overflow-y: auto;
301
- overflow-x: hidden;
301
+ overflow-x: clip;
302
+ }
303
+
304
+ /* Remove top padding when a flush-top hero is present (the hero wants to
305
+ sit directly against the header). Negative-margin tricks can't escape
306
+ the intermediate flex containers, so we remove padding at the source. */
307
+ main#content:has([data-component="hero"].hero-flush-top) {
308
+ padding-top: 0;
302
309
  }
303
310
 
304
311
  /* Content Sections - Clean Card Design with Animation */
@@ -545,9 +545,10 @@ html.bp-max-mobile-landscape[data-layout="article"] #audio-player .audio-player-
545
545
  }
546
546
 
547
547
  html.bp-max-mobile-landscape[data-layout="article"] #audio-player .audio-btn {
548
- width: 36px;
549
- height: 36px;
550
- min-height: 36px;
548
+ width: 44px;
549
+ height: 44px;
550
+ min-height: 44px;
551
+ touch-action: manipulation;
551
552
  }
552
553
 
553
554
  html.bp-max-mobile-landscape[data-layout="article"] #audio-player .audio-progress-container {
@@ -610,19 +610,20 @@ html:not([data-layout="article"]):not([data-layout="focused"]):not([data-layout=
610
610
 
611
611
  html.bp-max-mobile-landscape[data-layout="article"] footer #prevBtn,
612
612
  html.bp-max-mobile-landscape[data-layout="article"] footer #nextBtn {
613
- width: 36px;
614
- max-width: 36px;
615
- height: 36px;
616
- min-height: 36px;
613
+ width: 44px;
614
+ max-width: 44px;
615
+ height: 44px;
616
+ min-height: 44px;
617
617
  padding: 0;
618
618
  overflow: hidden;
619
+ touch-action: manipulation;
619
620
  }
620
621
 
621
622
  html.bp-max-mobile-landscape[data-layout="article"] footer #prevBtn>svg,
622
623
  html.bp-max-mobile-landscape[data-layout="article"] footer #nextBtn>svg {
623
624
  display: block;
624
- width: 18px;
625
- height: 18px;
625
+ width: 20px;
626
+ height: 20px;
626
627
  margin: 0;
627
628
  }
628
629
 
@@ -269,7 +269,10 @@ html.bp-max-mobile-landscape[data-layout="article"] footer {
269
269
  max-width: 100vw;
270
270
  box-sizing: border-box;
271
271
  border-radius: 0;
272
- padding: var(--space-2) var(--space-3);
272
+ padding-top: var(--space-2);
273
+ padding-right: calc(var(--space-3) + env(safe-area-inset-right, 0px));
274
+ padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
275
+ padding-left: calc(var(--space-3) + env(safe-area-inset-left, 0px));
273
276
  display: flex;
274
277
  flex-direction: row;
275
278
  align-items: center;
@@ -289,7 +292,7 @@ html.bp-max-mobile-landscape[data-layout="article"] .nav-controls {
289
292
  html.bp-max-mobile-landscape[data-layout="article"] .nav-nav-buttons {
290
293
  order: 2;
291
294
  margin-left: auto;
292
- margin-right: var(--space-1);
295
+ margin-right: 0;
293
296
  display: flex;
294
297
  flex-direction: row;
295
298
  gap: var(--space-2);
@@ -43,8 +43,10 @@
43
43
 
44
44
  /* Hero content container */
45
45
  [data-component="hero"] .hero-content {
46
+ width: 100%;
46
47
  max-width: 800px;
47
48
  margin: 0 auto;
49
+ box-sizing: border-box;
48
50
  position: relative;
49
51
  z-index: 2;
50
52
  }
@@ -108,6 +110,28 @@
108
110
  border-color: transparent;
109
111
  }
110
112
 
113
+ /* Hero badge color variants - solid backgrounds */
114
+ .hero-badge.hero-badge-primary,
115
+ [data-component="hero"] .hero-badge.hero-badge-primary {
116
+ background: var(--color-primary);
117
+ color: var(--color-white);
118
+ border-color: transparent;
119
+ }
120
+
121
+ .hero-badge.hero-badge-secondary,
122
+ [data-component="hero"] .hero-badge.hero-badge-secondary {
123
+ background: var(--color-secondary);
124
+ color: var(--color-gray-900);
125
+ border-color: transparent;
126
+ }
127
+
128
+ .hero-badge.hero-badge-accent,
129
+ [data-component="hero"] .hero-badge.hero-badge-accent {
130
+ background: var(--color-accent);
131
+ color: var(--color-gray-900);
132
+ border-color: transparent;
133
+ }
134
+
111
135
  [data-component="hero"].hero-overlay .hero-badge,
112
136
  [data-component="hero"].hero-dark .hero-badge {
113
137
  background: var(--color-white-alpha-25);
@@ -139,14 +163,17 @@
139
163
 
140
164
  /* Full bleed - breaks out of container to span full viewport width */
141
165
  [data-component="hero"].hero-full-bleed {
142
- width: 100vw;
166
+ width: auto;
143
167
  margin-left: calc(50% - 50vw);
144
- border-radius: 0; /* Remove rounded corners when full bleed */
168
+ margin-right: calc(50% - 50vw);
169
+ border-radius: 0;
145
170
  }
146
171
 
147
- /* Flush top - removes container top padding (stackable with hero-full-bleed) */
172
+ /* Flush top - hero sits directly against the header.
173
+ Padding removal is handled by main#content:has() in 02-layout.css.
174
+ This class is retained as the semantic marker that triggers the :has() rule. */
148
175
  [data-component="hero"].hero-flush-top {
149
- margin-top: calc(-1 * var(--content-padding, var(--space-10)));
176
+ margin-top: 0;
150
177
  }
151
178
 
152
179
  /* Split hero - Image on one side, content on other */
@@ -83,4 +83,5 @@
83
83
 
84
84
  /* 7. Responsive & Accessibility (load last) */
85
85
  @import './responsive.css';
86
- @import './accessibility.css';
86
+ @import './responsive-structure.css';
87
+ @import './accessibility.css';
@@ -254,5 +254,5 @@ html.bp-max-mobile-landscape[data-layout="article"] .sidebar {
254
254
 
255
255
  /* Add bottom padding to content */
256
256
  html.bp-max-mobile-landscape[data-layout="article"] main#content {
257
- padding-bottom: calc(var(--space-8) + 52px);
257
+ padding-bottom: calc(var(--space-8) + 68px + env(safe-area-inset-bottom, 0px));
258
258
  }
@@ -0,0 +1,210 @@
1
+ /* ============================================================================
2
+ RESPONSIVE STRUCTURE - App shell, header chrome, footer/nav/audio chrome
3
+ ============================================================================
4
+
5
+ PURPOSE:
6
+ - Holds responsive behavior for framework chrome and shell structure.
7
+ - Keeps `responsive.css` focused on shared content/component responsiveness.
8
+
9
+ IMPORTANT:
10
+ - If a layout (article/focused/presentation/traditional) owns a responsive
11
+ behavior in layout/component DATA-LAYOUT rules, generic structure selectors
12
+ here must exclude that layout to avoid cascade conflicts.
13
+ ============================================================================ */
14
+
15
+ /* ============================================================================
16
+ LARGE DESKTOP (>= 1440px)
17
+ ============================================================================ */
18
+
19
+ html.bp-min-large-desktop #app {
20
+ max-width: 1440px;
21
+ height: 100%;
22
+ margin: 0 auto;
23
+ }
24
+
25
+ /* ============================================================================
26
+ DESKTOP (<= 1439px)
27
+ ============================================================================ */
28
+
29
+ html.bp-max-desktop #app {
30
+ margin: 0;
31
+ max-width: none;
32
+ height: 100%;
33
+ }
34
+
35
+ /* ============================================================================
36
+ TABLET PORTRAIT (<= 1023px)
37
+ ============================================================================ */
38
+
39
+ html.bp-max-tablet-portrait #app {
40
+ margin: 0;
41
+ border-radius: 0;
42
+ height: 100%;
43
+ }
44
+
45
+ /* Header at tablet portrait: tighter spacing but keep row layout */
46
+ html.bp-max-tablet-portrait header {
47
+ gap: var(--space-2);
48
+ }
49
+
50
+ html.bp-max-tablet-portrait #app > header h1 {
51
+ font-size: var(--font-size-lg);
52
+ }
53
+
54
+ html.bp-max-tablet-portrait #accessibility-menu {
55
+ right: var(--space-4);
56
+ }
57
+
58
+ /* Footer at tablet portrait: tighter gaps but keep row layout
59
+ Exclude article/focused - these layouts own footer behavior. */
60
+ html.bp-max-tablet-portrait:not([data-layout="article"]):not([data-layout="focused"]) .nav-controls {
61
+ gap: var(--space-2);
62
+ }
63
+
64
+ /* ============================================================================
65
+ MOBILE LANDSCAPE (<= 767px)
66
+ ============================================================================ */
67
+
68
+ html.bp-max-mobile-landscape #app {
69
+ margin: 0;
70
+ border-radius: 0;
71
+ height: 100%;
72
+ }
73
+
74
+ /* Mobile header: compact single-row layout */
75
+ html.bp-max-mobile-landscape header {
76
+ flex-wrap: nowrap;
77
+ justify-content: flex-start;
78
+ gap: var(--space-2);
79
+ }
80
+
81
+ html.bp-max-mobile-landscape #brand {
82
+ flex: 1 1 auto;
83
+ justify-content: flex-start;
84
+ min-width: 0;
85
+ }
86
+
87
+ html.bp-max-mobile-landscape #brand img.logo {
88
+ height: 36px;
89
+ padding: var(--space-1);
90
+ padding-left: 0;
91
+ }
92
+
93
+ html.bp-max-mobile-landscape #brand .brand-title {
94
+ font-size: var(--font-size-base);
95
+ white-space: nowrap;
96
+ overflow: hidden;
97
+ text-overflow: ellipsis;
98
+ }
99
+
100
+ html.bp-max-mobile-landscape #app > header h1 {
101
+ font-size: var(--font-size-base);
102
+ }
103
+
104
+ /* Hide header progress and breadcrumbs on mobile to save space */
105
+ html.bp-max-mobile-landscape .header-progress {
106
+ display: none !important;
107
+ }
108
+
109
+ html.bp-max-mobile-landscape .breadcrumbs {
110
+ display: none !important;
111
+ }
112
+
113
+ /* Generic mobile footer/nav/audio structure (traditional/default-like layouts).
114
+ Exclude article/focused - they define mobile footer/nav/audio in DATA-LAYOUT rules. */
115
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .nav-controls {
116
+ flex-direction: row;
117
+ flex-wrap: nowrap;
118
+ gap: var(--space-2);
119
+ }
120
+
121
+ /* Mobile footer: Previous/Next fill the row, Exit hidden (moved to sidebar) */
122
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .nav-nav-buttons {
123
+ display: flex;
124
+ gap: var(--space-2);
125
+ flex: 1;
126
+ width: auto;
127
+ }
128
+
129
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .nav-nav-buttons button {
130
+ flex: 1;
131
+ width: auto;
132
+ padding: var(--space-2) var(--space-3);
133
+ }
134
+
135
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .nav-exit-button {
136
+ display: none;
137
+ }
138
+
139
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .app-footer button {
140
+ width: auto;
141
+ }
142
+
143
+ /* Mobile footer: stacked layout with audio on top */
144
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .app-footer {
145
+ flex-direction: column;
146
+ gap: var(--space-2);
147
+ }
148
+
149
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .nav-controls {
150
+ width: 100%;
151
+ flex-direction: column;
152
+ gap: var(--space-2);
153
+ }
154
+
155
+ /* Audio player: full width, above nav buttons, compact variant */
156
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) #audio-player {
157
+ width: 100%;
158
+ order: -1;
159
+ margin-left: 0;
160
+ }
161
+
162
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .audio-player-controls {
163
+ width: 100%;
164
+ justify-content: center;
165
+ gap: var(--space-1);
166
+ background: var(--bg-surface);
167
+ border: var(--border-width-md) solid var(--border-default);
168
+ }
169
+
170
+ /* Hide progress bar and time on mobile (compact variant) */
171
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .audio-progress-container,
172
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .audio-time {
173
+ display: none;
174
+ }
175
+
176
+ /* Nav buttons: row layout filling width */
177
+ html.bp-max-mobile-landscape:not([data-layout="article"]):not([data-layout="focused"]) .nav-nav-buttons {
178
+ width: 100%;
179
+ justify-content: center;
180
+ }
181
+
182
+ /* ============================================================================
183
+ MOBILE PORTRAIT (<= 479px)
184
+ ============================================================================ */
185
+
186
+ html.bp-max-mobile-portrait #app {
187
+ margin: 0;
188
+ border-radius: 0;
189
+ height: 100%;
190
+ }
191
+
192
+ /* Mobile portrait: even tighter header */
193
+ html.bp-max-mobile-portrait #brand img.logo {
194
+ height: 32px;
195
+ padding-left: 0;
196
+ }
197
+
198
+ html.bp-max-mobile-portrait #brand .brand-title {
199
+ font-size: var(--font-size-sm);
200
+ }
201
+
202
+ html.bp-max-mobile-portrait #app > header h1 {
203
+ font-size: var(--font-size-sm);
204
+ }
205
+
206
+ /* Exclude article/focused - layout-specific button sizing takes precedence */
207
+ html.bp-max-mobile-portrait:not([data-layout="article"]):not([data-layout="focused"]) .app-footer button {
208
+ padding: var(--space-3) var(--space-4);
209
+ font-size: var(--font-size-sm);
210
+ }
@@ -57,6 +57,14 @@
57
57
  html.bp-max-tablet-portrait - Tablets and below (portrait)
58
58
  html.bp-max-mobile-landscape - Mobile devices (landscape)
59
59
  html.bp-max-mobile-portrait - Mobile devices (portrait)
60
+
61
+ MAINTAINER NOTE (layout-owned responsive behavior):
62
+ - Keep generic responsive.css rules focused on shared component/content patterns.
63
+ - When a layout (article/focused/presentation/traditional) has its own responsive
64
+ rules in framework/css/layouts/* or component DATA-LAYOUT sections, scope
65
+ generic structural rules to exclude that layout to avoid cascade conflicts.
66
+ - High-risk generic selectors: .app-footer, .nav-controls, .nav-nav-buttons,
67
+ .nav-exit-button, #audio-player, .audio-player-controls.
60
68
 
61
69
  LAST UPDATED: 2024-12-10
62
70
  ============================================================================ */
@@ -75,11 +83,7 @@ html.bp-min-large-desktop {
75
83
  --footer-padding-y: var(--space-3);
76
84
  }
77
85
 
78
- html.bp-min-large-desktop #app {
79
- max-width: 1440px;
80
- height: 100%;
81
- margin: 0 auto;
82
- }
86
+ /* App shell sizing moved to responsive-structure.css */
83
87
 
84
88
  /* ============================================================================
85
89
  DESKTOP (<= 1439px)
@@ -95,11 +99,7 @@ html.bp-max-desktop {
95
99
  --footer-padding-y: var(--space-5);
96
100
  }
97
101
 
98
- html.bp-max-desktop #app {
99
- margin: 0;
100
- max-width: none;
101
- height: 100%;
102
- }
102
+ /* App shell sizing moved to responsive-structure.css */
103
103
 
104
104
  /* ============================================================================
105
105
  TABLET LANDSCAPE (<= 1199px)
@@ -135,29 +135,7 @@ html.bp-max-tablet-portrait {
135
135
  --nav-padding-y: var(--space-2);
136
136
  }
137
137
 
138
- html.bp-max-tablet-portrait #app {
139
- margin: 0;
140
- border-radius: 0;
141
- height: 100%;
142
- }
143
-
144
- /* Header at tablet portrait: tighter spacing but keep row layout */
145
- html.bp-max-tablet-portrait header {
146
- gap: var(--space-2);
147
- }
148
-
149
- html.bp-max-tablet-portrait #app>header h1 {
150
- font-size: var(--font-size-lg);
151
- }
152
-
153
- html.bp-max-tablet-portrait #accessibility-menu {
154
- right: var(--space-4);
155
- }
156
-
157
- /* Footer at tablet portrait: tighter gaps but keep row layout */
158
- html.bp-max-tablet-portrait .nav-controls {
159
- gap: var(--space-2);
160
- }
138
+ /* Tablet portrait shell/header/footer chrome responsive rules moved to responsive-structure.css */
161
139
 
162
140
  html.bp-max-tablet-portrait .stats-grid,
163
141
  html.bp-max-tablet-portrait .features-grid {
@@ -210,51 +188,7 @@ html.bp-max-mobile-landscape {
210
188
  --nav-padding-y: var(--space-2);
211
189
  }
212
190
 
213
- html.bp-max-mobile-landscape #app {
214
- margin: 0;
215
- border-radius: 0;
216
- height: 100%;
217
- }
218
-
219
- /* Mobile header: compact single-row layout */
220
- html.bp-max-mobile-landscape header {
221
- flex-wrap: nowrap;
222
- justify-content: flex-start;
223
- gap: var(--space-2);
224
- }
225
-
226
- html.bp-max-mobile-landscape #brand {
227
- flex: 1 1 auto;
228
- justify-content: flex-start;
229
- min-width: 0;
230
- }
231
-
232
- html.bp-max-mobile-landscape #brand img.logo {
233
- height: 36px;
234
- padding: var(--space-1);
235
- padding-left: 0;
236
- }
237
-
238
- html.bp-max-mobile-landscape #brand .brand-title {
239
- font-size: var(--font-size-base);
240
- white-space: nowrap;
241
- overflow: hidden;
242
- text-overflow: ellipsis;
243
- }
244
-
245
- html.bp-max-mobile-landscape #app>header h1 {
246
- font-size: var(--font-size-base);
247
- }
248
-
249
- /* Hide header progress on mobile to save space */
250
- html.bp-max-mobile-landscape .header-progress {
251
- display: none !important;
252
- }
253
-
254
- /* Hide breadcrumbs on mobile */
255
- html.bp-max-mobile-landscape .breadcrumbs {
256
- display: none !important;
257
- }
191
+ /* Mobile shell/header/footer/audio chrome responsive rules moved to responsive-structure.css */
258
192
 
259
193
  /* Tabs: stack vertically on mobile */
260
194
  html.bp-max-mobile-landscape .content-tabs,
@@ -297,75 +231,7 @@ html.bp-max-mobile-landscape .assessment-nav-center {
297
231
  margin-bottom: var(--space-2);
298
232
  }
299
233
 
300
- html.bp-max-mobile-landscape .nav-controls {
301
- flex-direction: row;
302
- flex-wrap: nowrap;
303
- gap: var(--space-2);
304
- }
305
-
306
- /* Mobile footer: Previous/Next fill the row, Exit hidden (moved to sidebar) */
307
- html.bp-max-mobile-landscape .nav-nav-buttons {
308
- display: flex;
309
- gap: var(--space-2);
310
- flex: 1;
311
- width: auto;
312
- }
313
-
314
- html.bp-max-mobile-landscape .nav-nav-buttons button {
315
- flex: 1;
316
- width: auto;
317
- padding: var(--space-2) var(--space-3);
318
- }
319
-
320
- html.bp-max-mobile-landscape .nav-exit-button {
321
- display: none;
322
- }
323
-
324
-
325
-
326
- html.bp-max-mobile-landscape .app-footer button {
327
- width: auto;
328
- }
329
-
330
- /* Mobile footer: stacked layout with audio on top */
331
- html.bp-max-mobile-landscape .app-footer {
332
- flex-direction: column;
333
- gap: var(--space-2);
334
- }
335
-
336
- html.bp-max-mobile-landscape .nav-controls {
337
- width: 100%;
338
- flex-direction: column;
339
- gap: var(--space-2);
340
- }
341
-
342
- /* Audio player: full width, above nav buttons, compact variant */
343
- html.bp-max-mobile-landscape #audio-player {
344
- width: 100%;
345
- order: -1;
346
- margin-left: 0;
347
- }
348
-
349
- html.bp-max-mobile-landscape .audio-player-controls {
350
- width: 100%;
351
- justify-content: center;
352
- /* Apply compact variant styles on mobile */
353
- gap: var(--space-1);
354
- background: var(--bg-surface);
355
- border: var(--border-width-md) solid var(--border-default);
356
- }
357
-
358
- /* Hide progress bar and time on mobile (compact variant) */
359
- html.bp-max-mobile-landscape .audio-progress-container,
360
- html.bp-max-mobile-landscape .audio-time {
361
- display: none;
362
- }
363
-
364
- /* Nav buttons: row layout filling width */
365
- html.bp-max-mobile-landscape .nav-nav-buttons {
366
- width: 100%;
367
- justify-content: center;
368
- }
234
+ /* Generic mobile footer/nav/audio structure moved to responsive-structure.css */
369
235
 
370
236
  html.bp-max-mobile-landscape .review-actions {
371
237
  flex-direction: column;
@@ -394,25 +260,7 @@ html.bp-max-mobile-portrait {
394
260
  --nav-padding-y: var(--space-2);
395
261
  }
396
262
 
397
- html.bp-max-mobile-portrait #app {
398
- margin: 0;
399
- border-radius: 0;
400
- height: 100%;
401
- }
402
-
403
- /* Mobile portrait: even tighter header */
404
- html.bp-max-mobile-portrait #brand img.logo {
405
- height: 32px;
406
- padding-left: 0;
407
- }
408
-
409
- html.bp-max-mobile-portrait #brand .brand-title {
410
- font-size: var(--font-size-sm);
411
- }
412
-
413
- html.bp-max-mobile-portrait #app>header h1 {
414
- font-size: var(--font-size-sm);
415
- }
263
+ /* Mobile portrait shell/header/footer chrome responsive rules moved to responsive-structure.css */
416
264
 
417
265
  html.bp-max-mobile-portrait main#content>section {
418
266
  margin-bottom: var(--space-6);
@@ -428,10 +276,7 @@ html.bp-max-mobile-portrait .interaction {
428
276
  padding: var(--space-5);
429
277
  }
430
278
 
431
- html.bp-max-mobile-portrait .app-footer button {
432
- padding: var(--space-3) var(--space-4);
433
- font-size: var(--font-size-sm);
434
- }
279
+ /* Mobile portrait generic footer button sizing moved to responsive-structure.css */
435
280
 
436
281
  /* Keep traditional footer compact across breakpoints */
437
282
  html[data-layout="traditional"] {
@@ -86,7 +86,6 @@ my-course/
86
86
 
87
87
  **Progressive enhancement:** After import, AI agents or authors can replace image slides with interactive HTML, add assessments, or add engagement tracking.
88
88
 
89
-
90
89
  ## CLI Preview
91
90
 
92
91
  Test your course with a stub LMS wrapper.
@@ -182,7 +181,6 @@ coursecode narration --force # Regenerate all (ignore cache)
182
181
  coursecode narration --dry-run # Preview without generating
183
182
  ```
184
183
 
185
-
186
184
  ## CLI Export Content
187
185
 
188
186
  Extracts reviewable text content from SCORM course source files into structured Markdown.
@@ -320,17 +318,17 @@ coursecode build # Creates dist/ + *_<client>_proxy.zip per client
320
318
 
321
319
  ## Data Persistence
322
320
 
323
-
324
- **Never call SCORM API directly** - use these managers:
321
+ **Never call SCORM API directly** - use the framework APIs exposed on `CourseCode`:
325
322
 
326
323
  | Manager | Purpose | Key Methods |
327
324
  |---------|---------|-------------|
328
- | `StateManager` | Custom persistent data | `.set(key, value)`, `.get(key)` |
329
- | `ObjectiveManager` | Track learning objectives | Auto: built-in criteria + assessment-linked<br>Manual: `.setCompletionStatus(id, status)`, `.setSuccessStatus(id, status)`, `.setScore(id, score)` |
330
- | `InteractionManager` | Record student responses | Auto-logs interactions |
331
- | `FlagManager` | Boolean flags in suspend data | `.setFlag(name, bool)`, `.getFlag(name)` |
332
- | `EngagementManager` | Track content engagement | `.getProgress(slideId)`, `.isSlideComplete(slideId)` |
333
- | `ScoreManager` | Calculate and report scores | Auto: configured in `course-config.js` |
325
+ | `stateManager` | LMS + persistent course state (sole gateway) | `.getDomainState(domain)`, `.setDomainState(domain, value)`, `.reportScore(...)`, `.reportCompletion(...)`, `.setBookmark(...)` |
326
+ | `objectiveManager` | Track learning objectives | Auto: built-in criteria + assessment-linked<br>Manual: `.setCompletionStatus(id, status)`, `.setSuccessStatus(id, status)`, `.setScore(id, score)` |
327
+ | `interactionManager` | Record/report learner interactions | Usually auto-logged by built-in interactions |
328
+ | `flagManager` | Boolean/primitive flags in suspend data | `.setFlag(name, value)`, `.getFlag(name)`, `.removeFlag(name)` |
329
+ | `scoreManager` | Calculate/report course scores | Usually configured in `course-config.js` (auto) |
330
+
331
+ **Note:** Engagement tracking is normally configured in `course-config.js` (`slide.engagement`) and handled automatically by built-in components. Authors typically do not call an `engagementManager` directly.
334
332
 
335
333
  ### Objectives (in `course-config.js`)
336
334
 
@@ -1455,8 +1453,6 @@ parent.postMessage({ type: 'coursecode:resize', height: 400 }, '*');
1455
1453
  ```
1456
1454
 
1457
1455
  **Engagement tracking:** Use `flag` requirement with the key set by the embedded widget.
1458
-
1459
-
1460
1456
  ### Dropdown
1461
1457
 
1462
1458
  ```html
@@ -1511,9 +1507,6 @@ Hotspots open modals by default. Link to accordion with `data-accordion-id="acco
1511
1507
  </div>
1512
1508
  </div>
1513
1509
  ```
1514
-
1515
-
1516
-
1517
1510
  ### Value Display (Reactive Text)
1518
1511
 
1519
1512
  ```html
@@ -118,7 +118,7 @@ Slide visit tracking. Managed by [NavigationState.js](../js/navigation/Navigatio
118
118
 
119
119
  ### `engagement`
120
120
 
121
- Engagement tracking for navigation gating. Managed by [engagement-manager.js](../js/managers/engagement-manager.js).
121
+ Engagement tracking for navigation gating. Managed by [engagement-manager.js](../js/engagement/engagement-manager.js).
122
122
 
123
123
  ```js
124
124
  {