coursecode 0.1.48 → 0.1.49
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
CHANGED
|
@@ -6,6 +6,16 @@ CourseCode creates real project files you can inspect, version, and edit directl
|
|
|
6
6
|
|
|
7
7
|
Bring your own PDFs, Word docs, or PowerPoints, use AI to accelerate authoring, and deploy to any LMS format without vendor lock-in or subscriptions.
|
|
8
8
|
|
|
9
|
+
## View the demo
|
|
10
|
+
|
|
11
|
+
- [View the live CourseCode demo course](https://preview.coursecodecloud.com/preview/coursecode-demo/nTfJU2qvp23P0mguxrrGFZr4FlXfM03N)
|
|
12
|
+
|
|
13
|
+
## Explore the ecosystem
|
|
14
|
+
|
|
15
|
+
- [Build SCORM courses with the framework](https://coursecodeframework.com/scorm/)
|
|
16
|
+
- [Use CourseCode Desktop if you prefer a GUI](https://coursecodedesktop.com/scorm/)
|
|
17
|
+
- [Use CourseCode Cloud for hosted delivery](https://coursecodecloud.com)
|
|
18
|
+
|
|
9
19
|
## Features
|
|
10
20
|
|
|
11
21
|
- **MCP integration**: Works with Claude Code, Codex, Cursor, CourseCode Desktop, and any MCP-capable AI tool — previews, screenshots, linting, and testing without manual file sharing
|
|
@@ -20,6 +20,9 @@ import { createLoginHandlers } from './login-screen.js';
|
|
|
20
20
|
const config = window.STUB_CONFIG || {};
|
|
21
21
|
const LAUNCH_URL = config.launchUrl || '/';
|
|
22
22
|
const START_SLIDE = config.startSlide || null;
|
|
23
|
+
const QUERY = new URLSearchParams(window.location.search);
|
|
24
|
+
const SHOW_HEADER = resolveShowHeader();
|
|
25
|
+
const INITIAL_SKIP_GATING = resolveInitialSkipGating();
|
|
23
26
|
|
|
24
27
|
// State
|
|
25
28
|
let isInitialized = false;
|
|
@@ -44,27 +47,38 @@ function init() {
|
|
|
44
47
|
? createContentViewerHandlers({ initialContent: config.courseContent || null })
|
|
45
48
|
: null;
|
|
46
49
|
|
|
50
|
+
document.body.classList.toggle('stub-player-header-hidden', !SHOW_HEADER);
|
|
51
|
+
|
|
47
52
|
// Header Bar — viewer mode: Review + More menu (skip gating, reset)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
contentPanel
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
if (SHOW_HEADER) {
|
|
54
|
+
createHeaderBarHandlers({
|
|
55
|
+
onToggle: () => {},
|
|
56
|
+
onContent: () => {
|
|
57
|
+
if (contentPanel) {
|
|
58
|
+
contentPanel.classList.toggle('visible');
|
|
59
|
+
if (contentPanel.classList.contains('visible') && !contentLoaded && contentHandlers) {
|
|
60
|
+
contentHandlers.loadContent();
|
|
61
|
+
contentLoaded = true;
|
|
62
|
+
}
|
|
56
63
|
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
64
|
+
},
|
|
65
|
+
onReset: () => doReset(),
|
|
66
|
+
onSkipGating: (enabled) => {
|
|
67
|
+
if (enabled) {
|
|
68
|
+
loadCourse();
|
|
69
|
+
} else {
|
|
70
|
+
doReset();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
initialSkipGating: INITIAL_SKIP_GATING
|
|
74
|
+
});
|
|
75
|
+
} else if (INITIAL_SKIP_GATING !== null) {
|
|
76
|
+
try {
|
|
77
|
+
localStorage.setItem('coursecode-skipGating', INITIAL_SKIP_GATING ? 'true' : 'false');
|
|
78
|
+
} catch {
|
|
79
|
+
// ignore storage failures
|
|
66
80
|
}
|
|
67
|
-
}
|
|
81
|
+
}
|
|
68
82
|
|
|
69
83
|
// Load course if no login screen active
|
|
70
84
|
if (!document.getElementById('stub-player-login-screen')?.classList.contains('visible')) {
|
|
@@ -75,13 +89,39 @@ function init() {
|
|
|
75
89
|
setupOutsideClickListener(contentPanel);
|
|
76
90
|
}
|
|
77
91
|
|
|
92
|
+
function resolveShowHeader() {
|
|
93
|
+
const previewHeader = QUERY.get('previewHeader');
|
|
94
|
+
if (previewHeader === 'hidden') return false;
|
|
95
|
+
if (previewHeader === 'visible') return true;
|
|
96
|
+
|
|
97
|
+
const hideHeader = resolveBooleanQuery('hideHeader');
|
|
98
|
+
if (hideHeader !== null) return !hideHeader;
|
|
99
|
+
|
|
100
|
+
return config.showHeader !== false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveInitialSkipGating() {
|
|
104
|
+
const querySkipGating = resolveBooleanQuery('skipGating');
|
|
105
|
+
if (querySkipGating !== null) return querySkipGating;
|
|
106
|
+
return typeof config.skipGating === 'boolean' ? config.skipGating : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveBooleanQuery(name) {
|
|
110
|
+
const value = QUERY.get(name);
|
|
111
|
+
if (value === null) return null;
|
|
112
|
+
const normalized = value.toLowerCase();
|
|
113
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
|
114
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
78
118
|
// =============================================================================
|
|
79
119
|
// COURSE LOADING & NAVIGATION
|
|
80
120
|
// =============================================================================
|
|
81
121
|
|
|
82
122
|
function loadCourse() {
|
|
83
123
|
const frame = document.getElementById('stub-player-course-frame');
|
|
84
|
-
const skipGating =
|
|
124
|
+
const skipGating = resolveSkipGating();
|
|
85
125
|
let url = LAUNCH_URL;
|
|
86
126
|
if (skipGating) url += (url.includes('?') ? '&' : '?') + 'skipGating=true';
|
|
87
127
|
frame.src = url;
|
|
@@ -99,7 +139,7 @@ function handleFrameLoad() {
|
|
|
99
139
|
win.cmi5 = window.cmi5;
|
|
100
140
|
win.lti = window.lti;
|
|
101
141
|
|
|
102
|
-
if (
|
|
142
|
+
if (resolveSkipGating()) {
|
|
103
143
|
win.__SCORM_PREVIEW_SKIP_GATING = true;
|
|
104
144
|
}
|
|
105
145
|
|
|
@@ -115,6 +155,11 @@ function handleFrameLoad() {
|
|
|
115
155
|
}
|
|
116
156
|
}
|
|
117
157
|
|
|
158
|
+
function resolveSkipGating() {
|
|
159
|
+
if (INITIAL_SKIP_GATING !== null) return INITIAL_SKIP_GATING;
|
|
160
|
+
return document.getElementById('stub-player-skip-gating')?.checked === true;
|
|
161
|
+
}
|
|
162
|
+
|
|
118
163
|
function navigateToSlide(contentWindow, slideIdOrIndex) {
|
|
119
164
|
const maxAttempts = 50;
|
|
120
165
|
let attempts = 0;
|
|
@@ -158,10 +203,13 @@ function resolveSlideId(contentWindow, slideIdOrIndex) {
|
|
|
158
203
|
// =============================================================================
|
|
159
204
|
|
|
160
205
|
function doReset() {
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
206
|
+
// Viewer previews share an origin between the stub player and course iframe.
|
|
207
|
+
// Match the local /__reset route by clearing course/framework progress too.
|
|
208
|
+
let skipGating = null;
|
|
209
|
+
try { skipGating = localStorage.getItem('coursecode-skipGating'); } catch { /* ignore */ }
|
|
210
|
+
try { localStorage.clear(); } catch { /* ignore */ }
|
|
211
|
+
if (skipGating !== null) {
|
|
212
|
+
try { localStorage.setItem('coursecode-skipGating', skipGating); } catch { /* ignore */ }
|
|
165
213
|
}
|
|
166
214
|
window.location.reload();
|
|
167
215
|
}
|
|
@@ -102,9 +102,10 @@ export function generateHeaderBar({ isLive, hasContent }) {
|
|
|
102
102
|
* @param {Function} callbacks.onReset - () => void
|
|
103
103
|
* @param {Function} callbacks.onSkipGating - (enabled) => void
|
|
104
104
|
* @param {Function} callbacks.onStatus - () => void
|
|
105
|
+
* @param {boolean|null} callbacks.initialSkipGating - query/config override for skip-gating state
|
|
105
106
|
*/
|
|
106
107
|
export function createHeaderBarHandlers(callbacks) {
|
|
107
|
-
const { onToggle, onDebug, onConfig, onContent, onInteract, onCatalog, onEdit, onReset, onSkipGating, onStatus } = callbacks;
|
|
108
|
+
const { onToggle, onDebug, onConfig, onContent, onInteract, onCatalog, onEdit, onReset, onSkipGating, onStatus, initialSkipGating = null } = callbacks;
|
|
108
109
|
const closeMoreMenu = () => {
|
|
109
110
|
document.getElementById('stub-player-more-menu')?.classList.remove('visible');
|
|
110
111
|
};
|
|
@@ -211,6 +212,7 @@ export function createHeaderBarHandlers(callbacks) {
|
|
|
211
212
|
if (skipGatingCheckbox) {
|
|
212
213
|
// Restore persisted state (defaults to true / skip on)
|
|
213
214
|
const persistedSkip = (() => {
|
|
215
|
+
if (initialSkipGating !== null) return initialSkipGating;
|
|
214
216
|
try {
|
|
215
217
|
const stored = localStorage.getItem(SKIP_GATING_STORAGE_KEY);
|
|
216
218
|
return stored !== 'false';
|
|
@@ -333,6 +333,15 @@
|
|
|
333
333
|
height: 100%;
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
body.stub-player-header-hidden #stub-player-header {
|
|
337
|
+
display: none;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
body.stub-player-header-hidden #stub-player-course-frame {
|
|
341
|
+
top: 0;
|
|
342
|
+
height: 100%;
|
|
343
|
+
}
|
|
344
|
+
|
|
336
345
|
/* Responsive: icon-only mode on narrow viewports */
|
|
337
346
|
@media (max-width: 820px) {
|
|
338
347
|
#stub-player-header .btn-label {
|
package/lib/stub-player.d.ts
CHANGED
package/lib/stub-player.js
CHANGED
|
@@ -53,12 +53,15 @@ export { escapeHtml };
|
|
|
53
53
|
* @param {boolean} [config.liveReload] - True to enable live reload via SSE
|
|
54
54
|
* @param {string} [config.courseContent] - Markdown/HTML content for the content viewer
|
|
55
55
|
* @param {string|number} [config.startSlide] - Slide ID or index to navigate to on load
|
|
56
|
+
* @param {boolean} [config.showHeader] - False to hide the preview player header
|
|
57
|
+
* @param {boolean} [config.skipGating] - True to bypass navigation locks
|
|
56
58
|
* @returns {string} - Complete HTML for the player page
|
|
57
59
|
*/
|
|
58
60
|
export function generateStubPlayer(config) {
|
|
59
|
-
const { title, launchUrl, storageKey, passwordHash, isLive, liveReload, courseContent, startSlide, isDesktop, moduleBasePath = '/__stub-player' } = config;
|
|
61
|
+
const { title, launchUrl, storageKey, passwordHash, isLive, liveReload, courseContent, startSlide, isDesktop, showHeader = true, skipGating = null, moduleBasePath = '/__stub-player' } = config;
|
|
60
62
|
const hasPassword = !!passwordHash;
|
|
61
63
|
const hasContent = !!courseContent;
|
|
64
|
+
const headerVisible = showHeader !== false;
|
|
62
65
|
|
|
63
66
|
const stubPlayerStyles = loadStyles(isLive);
|
|
64
67
|
|
|
@@ -73,7 +76,7 @@ export function generateStubPlayer(config) {
|
|
|
73
76
|
${stubPlayerStyles}
|
|
74
77
|
</style>
|
|
75
78
|
</head>
|
|
76
|
-
<body>
|
|
79
|
+
<body class="${headerVisible ? '' : 'stub-player-header-hidden'}">
|
|
77
80
|
${hasPassword ? generateLoginScreen({ title: escapeHtml(title) }) : ''}
|
|
78
81
|
|
|
79
82
|
<iframe id="stub-player-course-frame" name="stub-player-course-frame"></iframe>
|
|
@@ -109,6 +112,8 @@ ${stubPlayerStyles}
|
|
|
109
112
|
liveReload: ${liveReload || false},
|
|
110
113
|
startSlide: ${startSlide !== undefined ? JSON.stringify(startSlide) : 'null'},
|
|
111
114
|
courseContent: ${hasContent ? JSON.stringify(courseContent) : 'null'},
|
|
115
|
+
showHeader: ${headerVisible},
|
|
116
|
+
skipGating: ${typeof skipGating === 'boolean' ? skipGating : 'null'},
|
|
112
117
|
isDesktop: ${isDesktop || false},
|
|
113
118
|
isCI: ${!!process.env.CI}
|
|
114
119
|
};
|