coursecode 0.1.57 → 0.1.59
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/README.md +8 -1
- package/bin/cli.js +16 -0
- package/framework/css/components/accordions.css +117 -31
- package/framework/css/layouts/article.css +7 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +4 -2
- package/framework/docs/FRAMEWORK_GUIDE.md +25 -7
- package/framework/docs/USER_GUIDE.md +14 -0
- package/framework/js/app/AppActions.js +17 -23
- package/framework/js/app/AppUI.js +7 -4
- package/framework/js/assessment/AssessmentActions.js +16 -15
- package/framework/js/assessment/AssessmentFactory.js +44 -15
- package/framework/js/automation/api-engagement.js +6 -3
- package/framework/js/components/interactions/drag-drop.js +4 -4
- package/framework/js/components/interactions/fill-in.js +7 -4
- package/framework/js/components/interactions/hotspot.js +2 -2
- package/framework/js/components/interactions/matching.js +4 -3
- package/framework/js/components/ui-components/audio-player.js +30 -8
- package/framework/js/components/ui-components/collapse.js +4 -2
- package/framework/js/components/ui-components/dropdown.js +7 -3
- package/framework/js/components/ui-components/embed-frame.js +5 -1
- package/framework/js/components/ui-components/flip-card.js +12 -6
- package/framework/js/components/ui-components/interactive-image.js +28 -52
- package/framework/js/components/ui-components/lightbox.js +19 -5
- package/framework/js/components/ui-components/modal.js +20 -7
- package/framework/js/components/ui-components/notifications.js +8 -3
- package/framework/js/components/ui-components/video-player.js +46 -20
- package/framework/js/core/event-bus.js +31 -28
- package/framework/js/dev/runtime-linter.js +6 -2
- package/framework/js/drivers/cmi5-driver.js +186 -83
- package/framework/js/drivers/driver-factory.js +7 -1
- package/framework/js/drivers/driver-interface.js +1 -1
- package/framework/js/drivers/http-driver-base.js +6 -0
- package/framework/js/drivers/lti-driver.js +112 -47
- package/framework/js/drivers/proxy-driver.js +347 -49
- package/framework/js/drivers/scorm-12-driver.js +213 -130
- package/framework/js/drivers/scorm-2004-driver.js +32 -19
- package/framework/js/drivers/scorm-driver-base.js +12 -0
- package/framework/js/drivers/standalone-driver.js +139 -0
- package/framework/js/engagement/engagement-trackers.js +38 -13
- package/framework/js/engagement/requirement-strategies.js +5 -1
- package/framework/js/main.js +7 -0
- package/framework/js/managers/assessment-manager.js +16 -1
- package/framework/js/managers/audio-manager.js +50 -16
- package/framework/js/managers/flag-manager.js +5 -1
- package/framework/js/managers/objective-manager.js +15 -7
- package/framework/js/managers/score-manager.js +27 -6
- package/framework/js/managers/video-manager.js +149 -70
- package/framework/js/navigation/NavigationActions.js +18 -4
- package/framework/js/navigation/document-gallery.js +7 -2
- package/framework/js/navigation/navigation-validators.js +3 -2
- package/framework/js/state/lms-connection.js +41 -16
- package/framework/js/state/state-commits.js +10 -5
- package/framework/js/state/state-manager.js +95 -9
- package/framework/js/state/state-validation.js +44 -11
- package/framework/js/utilities/course-helpers.js +3 -2
- package/framework/js/utilities/media-utils.js +28 -0
- package/framework/js/utilities/portable-assets.js +151 -0
- package/framework/js/utilities/ui-initializer.js +79 -5
- package/framework/js/utilities/utilities.js +32 -20
- package/framework/js/utilities/view-manager.js +16 -1
- package/framework/js/validation/scorm-validators.js +69 -0
- package/framework/version.json +2 -2
- package/lib/authoring-api.js +37 -18
- package/lib/build-linter.js +192 -2
- package/lib/build-packaging.js +47 -3
- package/lib/build.js +69 -8
- package/lib/cloud.js +40 -11
- package/lib/convert.js +30 -2
- package/lib/create.js +5 -3
- package/lib/dev.js +1 -1
- package/lib/headless-browser.js +66 -14
- package/lib/manifest/cmi5-manifest.js +26 -4
- package/lib/manifest/lti-tool-config.js +7 -2
- package/lib/manifest/manifest-factory.d.ts +1 -1
- package/lib/manifest/manifest-factory.js +3 -2
- package/lib/manifest/scorm-12-manifest.js +2 -2
- package/lib/manifest/scorm-2004-manifest.js +3 -28
- package/lib/manifest/scorm-proxy-manifest.js +37 -32
- package/lib/manifest/xml-utils.js +10 -0
- package/lib/narration.js +2 -2
- package/lib/portable-html.js +196 -0
- package/lib/preview-export.js +1 -1
- package/lib/preview-server.js +27 -6
- package/lib/project-utils.js +9 -1
- package/lib/proxy-templates/proxy.html +4 -1
- package/lib/proxy-templates/scorm-bridge.js +82 -10
- package/lib/scaffold.js +9 -4
- package/lib/stub-player/lms-api.js +63 -8
- package/package.json +5 -1
- package/schemas/adlcp_rootv1p2.xsd +110 -0
- package/schemas/coursecode_scorm12_package.xsd +5 -0
- package/schemas/coursecode_scorm2004_package.xsd +8 -0
- package/schemas/imscp_rootv1p1p2.xsd +304 -0
- package/template/course/slides/example-ui-showcase.js +1 -1
- package/template/vite.config.js +50 -22
package/README.md
CHANGED
|
@@ -223,7 +223,13 @@ coursecode deploy --repair-binding
|
|
|
223
223
|
coursecode build
|
|
224
224
|
```
|
|
225
225
|
|
|
226
|
-
**Share
|
|
226
|
+
**Share one file with no server**: Create a portable HTML course that recipients can double-click. Local progress restore is best effort and stays in that browser:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
coursecode export html
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Share a hosted review preview**: Export the preview shell for GitHub Pages, Netlify, or another static host:
|
|
227
233
|
|
|
228
234
|
```bash
|
|
229
235
|
coursecode preview --export
|
|
@@ -238,6 +244,7 @@ coursecode preview --export
|
|
|
238
244
|
| `coursecode create <name>` | Create a new course project; `create .` derives a title-cased course title and hyphenated npm name from the current folder |
|
|
239
245
|
| `coursecode init [name]` | Initialize the current directory, optionally with a course title |
|
|
240
246
|
| `coursecode preview` | Preview your course locally |
|
|
247
|
+
| `coursecode export html` | Create one shareable course file that opens without an LMS or server |
|
|
241
248
|
| `coursecode convert` | Convert PDFs, Word, PowerPoint to markdown |
|
|
242
249
|
| `coursecode mcp` | Start the MCP server for AI integration |
|
|
243
250
|
| `coursecode lint` | Validate course structure and content |
|
package/bin/cli.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* coursecode init [name] - Initialize the current directory
|
|
9
9
|
* coursecode dev - Start development server
|
|
10
10
|
* coursecode build - Build course package
|
|
11
|
+
* coursecode export html - Create a portable single-file course
|
|
11
12
|
* coursecode upgrade - Upgrade framework in current project
|
|
12
13
|
* coursecode clean - Remove example files from project
|
|
13
14
|
* coursecode new <type> - Create new slide, assessment, or config
|
|
@@ -123,6 +124,21 @@ program
|
|
|
123
124
|
await build(options);
|
|
124
125
|
});
|
|
125
126
|
|
|
127
|
+
// Standalone export commands
|
|
128
|
+
const exportCommand = program
|
|
129
|
+
.command('export')
|
|
130
|
+
.description('Export a course for use outside an LMS');
|
|
131
|
+
|
|
132
|
+
exportCommand
|
|
133
|
+
.command('html')
|
|
134
|
+
.description('Create one shareable HTML file that opens without a server')
|
|
135
|
+
.option('-o, --output <file>', 'Output HTML file path (defaults to the course title)')
|
|
136
|
+
.option('--no-lint', 'Skip the course lint step')
|
|
137
|
+
.action(async (options) => {
|
|
138
|
+
const { exportPortableHtml } = await import('../lib/portable-html.js');
|
|
139
|
+
await exportPortableHtml(options);
|
|
140
|
+
});
|
|
141
|
+
|
|
126
142
|
// Lint command
|
|
127
143
|
program
|
|
128
144
|
.command('lint')
|
|
@@ -247,8 +247,10 @@
|
|
|
247
247
|
ACCORDION STYLE VARIANTS
|
|
248
248
|
============================================================================
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
Global theme tokens are bridged to data attributes on <html>. Authors may
|
|
251
|
+
override that default on an individual .accordion. Keep every variant rule
|
|
252
|
+
scoped through the accordion container so a local attribute wins over an
|
|
253
|
+
inherited global attribute.
|
|
252
254
|
|
|
253
255
|
USAGE:
|
|
254
256
|
<div class="accordion" data-accordion-style="flush">
|
|
@@ -260,33 +262,51 @@
|
|
|
260
262
|
/* --------------------------------------
|
|
261
263
|
Flush Style - No outer border, dividers only
|
|
262
264
|
-------------------------------------- */
|
|
263
|
-
|
|
265
|
+
:is(
|
|
266
|
+
[data-accordion-style="flush"] .accordion:not([data-accordion-style]),
|
|
267
|
+
.accordion[data-accordion-style="flush"]
|
|
268
|
+
) {
|
|
264
269
|
border: none;
|
|
265
270
|
border-radius: 0;
|
|
266
271
|
box-shadow: none;
|
|
267
272
|
background: transparent;
|
|
268
273
|
}
|
|
269
274
|
|
|
270
|
-
|
|
275
|
+
:is(
|
|
276
|
+
[data-accordion-style="flush"] .accordion:not([data-accordion-style]),
|
|
277
|
+
.accordion[data-accordion-style="flush"]
|
|
278
|
+
) .accordion-item {
|
|
271
279
|
background: transparent;
|
|
272
280
|
border-bottom: 1px solid var(--color-gray-200);
|
|
273
281
|
}
|
|
274
282
|
|
|
275
|
-
|
|
283
|
+
:is(
|
|
284
|
+
[data-accordion-style="flush"] .accordion:not([data-accordion-style]),
|
|
285
|
+
.accordion[data-accordion-style="flush"]
|
|
286
|
+
) .accordion-item:last-child {
|
|
276
287
|
border-bottom: 1px solid var(--color-gray-200);
|
|
277
288
|
}
|
|
278
289
|
|
|
279
|
-
|
|
290
|
+
:is(
|
|
291
|
+
[data-accordion-style="flush"] .accordion:not([data-accordion-style]),
|
|
292
|
+
.accordion[data-accordion-style="flush"]
|
|
293
|
+
) .accordion-button {
|
|
280
294
|
padding-left: 0;
|
|
281
295
|
padding-right: 0;
|
|
282
296
|
}
|
|
283
297
|
|
|
284
|
-
|
|
298
|
+
:is(
|
|
299
|
+
[data-accordion-style="flush"] .accordion:not([data-accordion-style]),
|
|
300
|
+
.accordion[data-accordion-style="flush"]
|
|
301
|
+
) .accordion-body {
|
|
285
302
|
padding-left: 0;
|
|
286
303
|
padding-right: 0;
|
|
287
304
|
}
|
|
288
305
|
|
|
289
|
-
|
|
306
|
+
:is(
|
|
307
|
+
[data-accordion-style="flush"] .accordion:not([data-accordion-style]),
|
|
308
|
+
.accordion[data-accordion-style="flush"]
|
|
309
|
+
) .accordion-content {
|
|
290
310
|
background: transparent;
|
|
291
311
|
}
|
|
292
312
|
|
|
@@ -295,7 +315,10 @@
|
|
|
295
315
|
/* --------------------------------------
|
|
296
316
|
Separated Style - Gap between items
|
|
297
317
|
-------------------------------------- */
|
|
298
|
-
|
|
318
|
+
:is(
|
|
319
|
+
[data-accordion-style="separated"] .accordion:not([data-accordion-style]),
|
|
320
|
+
.accordion[data-accordion-style="separated"]
|
|
321
|
+
) {
|
|
299
322
|
border: none;
|
|
300
323
|
border-radius: 0;
|
|
301
324
|
box-shadow: none;
|
|
@@ -305,18 +328,27 @@
|
|
|
305
328
|
gap: var(--space-3);
|
|
306
329
|
}
|
|
307
330
|
|
|
308
|
-
|
|
331
|
+
:is(
|
|
332
|
+
[data-accordion-style="separated"] .accordion:not([data-accordion-style]),
|
|
333
|
+
.accordion[data-accordion-style="separated"]
|
|
334
|
+
) .accordion-item {
|
|
309
335
|
border: 1px solid var(--color-gray-200);
|
|
310
336
|
border-radius: var(--radius-lg);
|
|
311
337
|
overflow: hidden;
|
|
312
338
|
box-shadow: var(--shadow-sm);
|
|
313
339
|
}
|
|
314
340
|
|
|
315
|
-
|
|
341
|
+
:is(
|
|
342
|
+
[data-accordion-style="separated"] .accordion:not([data-accordion-style]),
|
|
343
|
+
.accordion[data-accordion-style="separated"]
|
|
344
|
+
) .accordion-item:last-child {
|
|
316
345
|
border-bottom: 1px solid var(--color-gray-200);
|
|
317
346
|
}
|
|
318
347
|
|
|
319
|
-
|
|
348
|
+
:is(
|
|
349
|
+
[data-accordion-style="separated"] .accordion:not([data-accordion-style]),
|
|
350
|
+
.accordion[data-accordion-style="separated"]
|
|
351
|
+
) .accordion-button:not(.collapsed) {
|
|
320
352
|
border-bottom: none;
|
|
321
353
|
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
322
354
|
}
|
|
@@ -326,56 +358,86 @@
|
|
|
326
358
|
/* --------------------------------------
|
|
327
359
|
Minimal Style - No borders, subtle hover
|
|
328
360
|
-------------------------------------- */
|
|
329
|
-
|
|
361
|
+
:is(
|
|
362
|
+
[data-accordion-style="minimal"] .accordion:not([data-accordion-style]),
|
|
363
|
+
.accordion[data-accordion-style="minimal"]
|
|
364
|
+
) {
|
|
330
365
|
border: none;
|
|
331
366
|
border-radius: 0;
|
|
332
367
|
box-shadow: none;
|
|
333
368
|
background: transparent;
|
|
334
369
|
}
|
|
335
370
|
|
|
336
|
-
|
|
371
|
+
:is(
|
|
372
|
+
[data-accordion-style="minimal"] .accordion:not([data-accordion-style]),
|
|
373
|
+
.accordion[data-accordion-style="minimal"]
|
|
374
|
+
) .accordion-item {
|
|
337
375
|
background: transparent;
|
|
338
376
|
border-bottom: none;
|
|
339
377
|
}
|
|
340
378
|
|
|
341
|
-
|
|
379
|
+
:is(
|
|
380
|
+
[data-accordion-style="minimal"] .accordion:not([data-accordion-style]),
|
|
381
|
+
.accordion[data-accordion-style="minimal"]
|
|
382
|
+
) .accordion-button {
|
|
342
383
|
padding: var(--space-3) var(--space-4);
|
|
343
384
|
border-radius: var(--radius-md);
|
|
344
385
|
}
|
|
345
386
|
|
|
346
|
-
|
|
387
|
+
:is(
|
|
388
|
+
[data-accordion-style="minimal"] .accordion:not([data-accordion-style]),
|
|
389
|
+
.accordion[data-accordion-style="minimal"]
|
|
390
|
+
) .accordion-button:hover {
|
|
347
391
|
background: var(--bg-muted);
|
|
348
392
|
}
|
|
349
393
|
|
|
350
|
-
|
|
394
|
+
:is(
|
|
395
|
+
[data-accordion-style="minimal"] .accordion:not([data-accordion-style]),
|
|
396
|
+
.accordion[data-accordion-style="minimal"]
|
|
397
|
+
) .accordion-button:not(.collapsed) {
|
|
351
398
|
background: var(--color-primary-alpha-05);
|
|
352
399
|
border-bottom: none;
|
|
353
400
|
}
|
|
354
401
|
|
|
355
|
-
|
|
402
|
+
:is(
|
|
403
|
+
[data-accordion-style="minimal"] .accordion:not([data-accordion-style]),
|
|
404
|
+
.accordion[data-accordion-style="minimal"]
|
|
405
|
+
) .accordion-content {
|
|
356
406
|
background: transparent;
|
|
357
407
|
}
|
|
358
408
|
|
|
359
|
-
|
|
409
|
+
:is(
|
|
410
|
+
[data-accordion-style="minimal"] .accordion:not([data-accordion-style]),
|
|
411
|
+
.accordion[data-accordion-style="minimal"]
|
|
412
|
+
) .accordion-body {
|
|
360
413
|
padding: var(--space-2) var(--space-4) var(--space-4);
|
|
361
414
|
}
|
|
362
415
|
|
|
363
416
|
/* Dark mode for minimal */
|
|
364
|
-
|
|
417
|
+
:is(
|
|
418
|
+
[data-theme="dark"][data-accordion-style="minimal"] .accordion:not([data-accordion-style]),
|
|
419
|
+
[data-theme="dark"] .accordion[data-accordion-style="minimal"]
|
|
420
|
+
) .accordion-button:not(.collapsed) {
|
|
365
421
|
background: var(--color-primary-alpha-20);
|
|
366
422
|
}
|
|
367
423
|
|
|
368
424
|
/* --------------------------------------
|
|
369
425
|
Boxed Style - Each item is a bordered box
|
|
370
426
|
-------------------------------------- */
|
|
371
|
-
|
|
427
|
+
:is(
|
|
428
|
+
[data-accordion-style="boxed"] .accordion:not([data-accordion-style]),
|
|
429
|
+
.accordion[data-accordion-style="boxed"]
|
|
430
|
+
) {
|
|
372
431
|
border: none;
|
|
373
432
|
border-radius: 0;
|
|
374
433
|
box-shadow: none;
|
|
375
434
|
background: transparent;
|
|
376
435
|
}
|
|
377
436
|
|
|
378
|
-
|
|
437
|
+
:is(
|
|
438
|
+
[data-accordion-style="boxed"] .accordion:not([data-accordion-style]),
|
|
439
|
+
.accordion[data-accordion-style="boxed"]
|
|
440
|
+
) .accordion-item {
|
|
379
441
|
border: 2px solid var(--color-gray-200);
|
|
380
442
|
border-radius: var(--radius-lg);
|
|
381
443
|
margin-bottom: var(--space-3);
|
|
@@ -383,38 +445,62 @@
|
|
|
383
445
|
background: var(--bg-surface);
|
|
384
446
|
}
|
|
385
447
|
|
|
386
|
-
|
|
448
|
+
:is(
|
|
449
|
+
[data-accordion-style="boxed"] .accordion:not([data-accordion-style]),
|
|
450
|
+
.accordion[data-accordion-style="boxed"]
|
|
451
|
+
) .accordion-item:last-child {
|
|
387
452
|
margin-bottom: 0;
|
|
388
453
|
}
|
|
389
454
|
|
|
390
|
-
|
|
455
|
+
:is(
|
|
456
|
+
[data-accordion-style="boxed"] .accordion:not([data-accordion-style]),
|
|
457
|
+
.accordion[data-accordion-style="boxed"]
|
|
458
|
+
) .accordion-button {
|
|
391
459
|
background: var(--bg-subtle);
|
|
392
460
|
}
|
|
393
461
|
|
|
394
|
-
|
|
462
|
+
:is(
|
|
463
|
+
[data-accordion-style="boxed"] .accordion:not([data-accordion-style]),
|
|
464
|
+
.accordion[data-accordion-style="boxed"]
|
|
465
|
+
) .accordion-button:hover {
|
|
395
466
|
background: var(--bg-muted);
|
|
396
467
|
}
|
|
397
468
|
|
|
398
|
-
|
|
469
|
+
:is(
|
|
470
|
+
[data-accordion-style="boxed"] .accordion:not([data-accordion-style]),
|
|
471
|
+
.accordion[data-accordion-style="boxed"]
|
|
472
|
+
) .accordion-button:not(.collapsed) {
|
|
399
473
|
background: var(--color-primary);
|
|
400
474
|
color: var(--color-white);
|
|
401
475
|
border-bottom: none;
|
|
402
476
|
}
|
|
403
477
|
|
|
404
|
-
|
|
478
|
+
:is(
|
|
479
|
+
[data-accordion-style="boxed"] .accordion:not([data-accordion-style]),
|
|
480
|
+
.accordion[data-accordion-style="boxed"]
|
|
481
|
+
) .accordion-button:not(.collapsed) .accordion-icon {
|
|
405
482
|
color: var(--color-white);
|
|
406
483
|
}
|
|
407
484
|
|
|
408
|
-
|
|
485
|
+
:is(
|
|
486
|
+
[data-accordion-style="boxed"] .accordion:not([data-accordion-style]),
|
|
487
|
+
.accordion[data-accordion-style="boxed"]
|
|
488
|
+
) .accordion-content {
|
|
409
489
|
border-top: 2px solid var(--color-gray-200);
|
|
410
490
|
}
|
|
411
491
|
|
|
412
492
|
/* Dark mode for boxed */
|
|
413
|
-
|
|
493
|
+
:is(
|
|
494
|
+
[data-theme="dark"][data-accordion-style="boxed"] .accordion:not([data-accordion-style]),
|
|
495
|
+
[data-theme="dark"] .accordion[data-accordion-style="boxed"]
|
|
496
|
+
) .accordion-item {
|
|
414
497
|
border-color: var(--border-default);
|
|
415
498
|
}
|
|
416
499
|
|
|
417
|
-
|
|
500
|
+
:is(
|
|
501
|
+
[data-theme="dark"][data-accordion-style="boxed"] .accordion:not([data-accordion-style]),
|
|
502
|
+
[data-theme="dark"] .accordion[data-accordion-style="boxed"]
|
|
503
|
+
) .accordion-content {
|
|
418
504
|
border-top-color: var(--border-default);
|
|
419
505
|
}
|
|
420
506
|
|
|
@@ -707,4 +793,4 @@
|
|
|
707
793
|
[data-theme="dark"] .accordion-badge-outline {
|
|
708
794
|
border-color: rgba(var(--color-primary-rgb), 0.4);
|
|
709
795
|
color: var(--color-primary-light);
|
|
710
|
-
}
|
|
796
|
+
}
|
|
@@ -101,6 +101,13 @@
|
|
|
101
101
|
margin: 0 auto;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/* Breadcrumbs are positioned absolutely and do not reserve layout space.
|
|
105
|
+
Add clearance only while they are visible; mobile hides them entirely. */
|
|
106
|
+
html:not(.bp-max-mobile-landscape)[data-layout="article"]
|
|
107
|
+
.breadcrumbs:not(.hidden):not([hidden]) ~ #slide-container {
|
|
108
|
+
padding-top: var(--space-4);
|
|
109
|
+
}
|
|
110
|
+
|
|
104
111
|
/* Footer styling moved to components/footer.css - DATA-LAYOUT VARIANTS section */
|
|
105
112
|
|
|
106
113
|
/* Audio player styling moved to components/audio-player.css - DATA-LAYOUT VARIANTS section */
|
|
@@ -618,7 +618,7 @@ createNumericQuestion({
|
|
|
618
618
|
```javascript
|
|
619
619
|
createHotspotQuestion({
|
|
620
620
|
id: 'hot1', prompt: 'Click the correct region:',
|
|
621
|
-
image: { src: 'assets/images/diagram.png', alt: 'Diagram' },
|
|
621
|
+
image: { src: 'assets/images/diagram.png', alt: 'Diagram' }, // Relative to course/assets/
|
|
622
622
|
hotspots: [
|
|
623
623
|
// pos = [x%, y%, width%, height%]
|
|
624
624
|
{ id: 'zone1', pos: [10, 20, 15, 10], correct: true, label: 'Correct Zone', feedback: 'Good choice!' },
|
|
@@ -627,6 +627,8 @@ createHotspotQuestion({
|
|
|
627
627
|
});
|
|
628
628
|
```
|
|
629
629
|
|
|
630
|
+
Interaction APIs such as `createHotspotQuestion()` resolve relative asset paths against `course/assets/`. In direct HTML attributes such as `<img src>`, use the packaged path `course/assets/...` as shown in the interactive-image example below. Declarative components may document their own path base; for example, lightbox `href` values are relative to `course/` and normally begin with `assets/`.
|
|
631
|
+
|
|
630
632
|
**Appearance themes**: `'correct'`, `'incorrect'`, `'primary'`, `'accent'` — or provide custom `appearance` object.
|
|
631
633
|
|
|
632
634
|
### Sequencing
|
|
@@ -1477,7 +1479,7 @@ parent.postMessage({ type: 'coursecode:resize', height: 400 }, '*');
|
|
|
1477
1479
|
|
|
1478
1480
|
```html
|
|
1479
1481
|
<div data-component="interactive-image" id="diagram">
|
|
1480
|
-
<img src="assets/images/diagram.png" alt="Diagram" />
|
|
1482
|
+
<img src="course/assets/images/diagram.png" alt="Diagram" />
|
|
1481
1483
|
<button data-hotspot-id="zone1" data-title="Component A" data-body="Description here..."
|
|
1482
1484
|
class="interactive-image-hotspot" style="top: 20%; left: 30%;">A</button>
|
|
1483
1485
|
<button data-hotspot-id="zone2" data-title="Component B" data-body="More details..."
|
|
@@ -72,7 +72,7 @@ The framework supports multiple LMS formats:
|
|
|
72
72
|
| `scorm1.2` | SCORM 1.2 (legacy LMS) | 4KB suspend_data (Strict Diet Mode found in `scorm-12-driver.js`) |
|
|
73
73
|
| `lti` | LTI 1.3 (remote-hosted, JWT launch) | Unlimited (host-dependent) |
|
|
74
74
|
|
|
75
|
-
> **Note on SCORM 1.2 Strict Diet Mode:**
|
|
75
|
+
> **Note on SCORM 1.2 Strict Diet Mode:** The framework preserves the complete CourseCode state and compresses it into an ASCII-safe representation. If the compressed value still exceeds SCORM 1.2's 4,096-character limit, the write fails loudly instead of silently discarding learner state. Courses that exceed the limit must reduce persisted data or use SCORM 2004, cmi5, or LTI.
|
|
76
76
|
|
|
77
77
|
**Set format in `course-config.js`** (for local CLI builds):
|
|
78
78
|
```javascript
|
|
@@ -138,11 +138,20 @@ The browser only downloads the one chunk matching the meta tag. Unused driver ch
|
|
|
138
138
|
|---------|--------|---------|
|
|
139
139
|
| `coursecode build` | `dist/` | Universal build + format manifest + meta tag stamped |
|
|
140
140
|
| `coursecode build` (with `PACKAGE=true`) | `dist/` + ZIP | Same + format-specific ZIP for LMS upload |
|
|
141
|
+
| `coursecode export html` | `<course-title>.html` | One portable learner file with an embedded standalone driver and course assets; no LMS manifest or server |
|
|
141
142
|
| `coursecode preview --export` | `course-preview/` | Copy of `dist/` wrapped in stub player (for Netlify/GitHub Pages) |
|
|
142
143
|
| `coursecode deploy` | Uploads `dist/` | Cloud hosts universal build, assembles format ZIPs on demand. Flags: `--promote` (force live), `--stage` (force staged), `--preview` (preview-only: production untouched, preview always moved), `--repair-binding` (clear stale local cloud binding first if the remote course was deleted). `--promote`/`--stage` are mutually exclusive; `--preview` stacks with either. **GitHub-linked courses**: production deploy blocked; only `--preview` allowed (see GitHub Source Guard below). |
|
|
143
144
|
|
|
144
145
|
The ZIP never includes preview/stub player assets. Preview is a separate concern (see below).
|
|
145
146
|
|
|
147
|
+
#### Portable HTML Architecture
|
|
148
|
+
|
|
149
|
+
`coursecode export html` is intentionally smaller than preview export. It builds the normal learner runtime with `LMS_FORMAT=standalone`, uses `vite-plugin-singlefile` to inline generated JavaScript and CSS, embeds copied files from `course/assets/` plus the document-gallery manifest as data URLs, and omits LMS schemas, manifests, packaging, and the preview/stub-player shell.
|
|
150
|
+
|
|
151
|
+
The standalone driver keeps the same semantic state contract as the LMS drivers. It saves bookmark, completion, success, score, and CourseCode suspend state to browser `localStorage`, keyed by course title and version. Restore is best effort because browsers define storage behavior for `file://` documents; when storage is unavailable, the course remains usable and keeps state only for the open session.
|
|
152
|
+
|
|
153
|
+
Portable HTML is for direct file sharing, offline reference/training, USB delivery, and unmanaged kiosks. It does not provide learner identity, centralized reporting, cross-device sync, tamper-resistant records, or an administrator view. Remote embeds such as YouTube still require network access, and media-heavy courses can produce large HTML files.
|
|
154
|
+
|
|
146
155
|
#### Re-Stamping for Different Formats
|
|
147
156
|
|
|
148
157
|
`lib/build-packaging.js` exports `stampFormat(html, format)` for re-stamping the meta tag:
|
|
@@ -166,7 +175,9 @@ Both are pure Node utilities.
|
|
|
166
175
|
|------|---------|
|
|
167
176
|
| `framework/js/state/lms-connection.js` | `getLMSFormat()` — runtime priority chain (meta → env → default) |
|
|
168
177
|
| `framework/js/drivers/driver-factory.js` | Dynamic `import()` switch — loads one driver at runtime |
|
|
178
|
+
| `framework/js/drivers/standalone-driver.js` | Best-effort local persistence for portable HTML |
|
|
169
179
|
| `lib/build-packaging.js` | `stampFormat()` — pure string meta tag re-stamping; `stampFormatInHtml()` — file-based wrapper |
|
|
180
|
+
| `lib/portable-html.js` | Single-file Vite plugin wrapper, copied-asset embedding, and export validation |
|
|
170
181
|
| `lib/manifest/manifest-factory.js` | `generateManifest()` — generates format-specific manifests |
|
|
171
182
|
| Both `vite.config.js` files | Post-build `closeBundle` hook stamps meta tag into `dist/index.html` |
|
|
172
183
|
|
|
@@ -385,7 +396,7 @@ After images are acquired, the module:
|
|
|
385
396
|
| `state-validation.js` | State hydration, migration, validation |
|
|
386
397
|
| `transaction-log.js` | Ring buffer for debugging |
|
|
387
398
|
|
|
388
|
-
> **
|
|
399
|
+
> **Persistence encoding:** SCORM 2004 uses `lz-string` UTF-16 compression. SCORM 1.2 uses an ASCII-safe `CC12:` encoded representation because legacy LMS implementations may reject UTF-16 suspend data. cmi5 and LTI persist structured JSON through their HTTP state endpoints. These format differences are transparent to framework consumers.
|
|
389
400
|
|
|
390
401
|
#### Domain API
|
|
391
402
|
|
|
@@ -474,7 +485,7 @@ Migrations run sequentially (v1→v2→v3), so each only handles one version jum
|
|
|
474
485
|
|
|
475
486
|
## Logging & Error Handling
|
|
476
487
|
|
|
477
|
-
**
|
|
488
|
+
**Logger and event communication are integrated:** use `logger.*` for observability. `logger.warn()`, `logger.error()`, and `logger.fatal()` automatically emit structured `log:warn` or `log:error` events. Emit an additional domain event only when another framework component needs to react to that specific failure; do not duplicate a `log:error` event manually.
|
|
478
489
|
|
|
479
490
|
### Logger (Observability)
|
|
480
491
|
|
|
@@ -495,7 +506,7 @@ Global as `logger` or `window.logger` (no import needed). Auto-filtered by envir
|
|
|
495
506
|
|------|------|----------|---------|
|
|
496
507
|
| **Tier 1** | Contract violations, programming errors | Always `throw` | Wrong parameter types, API misuse, double-init |
|
|
497
508
|
| **Tier 2** | Runtime/init failures (missing DOM, bad config) | `logger.fatal()` → throws in DEV, logs+degrades in PROD | Missing container element, invalid data attributes |
|
|
498
|
-
| **Tier 3** |
|
|
509
|
+
| **Tier 3** | Recoverable background or unload failures | Structured logger event; retain/retry or emit a domain event when actionable | Debounced commit retry, best-effort unload save |
|
|
499
510
|
|
|
500
511
|
**`logger.fatal(message, context)`** — Tier 2 handler in `framework/js/utilities/logger.js`:
|
|
501
512
|
|
|
@@ -515,7 +526,14 @@ All error events follow the standardized shape:
|
|
|
515
526
|
{ domain, operation, message, stack?, context? }
|
|
516
527
|
```
|
|
517
528
|
|
|
518
|
-
**When to catch
|
|
529
|
+
**When to catch depends on who can still act on the failure:**
|
|
530
|
+
|
|
531
|
+
- **Critical awaited work** (assessment submission, completion, explicit flush/terminate): log or clean up, then re-throw so the caller cannot treat the operation as successful.
|
|
532
|
+
- **Detached background work** (debounced commits): do not create an unhandled rejection. Log with structured context, preserve dirty state, and retry or emit an actionable domain event.
|
|
533
|
+
- **Unload/pagehide work**: the browser is leaving and cannot await a recovery path. Use the driver's best-effort unload transport and log any synchronous or asynchronous failure with structured context; do not re-throw into the abandoned event handler.
|
|
534
|
+
- **Expected capability probes**: a documented unsupported optional LMS field may degrade to a conservative default. Do not suppress failures for required resume, completion, score, or assessment fields.
|
|
535
|
+
|
|
536
|
+
Critical-path example:
|
|
519
537
|
|
|
520
538
|
```javascript
|
|
521
539
|
try {
|
|
@@ -633,11 +651,11 @@ Courses linked to GitHub for production deploys are protected from accidental CL
|
|
|
633
651
|
| Layer | Mechanism | Key |
|
|
634
652
|
|-------|-----------|-----|
|
|
635
653
|
| **CLI (local)** | `deploy()` reads `sourceType` from `.coursecoderc.json` | Fast fail before build — no wasted time |
|
|
636
|
-
| **Server (safety net)** |
|
|
654
|
+
| **Server (safety net)** | Finalize endpoint rejects non-preview CLI uploads when `github_repo` is set | Can't bypass via old CLI or `curl` |
|
|
637
655
|
|
|
638
656
|
- **`--preview` always allowed** — useful for testing without touching production pointer
|
|
639
657
|
- **`.coursecoderc.json`** carries `sourceType` and `githubRepo` (committed to repo by cloud's GitHub integration), so anyone cloning gets the guard automatically
|
|
640
|
-
- **Self-healing
|
|
658
|
+
- **Self-healing link state:** Both `status()` and `deploy()` treat the server's `github_repo` as authoritative. They add or refresh local `sourceType`/`githubRepo` metadata when linked and clear it when unlinked. No manual cleanup or repo commit is needed.
|
|
641
659
|
- CLI error code: `github_source_blocked` (structured JSON for Desktop/CI consumers)
|
|
642
660
|
|
|
643
661
|
---
|
|
@@ -51,6 +51,7 @@ A complete guide to creating interactive e-learning courses with AI assistance.
|
|
|
51
51
|
9. [Sharing and Deploying](#sharing-and-deploying)
|
|
52
52
|
- [Sharing Previews](#sharing-previews)
|
|
53
53
|
- [Preview Export Options](#preview-export-options)
|
|
54
|
+
- [Sharing One File Without a Server](#sharing-one-file-without-a-server)
|
|
54
55
|
- [Understanding LMS Formats](#understanding-lms-formats)
|
|
55
56
|
- [Standard Deployment](#standard-deployment)
|
|
56
57
|
- [CDN Deployment (Advanced)](#cdn-deployment-advanced)
|
|
@@ -637,6 +638,19 @@ coursecode preview --export --no-content
|
|
|
637
638
|
- `--nojekyll`: add `.nojekyll` (important for GitHub Pages)
|
|
638
639
|
- `--no-content`: hide the content viewer panel in exported preview
|
|
639
640
|
|
|
641
|
+
### Sharing One File Without a Server
|
|
642
|
+
|
|
643
|
+
For training that will be opened directly from email, a shared drive, USB media, or a local computer, export one portable HTML file:
|
|
644
|
+
|
|
645
|
+
```bash
|
|
646
|
+
coursecode export html
|
|
647
|
+
coursecode export html --output ./Safety_Training.html
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
The recipient double-clicks the `.html` file and uses it in a modern browser. CourseCode embeds the learner runtime and local course assets, so no LMS, web server, install, or ZIP extraction is required. Progress and resume state are saved locally in the browser when `file://` storage is available. If the browser blocks local storage, the course still works but restore is limited to the current session.
|
|
651
|
+
|
|
652
|
+
This export is a good fit for informal training, reference material, stakeholder review, offline delivery, and unmanaged kiosks. It does not provide centralized learner records, identity, administrator reporting, cross-device sync, or certified completion evidence. Internet-hosted content such as YouTube still needs a connection, and large audio/video files make the exported HTML correspondingly large.
|
|
653
|
+
|
|
640
654
|
### Understanding LMS Formats
|
|
641
655
|
|
|
642
656
|
An LMS (Learning Management System) is the platform your organization uses to deliver training — think Cornerstone, Moodle, Canvas, Docebo, etc. CourseCode packages your course in a format your LMS understands.
|
|
@@ -107,40 +107,39 @@ export async function checkCompletion() {
|
|
|
107
107
|
return false;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
successStatus: stateManager.getSuccess() || 'unknown',
|
|
115
|
-
isOnLastSlide: true
|
|
116
|
-
});
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// On a completion slide, so evaluate final status.
|
|
110
|
+
// Always re-evaluate on a completion slide. Completion and success are
|
|
111
|
+
// independent: a learner can complete a first attempt with a failed
|
|
112
|
+
// success status, then pass a permitted retake later in the same or a
|
|
113
|
+
// resumed session.
|
|
121
114
|
const assessmentSlides = await CourseHelpers.getSlidesByType('assessment');
|
|
122
115
|
const assessmentConfigs = await CourseHelpers.getAssessmentConfigs();
|
|
123
116
|
|
|
124
117
|
// Completion is based on submission requirements.
|
|
125
118
|
const assessmentsRequiringSubmission = assessmentSlides.filter(s =>
|
|
126
|
-
assessmentConfigs.get(s
|
|
119
|
+
assessmentConfigs.get(AssessmentManager.resolveAssessmentId(s))?.completionRequirements?.requireSubmission
|
|
127
120
|
);
|
|
128
121
|
|
|
129
122
|
const allSubmitted = assessmentsRequiringSubmission.every(s =>
|
|
130
|
-
AssessmentManager.meetsCompletionRequirements(
|
|
123
|
+
AssessmentManager.meetsCompletionRequirements(
|
|
124
|
+
AssessmentManager.resolveAssessmentId(s),
|
|
125
|
+
{ requireSubmission: true }
|
|
126
|
+
)
|
|
131
127
|
);
|
|
132
128
|
|
|
133
129
|
const completionStatus = allSubmitted ? 'completed' : 'incomplete';
|
|
134
130
|
|
|
135
131
|
// Success is based on passing requirements.
|
|
136
132
|
const assessmentsRequiringPass = assessmentSlides.filter(s =>
|
|
137
|
-
assessmentConfigs.get(s
|
|
133
|
+
assessmentConfigs.get(AssessmentManager.resolveAssessmentId(s))?.completionRequirements?.requirePass
|
|
138
134
|
);
|
|
139
135
|
|
|
140
136
|
let successStatus = 'unknown';
|
|
141
137
|
if (assessmentsRequiringPass.length > 0) {
|
|
142
138
|
const allPassed = assessmentsRequiringPass.every(s =>
|
|
143
|
-
AssessmentManager.meetsCompletionRequirements(
|
|
139
|
+
AssessmentManager.meetsCompletionRequirements(
|
|
140
|
+
AssessmentManager.resolveAssessmentId(s),
|
|
141
|
+
{ requirePass: true }
|
|
142
|
+
)
|
|
144
143
|
);
|
|
145
144
|
successStatus = allPassed ? 'passed' : 'failed';
|
|
146
145
|
// Note: Course score reporting is handled by ScoreManager (configure in course-config.js)
|
|
@@ -188,14 +187,9 @@ function _finalizeCurrentSlideProgress() {
|
|
|
188
187
|
if (currentSlideId) {
|
|
189
188
|
NavigationState.addVisitedSlide(currentSlideId);
|
|
190
189
|
|
|
191
|
-
// Update progress
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const totalSequentialSlides = slides.filter(s =>
|
|
195
|
-
s.navigation?.sequence?.included !== false
|
|
196
|
-
).length;
|
|
197
|
-
stateManager.updateProgressMeasure(totalSequentialSlides);
|
|
198
|
-
}
|
|
190
|
+
// Update progress one final time using the same dynamic sequence rules
|
|
191
|
+
// as navigation (conditional/remedial slides must not inflate it).
|
|
192
|
+
NavigationActions.syncProgressMeasure();
|
|
199
193
|
}
|
|
200
194
|
}
|
|
201
195
|
|
|
@@ -24,6 +24,7 @@ const completionFeedbackTemplate = `
|
|
|
24
24
|
|
|
25
25
|
// Store the active rating interaction instance
|
|
26
26
|
let activeRatingInteraction = null;
|
|
27
|
+
const isStandaloneRuntime = document.querySelector('meta[name="lms-format"]')?.content === 'standalone';
|
|
27
28
|
|
|
28
29
|
const MODAL_DEFINITIONS = {
|
|
29
30
|
exit: {
|
|
@@ -45,8 +46,9 @@ const MODAL_DEFINITIONS = {
|
|
|
45
46
|
title: 'Course Complete!',
|
|
46
47
|
body: `
|
|
47
48
|
<p><strong>Congratulations!</strong> You have successfully completed this course.</p>
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
${isStandaloneRuntime
|
|
50
|
+
? '<p>Your completion status and final score have been saved in this browser.</p><p>When you click "Complete & Exit" below, the session will finish. You can close the file or reopen it later.</p>'
|
|
51
|
+
: '<p>Your completion status and final score have been recorded in the Learning Management System.</p><p>When you click "Complete & Exit" below, this window will close and you will be returned to the LMS.</p>'}
|
|
50
52
|
${completionFeedbackTemplate}
|
|
51
53
|
`,
|
|
52
54
|
footer: `
|
|
@@ -61,8 +63,9 @@ const MODAL_DEFINITIONS = {
|
|
|
61
63
|
postExit: {
|
|
62
64
|
title: 'Session Closed',
|
|
63
65
|
body: `
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
${isStandaloneRuntime
|
|
67
|
+
? '<p>Your progress has been saved in this browser. It is now safe to close this file.</p><p>Open the HTML file again to resume later.</p>'
|
|
68
|
+
: '<p>Your progress has been saved in the LMS. It is now safe to close this window.</p><p>If this window does not close automatically, please close it manually and return to the LMS.</p>'}
|
|
66
69
|
`,
|
|
67
70
|
footer: '',
|
|
68
71
|
config: {
|